惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Apple Machine Learning Research
Apple Machine Learning Research
Y
Y Combinator Blog
博客园 - 【当耐特】
V
Visual Studio Blog
GbyAI
GbyAI
V
V2EX
P
Proofpoint News Feed
Microsoft Azure Blog
Microsoft Azure Blog
Microsoft Security Blog
Microsoft Security Blog
D
DataBreaches.Net
Hugging Face - Blog
Hugging Face - Blog
A
About on SuperTechFans
The Cloudflare Blog
阮一峰的网络日志
阮一峰的网络日志
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
aimingoo的专栏
aimingoo的专栏
B
Blog RSS Feed
量子位
MongoDB | Blog
MongoDB | Blog
有赞技术团队
有赞技术团队
人人都是产品经理
人人都是产品经理
Stack Overflow Blog
Stack Overflow Blog
小众软件
小众软件

谭新宇的博客

源码阅读导引:Ray、Kubernetes、KubeRay 与 Volcano KubeRay 源码阅读:从 RayJob 看组件分工与协作 KubeRay 源码阅读:一次 Pod 变化怎样触发 Reconcile KubeRay 源码阅读:一个 Operator 如何管理 100 个 RayCluster KubeRay 源码阅读:Operator 重启后如何继续工作 Volcano 源码阅读:从 Pod 到作业 Volcano 源码阅读:一次调度怎样满足 Gang 约束 Volcano 源码阅读:多个作业怎样共享与竞争资源 KubeRay 与 Volcano:从 RayJob 到 PodGroup 2025 年终总结:从时序数据库到 AI Infra 的转身 Ray 编译踩坑记:老版本在老系统上的编译之路 让 Ray Distributed Debugger 在 Kuberay 下可用 数据库内核开发 5 年,我从无数坑中学到的 14 个宝贵教训 2024 年终总结:在清华 IoTDB 创业公司中构建起摩尔定律成长节奏 2023 年终总结:从清华 Apache IoTDB 组到创业公司天谋科技 2023 IoTDB 用户大会分享:如何用 IoTDB 监控工具进行深度系统调优 2022 年终总结:记清华硕士的秋招之年 2022 互联网求职经验分享 2022 TiDB Hackathon 产品组最佳校园奖总结 Have we reached consensus on consensus? TiKV 源码阅读三部曲(三)写流程 TiKV 源码阅读三部曲(二)读流程 TiKV 源码阅读三部曲(一)重要模块 Talent-Plan:用 Rust 实现简易 KV 引擎 Talent-Plan:用 Rust 实现 Percolator 算法 分布式事务概述和对应代码框架介绍 Raft 算法和对应代码框架介绍 2021 年终总结:记我在清华 Apache IoTDB 组的成长 2021 Talent Plan KV 学习营结营总结
KubeRay 源码阅读:从 Operator 主备切换到 Ray 服务恢复
谭新宇 · 2026-09-15 · via 谭新宇的博客

demo-job 正在运行,负责它的 Operator Pod 突然退出。已有的 Ray 计算可能继续执行,但谁来补 worker、回写作业状态、清理集群?如果退出的是 head Pod,答案又会不同。

第四篇已经解释了进程重启后怎样依靠已有资源继续协调。这篇接着看备用 Operator 如何获得处理资格,再把故障移到 Kubernetes 控制面和 Ray 节点,分别检查管理流程与计算受什么影响。最后单独用 RayService 分析服务切换时的请求表现。

阅读基线为 KubeRay 6bf05eb17a3e、Ray 3f785c0711b9,依赖 controller-runtime v0.24.1、client-go v0.37.0。引用文件与基线一致;下文是源码分析和有条件的故障推演,没有集群实测数据。

1. 开了选主,为什么仍只有一个 Operator

选主是在多个 Operator 实例中确定由谁负责协调资源;负责协调的实例称为主实例或 leader,其余实例等待接管。选主开关和实际部署几个副本来自不同配置。

这里分析通过 Helm 安装 Operator 时使用的配置。Helm 将 Kubernetes 资源模板等文件组织成一个 chart,values 则提供用于生成具体资源配置的参数。先对照这个版本的默认值:

进程和 chart 默认都打开了选主,但 chart 只部署一个副本。这个实例仍会参与选举,只是退出以后没有现成的备用实例接手。向此提交的本地 chart 传入以下 values,才会部署两个副本:

1
2
replicas: 2
leaderElectionEnabled: true

ConfigMap 是 Kubernetes 用来保存配置数据的资源。若启用 configuration.enabled,chart 会把选主配置写进 ConfigMap;Operator 使用 --config 时忽略命令行配置,排查应以挂载文件为准。配置模板与 main.go 中的配置加载分支共同决定最终值。

