KubeRay 源码阅读:从 Operator 主备切换到 Ray 服务恢复
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 则提供用于生成具体资源配置的参数。先对照这个版本的默认值:
| 层次 | 此版本的默认行为 | 证据 |
|---|---|---|
| Operator 配置 | DefaultEnableLeaderElection = true,命令行参数使用这个默认值 |
defaults.go、main.go |
| Helm values | replicas: 1,leaderElectionEnabled: true |
values.yaml |
| Helm Deployment | 副本数取自 values;默认参数路径传入 --enable-leader-election |
deployment.yaml |
进程和 chart 默认都打开了选主,但 chart 只部署一个副本。这个实例仍会参与选举,只是退出以后没有现成的备用实例接手。向此提交的本地 chart 传入以下 values,才会部署两个副本:
1 | |
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 | |
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 | |
hostname 与 UUID 组合起来,区分每次启动的进程实例。排查谁在持有锁,可以看 Lease 的 holderIdentity;只看 Pod 是否 Ready 还不够。
readyz 是供就绪探针访问的检查接口。KubeRay 为它注册的是 healthz.Ping,没有检查当前实例是否持有 Lease。所以两个 Pod 同时 Ready 完全可能是正常的主备状态,还要结合持有者身份、续约变化和协调日志判断。
主备之间通过 API Server 中的同一个 Lease 竞争领导权,关系如图 1。
每个实例通过 API Server 读写同一个 Lease,leader 持续续约。候选者观察到租约不再有效后尝试更新,API 的版本冲突检查裁决竞争写入。client-go 根据本地观察时间判断租约是否有效,不能只拿本机时间减去 renewTime 就复现其全部判断。
此处沿用 controller-runtime 的 LeaseDuration=15s、RenewDeadline=10s、RetryPeriod=2s:分别控制候选者等待、leader 续约重试期限和重试间隔。KubeRay 没有覆盖这三个值。它们不是接管耗时承诺,实际还受 API 延迟、进程调度和 Controller 启动影响。
3. 新 leader 从哪里继续
备用实例拥有自己的 Manager 和缓存。controller-runtime 先启动并同步缓存,再进入选主流程;获得领导权后启动需要选主的运行组件。下面是 OnStartedLeading 的连续节选:
1 | |
新 leader 启动自己的 Controller,通过监听对象、执行协调接手工作。旧进程的 goroutine、调用栈和内存定时器都不会转移过来。如果切换恰好发生在请求已经发出、状态还没写回的时候,处理过程如图 2。
假设旧 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 调用和存储访问关系往下看,可以判断某个组件失联后,哪些工作也会跟着停下来。
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: redis 和 redisAddress 后,KubeRay 向 head 注入 RAY_REDIS_ADDRESS 等参数。Ray 的 GetStorageType 有如下连续分支:
1 | |
因此不能仅看到 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 将这些步骤展开。
reconcileServicesToReadyCluster 的连续节选说明,两次 Service 更新是顺序调用:
1 | |
Service 的 selector 是选择后端 Pod 的标签条件。reconcileServices 更新入口 Service 的 selector,使其指向目标集群,随后状态计算确认入口指向 pending,将它提升为 active;旧集群按删除延迟清理。它们没有组成跨资源事务,第二次更新失败时可以留下部分切换状态,需后续协调。
旧集群存活、新集群有足够容量时,这种准备后切换能减少服务空窗。不过,Service 变更还需传播到实际转发路径,在途长请求、连接断开和客户端重试都有各自行为。若 active 已故障而 pending 尚未就绪,准备过程也不能补回已中断的请求。判断可用性应实际观测错误率、延迟和长连接行为,不能把这条路径统称为“零中断”。
7. 按故障落点作判断
下表是前述条件下的推演,不是故障注入结果;“可能继续”均以相关节点、网络和依赖仍正常为前提。
| 故障场景 | 谁来推进恢复 | 可能继续的能力 | 不能据此保证 |
|---|---|---|---|
| 备用 Operator 退出 | Kubernetes 补副本 | leader 协调、Ray 计算 | 再次故障时仍有备用 |
| leader 退出,备用可访问 API | Lease 选主、新 leader 协调 | 已有 Ray 计算 | 无接管间隔、在途操作只执行一次 |
| 主备均无法访问控制面 | 控制面或网络恢复后重建协调 | 部分已有计算与请求 | 新增 Pod、扩容和状态及时更新 |
| worker 丢失 | KubeRay 补 Pod,Ray 按策略恢复计算 | 其他健康 worker 的计算 | actor 内存、丢失对象全部恢复 |
| head 丢失,无 GCS 容错 | 按配置重建运行环境 | 不承诺原集群持续运行 | 原程序接着原位置执行 |
| head 丢失,Redis 容错条件满足 | head 重建、GCS 加载、worker 重连 | 存活 worker 上已有计算或 Serve 副本 | head 上的进程和每个请求都恢复 |
| Redis 数据丢失或不可访问 | 存储系统恢复 | 取决于剩余状态与等待期限 | GCS 一定恢复到故障前状态 |
| RayService 切换中失败 | Controller 重试并重新观察资源 | 仍健康且可达的服务路径 | 跨资源原子切换、请求零失败 |
排查时可以从 Pod 状态开始,但还要继续查它背后的恢复依据。两个 Operator 都 Ready,要看 Lease、实际配置和监听范围;head 已重建,要看 GCS 后端、原 CR UID 和程序检查点;pending 已转 active,则要核对 Service 指向及实际请求表现。管理进程重新工作、计算继续执行和请求恢复正常,分别需要这些证据来判断。
前五篇到这里完成了 KubeRay 管理与恢复流程的分析。第六篇开始增加 Volcano,继续追踪资源不足时 Pod 如何获得运行机会;调度器驱逐 Pod 后,计算恢复仍需遵守本篇说明的应用与 Ray 故障边界。
参考资料
- KubeRay 源码基线(6bf05eb17a3e)
- Ray 源码基线(3f785c0711b9)
- KubeRay:ray-operator/apis/config/v1alpha1/defaults.go
- KubeRay:ray-operator/main.go
- KubeRay:helm-chart/kuberay-operator/values.yaml
- KubeRay:helm-chart/kuberay-operator/templates/deployment.yaml
- KubeRay:helm-chart/kuberay-operator/templates/configmap.yaml
- controller-runtime:pkg/manager/internal.go
- Kubernetes Lease 文档
- controller-runtime:pkg/leaderelection/leader_election.go
- client-go:tools/leaderelection/leaderelection.go
- KubeRay:ray-operator/controllers/ray/rayjob_controller.go
- KubeRay:ray-operator/controllers/ray/rayservice_controller.go
- Kubernetes 组件职责
- Ray:doc/source/ray-core/fault_tolerance/tasks.rst
- Ray:doc/source/ray-core/fault_tolerance/actors.rst
- KubeRay:ray-operator/controllers/ray/raycluster_controller.go
- Ray:src/ray/gcs/gcs_server.cc
- KubeRay:ray-operator/controllers/ray/common/pod.go
- Redis 持久化文档
- Ray:doc/source/ray-core/fault_tolerance/gcs.rst
- KubeRay:ray-operator/pkg/features/features.go
- KubeRay:ray-operator/controllers/ray/utils/validation.go
- Helm chart、模板与 values