副本最好分散到不同节点,否则节点故障可能同时带走主备。还要留意此 chart 的 Deployment 使用 Recreate 更新策略:更新时先终止旧版本 Pod,再创建新版本 Pod。因此,多副本配置本身不保证 Operator 升级期间持续有人协调。以下讨论存活备用实例接管的场景。

RayCluster、RayJob 和 RayService 这些 Controller 随所在 Manager 一起参与选主,不会各自选出一个主实例。因此,增加备用副本不会自动提高它们的协调吞吐。反过来,若把选主关掉,多个实例都会启动协调;这不是把工作平均分给它们。

2. 哪些实例竞争同一个 Lease

Lease 是 Kubernetes 保存租约状态的资源,包含持有者身份和续约信息。它可用于组件选主,具体抢占和续约逻辑由客户端执行。

KubeRay 在 Manager 配置中指定锁的名称和命名空间。以下是 main.go 的连续字段节选:

1
2
3
LeaderElection:          *config.EnableLeaderElection,
LeaderElectionID: "ray-operator-leader",
LeaderElectionNamespace: config.LeaderElectionNamespace,

controller-runtime 默认使用 coordination.k8s.io 的 Lease。ServiceAccount 是 Pod 内程序访问 Kubernetes API 时使用的身份。锁的命名空间未指定时,从 Pod 挂载的 ServiceAccount namespace 文件读取;集群外无法读取时需要显式配置。它与 watchNamespace 不同:前者决定锁放在哪里,后者决定观察哪些业务资源。

同一 Kubernetes 集群内,竞争相同 namespace/name 的实例才属于同一组选主。两个安装若锁在不同 namespace,却观察重叠的 RayCluster,可以各自选出 leader;锁相同也不会自动分片业务对象。更改锁命名空间时,还要提供相应 Lease 权限,chart 默认只在安装 namespace 创建选主 Role。

持有者身份也不是固定的 Deployment 名称。NewResourceLock 的连续节选如下:

1
2
3
4
5
id, err := os.Hostname()
if err != nil {
return nil, err
}
id = id + "_" + string(uuid.NewUUID())

hostname 与 UUID 组合起来,区分每次启动的进程实例。排查谁在持有锁,可以看 Lease 的 holderIdentity;只看 Pod 是否 Ready 还不够。

readyz 是供就绪探针访问的检查接口。KubeRay 为它注册的是 healthz.Ping,没有检查当前实例是否持有 Lease。所以两个 Pod 同时 Ready 完全可能是正常的主备状态,还要结合持有者身份、续约变化和协调日志判断。

主备之间通过 API Server 中的同一个 Lease 竞争领导权,关系如图 1。

图 1:Operator 实例竞争同一 Lease

图 1:Operator 实例竞争同一 Lease 查看原图

每个实例通过 API Server 读写同一个 Lease,leader 持续续约。候选者观察到租约不再有效后尝试更新,API 的版本冲突检查裁决竞争写入。client-go 根据本地观察时间判断租约是否有效,不能只拿本机时间减去 renewTime 就复现其全部判断。

此处沿用 controller-runtime 的 LeaseDuration=15sRenewDeadline=10sRetryPeriod=2s:分别控制候选者等待、leader 续约重试期限和重试间隔。KubeRay 没有覆盖这三个值。它们不是接管耗时承诺,实际还受 API 延迟、进程调度和 Controller 启动影响。

3. 新 leader 从哪里继续

备用实例拥有自己的 Manager 和缓存。controller-runtime 先启动并同步缓存,再进入选主流程;获得领导权后启动需要选主的运行组件。下面是 OnStartedLeading 的连续节选:

1
2
3
4
5
6
7
OnStartedLeading: func(_ context.Context) {
if err := cm.startLeaderElectionRunnables(); err != nil {
cm.errChan <- err
return
}
close(cm.elected)
},

新 leader 启动自己的 Controller,通过监听对象、执行协调接手工作。旧进程的 goroutine、调用栈和内存定时器都不会转移过来。如果切换恰好发生在请求已经发出、状态还没写回的时候,处理过程如图 2。

图 2:请求在途时,新 leader 依据资源重新协调

图 2:请求在途时,新 leader 依据资源重新协调 查看原图

假设旧 leader 已成功创建 demo-job 的 submitter Kubernetes Job,却来不及回写 RayJob 状态就退出。新 leader 再处理这个 RayJob 时,会读取 CR 和相关资源;createK8sJobIfNeed 按既定名称查询提交用 Job,查到后直接返回。若缓存还没看到创建结果,重复创建同名对象也可能遇到 AlreadyExists,随后仍需重试、重新观察。后续仍按同一个身份查询和判断,流程才有机会继续推进。

还有一个问题需要单独看:旧 leader 发出去的请求,切换以后会怎样?client-go 的包说明明确不提供 fencing,即从操作接收端强制拒绝旧 leader 的隔离保证。租约变化不会撤销已被 API Server 或 Ray Dashboard 接收的请求,也不会把外部副作用回滚。

失去领导权时,controller-runtime 的 OnStoppedLeading 将优雅停机等待设为零并返回 leader election lost;KubeRay 的 exitOnError 使进程退出。如果要从接收端拒绝过期主实例,通常需要随请求携带并校验代表本次领导权的递增令牌;这里没有这样的机制。进程暂停、网络异常和请求在途仍需分别分析。

发送方超时时,接收方可能已经执行了请求,再次重试就可能重复执行。Kubernetes 对同名资源的约束,也管不到用户程序向数据库写入结果的操作。业务若要求 exactly-once 效果,需要接收端幂等键或事务等配合;即使没有主备切换,只丢失一次响应,也会遇到这个问题。

接管后也不要求把每个内存细节复原。比如 RayService 把旧集群的待删除时间放在本地 map 中,新实例观察到待清理集群后可以重新安排延迟。其结果可能是清理晚一些,不能把旧进程的定时器视为持久化承诺,见 cleanUpRayClusterInstance

4. Kubernetes 控制面失联时,备用能做什么

主备都无法访问 API Server 时,备用无法更新 Lease,leader 也无法正常续约;CR、Pod 的查询和写入同样受阻。若 API Server 的存储依赖 etcd 不可用,继续增加 Operator 副本无法补上这条依赖。

已有 Ray 进程在节点和网络正常时可能继续执行,但补建 Pod、扩容、调度新增资源和回写状态会受到影响。kubelet 对本机容器的处理,与控制面创建、调度一个新 Pod 是不同路径。故障范围应先区分“某个 Operator 到 API 的链路断了”与“所有实例都访问不了控制面”。

图 3 把这些依赖放在一起。图中的 Redis 是供 GCS 使用的外部键值存储服务,其恢复条件在下一节展开。排查时沿 API 调用和存储访问关系往下看,可以判断某个组件失联后,哪些工作也会跟着停下来。

图 3:Operator、Kubernetes、Ray 与持久化存储的故障边界

图 3:Operator、Kubernetes、Ray 与持久化存储的故障边界 查看原图

5. worker、head 和 GCS 的恢复条件

worker Pod 丢失后,KubeRay 根据仍有效的期望配置补足 Pod,Kubernetes 调度并启动容器。该节点上的 task、actor 能否恢复,则取决于 Ray 的重试、重启策略及状态来源。普通 task 的 max_retries 约束重试;actor 的 max_restarts 默认是零,即使允许重启,也需要应用恢复其内存状态。例如,程序可以把进度写入检查点,再由重启后的程序读取;检查点中保存哪些内容、怎样恢复,需要应用自己实现。

head 还承载 GCS 等进程。默认内存 GCS 丢失后,重建一个 head Pod 无法还原原集群元数据,更不会恢复 driver 的 Python 调用栈。第一篇的普通 demo-job 不能因此获得断点续跑能力。Pod 补建也受配置限制,例如已标记为 provisioned(完成初始资源准备),且带 ray.io/disable-provisioned-head-restart 禁用标记的集群会跳过 head 重建,见 reconcilePods

要让 GCS 进程退出后仍能读回元数据,就需要把这部分数据放到进程之外。这里以 Redis 为后端:配置 gcsFaultToleranceOptions.backend: redisredisAddress 后,KubeRay 向 head 注入 RAY_REDIS_ADDRESS 等参数。Ray 的 GetStorageType 有如下连续分支:

1
2
3
4
5
6
7
8
if (RayConfig::instance().gcs_storage() == kInMemoryStorage) {
if (!config_.redis_address.empty()) {
RAY_LOG(INFO) << "Using external Redis for KV storage: "
<< BuildAddress(config_.redis_address, config_.redis_port);
return StorageType::REDIS_PERSIST;
}
return StorageType::IN_MEMORY;
}

因此不能仅看到 gcs_storage 默认是 memory,就认定显式 Redis 配置无效。GCS 重启时从后端加载元数据;恢复要求 Redis 数据仍在、可访问,并使用相同存储命名空间。这里的“存储命名空间”用来区分不同 Ray 集群的后端数据,与 Kubernetes namespace 是两种范围。KubeRay 的 configureRedisFT 默认用 RayCluster UID 隔离数据。同一个 CR 更换 head 保持 UID,删掉再建同名 CR 则不会保持;自定义 externalStorageNamespace 也不应让两个活跃集群误用同一份状态。

GCS 把元数据保存在这里,所有 Ray 对象、actor 内存和业务检查点并不会因此一起保存。Redis 自身故障后还能保留多少数据,要继续看它的可用性、落盘和复制策略。

数据保留还受集群删除流程影响。Redis 路径默认启用清理,Controller 会通过 finalizer 和清理 Job 删除对应存储命名空间的数据;外部 Redis 不等于永久归档。若要分析删后恢复,应先核对清理配置和数据是否仍在。

GCS 恢复期间,存活 worker 上已有计算和对象可能继续可用,但 actor 创建、节点注册等依赖 GCS 的操作暂停。KubeRay 在启用 GCS 容错且用户未覆盖时,把 worker 重连超时设为 600 秒;这只是等待预算,超时仍会退出。head 故障还会带走其上的其他进程。锁定版 GCS 文档把官方支持的 Redis 容错范围限定在 KubeRay 上的 Ray Serve,不能据此承诺任意 RayJob 自动恢复。

此源码还存在 backend: rocksdb。RocksDB 是进程内使用的键值存储库,这条路径要把数据文件放在可恢复的持久卷上,并限制为单个写入者。它还要求开启默认关闭的 alpha 实验特性 GCSFaultToleranceEmbeddedStorage,使用支持该后端的 Linux Ray 镜像。本文不展开这条路径,也不把它当作稳定发布保证。

6. RayService 切换,能保证每个请求成功吗

这里换成一个持续提供 Ray Serve 服务的独立例子,对应第一篇资源关系图的右侧。RayService 与 demo-job 是两种使用方式;RayJob 完成后不会进入 RayService 的状态机。

RayService 的 active 指当前集群,pending 指准备接替的集群。这里讨论传统的新集群蓝绿升级:保留当前集群的同时准备另一套集群,等它可用后再切换访问入口,不包括增量流量迁移。pending 的 Serve 应用非空且全部为 RUNNING 后,Controller 才把它选作切换目标,见 getAndCheckServeStatus

新集群准备好之后,访问入口和 RayService 状态还要分别更新。图 4 将这些步骤展开。

图 4:RayService 的传统 active/pending 切换

图 4:RayService 的传统 active/pending 切换 查看原图

reconcileServicesToReadyCluster 的连续节选说明,两次 Service 更新是顺序调用:

1
2
3
4
5
6
7
8
headSvc, err := r.reconcileServices(ctx, rayServiceInstance, rayClusterInstance, utils.HeadService)
if err != nil {
return headSvc, nil, err
}
serveSvc, err := r.reconcileServices(ctx, rayServiceInstance, rayClusterInstance, utils.ServingService)
if err != nil {
return headSvc, serveSvc, err
}

Service 的 selector 是选择后端 Pod 的标签条件。reconcileServices 更新入口 Service 的 selector,使其指向目标集群,随后状态计算确认入口指向 pending,将它提升为 active;旧集群按删除延迟清理。它们没有组成跨资源事务,第二次更新失败时可以留下部分切换状态,需后续协调。

旧集群存活、新集群有足够容量时,这种准备后切换能减少服务空窗。不过,Service 变更还需传播到实际转发路径,在途长请求、连接断开和客户端重试都有各自行为。若 active 已故障而 pending 尚未就绪,准备过程也不能补回已中断的请求。判断可用性应实际观测错误率、延迟和长连接行为,不能把这条路径统称为“零中断”。

7. 按故障落点作判断

下表是前述条件下的推演,不是故障注入结果;“可能继续”均以相关节点、网络和依赖仍正常为前提。

排查时可以从 Pod 状态开始,但还要继续查它背后的恢复依据。两个 Operator 都 Ready,要看 Lease、实际配置和监听范围;head 已重建,要看 GCS 后端、原 CR UID 和程序检查点;pending 已转 active,则要核对 Service 指向及实际请求表现。管理进程重新工作、计算继续执行和请求恢复正常,分别需要这些证据来判断。

前五篇到这里完成了 KubeRay 管理与恢复流程的分析。第六篇开始增加 Volcano,继续追踪资源不足时 Pod 如何获得运行机会;调度器驱逐 Pod 后,计算恢复仍需遵守本篇说明的应用与 Ray 故障边界。

参考资料