第 5 篇|KubeRay 高可用:从主备切换到 Ray 服务恢复
本文最后更新于:2 天前
前言
demo-job 正在运行,负责它的 Operator Pod 突然退出。已有的 Ray 计算也许仍在继续,但谁来补 worker、回写作业状态和清理集群?如果退出的是 head Pod,恢复时又要从哪里找回 Ray 的状态?
第四篇讨论一次协调中断后,新的协调怎样依靠 Kubernetes 资源继续推进。这一篇把故障范围扩展到管理进程、Kubernetes 控制面、Ray 集群和服务入口。它们保存的状态不在同一个地方,判断能否恢复时,必须先确认中断发生在哪一层,以及接手者还能读到哪些状态。
先把四层恢复关系放在一起
一次完整的服务恢复可能经过四层,但四层解决的问题不同。
| 层次 | 中断后要恢复什么 | 重新开始时读取什么 |
|---|---|---|
| Operator 管理层 | 继续协调 RayCluster、RayJob 和 RayService | API Server 中的资源及其 status、关联对象 |
| Kubernetes 控制面 | 继续创建、调度和维护 Pod | etcd 中保存的 Kubernetes API 对象 |
| Ray 集群层 | 恢复节点成员、task、actor 和 GCS 元数据 | 存活进程、Ray 重试策略、GCS 后端、应用检查点 |
| RayService 服务层 | 让稳定入口指向可提供服务的 Ray 集群 | 当前集群与接替集群的状态、Serve 应用状态、Service 的后端选择条件 |
Operator 多副本负责管理层接管,接管还依赖 API Server 可用。Ray 的计算进度由运行进程、重试策略和检查点接续。一次故障可能只影响其中一层,也可能需要几层依次恢复,下面分别展开。
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 |
默认配置会部署一个参与选主的 Operator 实例。要在它退出后由另一个实例接手,需要同时增加副本数。向本地 chart 传入以下 values,可以部署两个参与同一组选主的副本:
1 | |
ConfigMap 是 Kubernetes 用来保存配置数据的资源。若启用 configuration.enabled,chart 会把选主配置写进 ConfigMap;Operator 使用 --config 时忽略命令行配置,最终以挂载文件里的配置为准。配置模板与 main.go 中的配置加载分支共同决定最终值。
把副本分散到不同节点,可以让单个节点退出时仍有备用实例存活。部署升级还涉及 Deployment 的更新策略:此 chart 使用 Recreate,先终止旧版本 Pod,再创建新版本 Pod,所以升级期间会等待新实例启动。下面的接管流程以备用实例已经运行且能够访问 API Server 为前提。
Manager 是同一 Operator 进程中组织客户端、缓存和 Controller 的运行组件。RayCluster、RayJob 和 RayService 的 Controller 随所在 Manager 一起参与选主,由 leader 统一启动协调。备用副本提供接管能力,协调并发仍由第三篇的配置控制。若关闭选主,每个实例都会独立协调自己观察到的对象,观察范围重叠时也会处理相同对象。
Lease 怎样确定当前 leader
Lease 是 Kubernetes 保存租约状态的资源,包含持有者身份和续约信息。KubeRay 在 Manager 配置中指定锁的名称和命名空间。以下是 main.go 的连续字段节选:
1 | |
controller-runtime 默认使用 coordination.k8s.io 的 Lease。ServiceAccount 是 Pod 内程序访问 Kubernetes API 时使用的身份。锁的命名空间未指定时,从 Pod 挂载的 ServiceAccount namespace 文件读取;集群外无法读取时需要显式配置。它与 watchNamespace 不同:前者决定锁放在哪里,后者决定观察哪些业务资源。
同一 Kubernetes 集群内,竞争相同 namespace/name 的实例属于同一组选主。这里需要同时看锁的范围和业务资源的观察范围:前者确定哪些实例互为主备,后者确定 leader 接手哪些对象。两个安装若使用不同 namespace 中的锁,即使观察相同的 RayCluster,也会各自选出 leader;共用一把锁的实例则由一个 leader 协调,并不按对象分片。更改锁命名空间时,还需要相应 Lease 权限;chart 默认在安装 namespace 创建的选主 Role,就是这组权限规则。
Lease 还要区分前后启动的具体进程。创建资源锁的 NewResourceLock 用以下代码生成持有者身份:
1 | |
hostname 与 UUID 组合起来,区分每次启动的进程实例。Lease 的 holderIdentity 记录当前持有者身份,Pod 是否 Ready 则描述就绪探针的结果,二者表达的状态不同。
readyz 是供就绪探针访问的检查接口。KubeRay 为它注册的是 healthz.Ping,没有检查当前实例是否持有 Lease。因此,两个 Pod 同时 Ready 可以是正常的主备状态;当前由谁负责协调,以 Lease 的持有者和续约情况为依据。
主备之间通过 API Server 中的同一个 Lease 竞争领导权,关系如图 1。
每个实例通过 API Server 读写同一个 Lease,leader 持续续约。候选者观察到租约不再有效后尝试更新,API 的版本冲突检查裁决竞争写入。client-go 使用本地观察到租约变化的时间判断有效期;Lease 中的 renewTime 记录续约时间,还需要结合这段观察过程理解接管时机。
此处沿用 controller-runtime 的 LeaseDuration=15s、RenewDeadline=10s、RetryPeriod=2s:分别控制候选者等待、leader 续约重试期限和重试间隔。KubeRay 没有覆盖这三个值。完整接管还包括 API 请求、进程调度和 Controller 启动,因此这些参数描述的是选主过程中的等待规则,不能直接当作端到端接管时间。
从旧 leader 退出到新 leader 开始协调
把一次接管顺着时间展开,可以看到五个阶段:
- 两个 Operator Pod 都已启动,各自建立 Manager 和本地缓存;只有 Lease 持有者启动需要选主的 Controller。
- leader 周期性续约。备用实例保持运行并观察 Lease,但此时不处理 RayCluster、RayJob 和 RayService。
- leader 退出或无法在期限内续约。它原有的 goroutine、调用栈和内存定时器随进程结束,不会传给备用实例。
- Lease 失效后,仍能访问 API Server 的候选者竞争更新 Lease,其中一个成为新 leader。
- 新 leader 启动需要选主的 Controller,从自己的缓存读取现有资源,按照第二至第四篇介绍的协调逻辑继续推进。
controller-runtime 先启动并同步缓存,再进入选主流程;获得领导权后启动需要选主的运行组件。下面是 OnStartedLeading 的连续节选:
1 | |
图 2 把第 3 至第 5 阶段放到一个具体请求上。
假设旧 leader 已成功创建 demo-job 的 submitter Kubernetes Job,也就是负责向 Ray 提交程序的 Job,随后在回写 RayJob 状态前退出。新 leader 读取 RayJob 和它关联的资源;createK8sJobIfNeed 按既定名称查询提交用 Job,查到后直接返回。若缓存尚未看到刚创建的对象,再次创建同名对象会收到 AlreadyExists,后续协调重新读取并判断。第四篇保存下来的资源身份,让前后两个进程围绕同一套资源继续推进。
失去领导权时,controller-runtime 的 OnStoppedLeading 将优雅停机等待设为零并返回 leader election lost;KubeRay 的 exitOnError 随后退出进程。这个动作停止旧 leader 继续发起协调。已经被 API Server 或 Ray Dashboard 接收的请求仍由接收端处理,所以切换点附近可能出现“操作已生效,发送方还没来得及记录结果”的状态。
新 leader 会重新查询这些在途请求的结果。Kubernetes 资源创建依靠固定名称识别已有对象;向外部系统写入业务数据时,则需要该系统自己的幂等键或事务。client-go 把接收端拒绝旧 leader 后续操作的能力称为 fencing;KubeRay 的请求没有携带代表本次领导权的递增令牌,因此恢复依据仍是接收端留下的实际状态。
进程内的安排也要重新建立。例如 RayService 把旧集群的待删除时间放在本地 map 中,即进程内的映射表。新实例观察到待清理集群后会重新安排延迟,清理时刻可能比旧实例原先安排的晚一些。这段逻辑位于 cleanUpRayClusterInstance。它说明了主备切换的边界:Kubernetes 资源中的状态可以重新读取,本地计时和调用过程则从新进程重新开始。
Kubernetes 控制面中断时哪些能力仍在
Operator 接管依赖 API Server,因为 Lease 续约、资源监听和资源写入都经过它。如果主备都无法访问 API Server,候选者无法可靠更新 Lease,Controller 也无法查询和维护 RayJob、RayCluster 与 Pod。此时增加 Operator 副本并不能绕过共同的控制面依赖。
etcd 保存 Kubernetes API 对象。如果中断来自 API Server、etcd 或两者之间的链路,需要先恢复这条控制面路径,Operator 才能从已有对象继续协调。已有 Ray 进程在节点和网络正常时可能继续执行;补建 Pod、扩容、调度新增资源和回写状态要等控制面恢复。节点上的 kubelet 负责本机容器管理,控制面负责创建和调度新的 Pod,两条路径承担不同工作。
图 3 把管理层、控制面、Ray 进程和外部存储的依赖放在一起。GCS(Global Control Service)保存和管理 Ray 集群元数据,例如节点和 actor 信息。图中的 Redis 是 GCS 可使用的外部键值存储,下一节继续解释它在 head 恢复中的作用。
Ray 集群怎样分层恢复
控制面恢复后,KubeRay 可以重新维护 Pod 数量,但“Pod 被补回来”和“原来的计算继续”仍是两个问题。前者由 Kubernetes 资源协调完成;后者取决于丢失的是 worker 还是 head,以及 Ray 和应用保存了哪些状态。
worker 丢失:补回节点,再按计算策略恢复
worker Pod 丢失后,KubeRay 根据 RayCluster 的期望配置补足 Pod,Kubernetes 调度并启动新的容器。这一步恢复的是集群成员数量。原 worker 上的 task、actor 和对象如何处理,由 Ray 与应用层决定。
普通 task 的 max_retries 约束重试;actor 的 max_restarts 默认是零。即使允许 actor 重启,它原有的进程内状态也需要恢复来源。例如,程序可以把进度写入检查点,再由重启后的程序读取;检查点保存哪些内容、在什么时机提交,属于应用自己的恢复设计。
新的 worker Pod 回到集群,只恢复了可用的计算环境。接下来由 Ray 决定哪些计算重新执行,应用再从检查点读回需要延续的业务进度。
head 丢失:先恢复 GCS,再让 worker 重连
head 承载 GCS 等进程。新的 head Pod 可以由 KubeRay 重建,但 GCS 重启后能读回多少集群信息,取决于它使用的存储后端。默认内存后端把数据留在原进程中;head 丢失后,新进程只能建立新的运行环境,原集群元数据需要其他来源。driver 的 Python 调用栈和应用进度也不属于 GCS 元数据,仍要由各自的运行进程或检查点保存。
是否补建 head 还受配置控制:已标记为 provisioned(完成初始资源准备),且带 ray.io/disable-provisioned-head-restart 禁用标记的集群会跳过 head 重建,这个判断位于维护 Pod 的 reconcilePods 中。
启用 GCS 容错后,恢复链变为:KubeRay 重建 head Pod,新的 GCS 连接外部后端并加载元数据,存活 worker 在重连期限内重新连接 GCS,随后依赖 GCS 的节点注册、actor 管理等操作继续进行。KubeRay 在用户未覆盖时把 worker 重连超时设为 600 秒;超过期限仍未恢复连接的 worker 会退出。
GCS 元数据从哪里读回
这里以 Redis 为后端。配置 gcsFaultToleranceOptions.backend: redis 和 redisAddress 后,KubeRay 向 head 注入 RAY_REDIS_ADDRESS 等参数。Ray 的 GetStorageType 有如下连续分支:
1 | |
当 gcs_storage 使用默认的 memory 时,这段代码会先检查 Redis 地址;有地址就选择 Redis 持久化后端,否则使用内存。GCS 重启后从选定后端加载元数据,所以恢复需要 Redis 数据仍在、能够访问,并继续使用同一存储命名空间。
这里的“存储命名空间”用来隔离不同 Ray 集群的后端数据,与 Kubernetes namespace 不是同一个概念。KubeRay 的 configureRedisFT 默认使用 RayCluster UID。UID 是 Kubernetes 为每个资源对象分配的唯一标识:同一个 RayCluster 对象更换 head 时保留 UID,删除后再建同名对象会得到新的 UID。自定义 externalStorageNamespace 时,也要让同时运行的集群使用不同的数据范围。
Redis 保存的是 GCS 元数据。Ray 对象、actor 内存、driver 调用栈和业务检查点有各自的生命周期。Redis 自身的数据能否在故障后保留,又取决于 Redis 的副本与落盘配置。把这些状态分开,才能判断一次 head 恢复最终能把服务带回到哪一步。
存储数据还跟随 RayCluster 生命周期清理。Redis 路径默认通过 finalizer 和清理 Job 删除对应存储命名空间的数据。finalizer 是资源删除前必须完成清理的标记;清理 Job 完成后,Controller 才结束这部分生命周期。外部 Redis 为同一个 RayCluster 的 head 重启提供恢复来源,删除 RayCluster 则按清理策略释放这组数据。
这份源码还提供 backend: rocksdb 路径。RocksDB 是嵌入 GCS 进程的键值存储,需要把数据文件放在可重新挂载的持久卷上,并保持单个写入者。该路径属于 alpha 实验功能,要求开启默认关闭的 GCSFaultToleranceEmbeddedStorage,并使用支持该后端的 Linux Ray 镜像。下文继续采用 Redis 场景。
GCS 文档将官方支持的 Redis 容错范围限定在 KubeRay 上的 Ray Serve。用于 RayJob 时,需要结合 driver 是否仍在、task 和 actor 的重试策略以及应用检查点,逐层判断能够恢复的进度。第一篇的普通 demo-job 不会仅因启用了 GCS 持久化就自动获得断点续跑能力。
RayService 怎样把新集群接到原有入口
这里换成一个持续提供 Ray Serve 服务的独立例子,对应第一篇资源关系图的右侧。RayService 与 demo-job 是两种使用方式;RayJob 完成后不会进入 RayService 的状态机。
Ray Serve 把应用部署为持续接受请求的服务。RayService 除了管理 Ray 集群,还维护两个角色:active 是当前承载流量的集群,pending 是正在准备接替它的集群。传统蓝绿升级保留 active,同时创建完整的 pending,等新集群可以服务后再切换稳定入口。
一次切换沿以下顺序推进:
- active 继续承接请求,Controller 创建或更新 pending RayCluster。
- Controller 查询 pending 上的 Serve 应用;应用列表非空且全部为
RUNNING,才把 pending 视为可切换目标。对应判断位于getAndCheckServeStatus。 - Controller 依次更新 head Service 和 Serve Service 的 selector,使两个稳定入口指向 pending。
- 状态计算确认入口已经指向 pending,将 pending 提升为新的 active,并清空 pending 状态。
- 旧集群进入延迟清理,给原有连接和收尾过程留出时间。
图 4 把这五步放到同一条时间线上。
reconcileServicesToReadyCluster 的连续节选说明,两次 Service 更新是顺序调用:
1 | |
Kubernetes Service 为一组 Pod 提供稳定入口,selector 是选择后端 Pod 的标签条件。两次调用分别维护 head 入口和 Serve 请求入口。后续状态计算会核对两个 Service 指向的集群,再决定是否完成角色提升。
如果切换在中间停止,已经写入 API Server 的结果会保留。例如 head Service 已更新而 Serve Service 尚未更新,新一轮协调会重新读取两个 Service 并继续执行更新;如果角色提升已经写回 RayService status,新的状态会成为下一轮清理旧集群的依据;若写回前退出,Controller 会根据两个 Service 的实际指向重新计算。这里继续沿用第四篇的恢复方式:每个阶段都把进展写入资源,下一次协调通过实际对象判断从哪里接着走。
active 健康且 pending 有足够容量时,先准备再切换可以缩短服务入口没有可用后端的时间。Service selector 的变化还要传播到实际转发路径,在途长请求、已有连接和客户端重试会影响用户看到的切换过程。若 active 已经故障而 pending 尚未就绪,服务仍要等待新集群准备完成;受影响的请求由客户端或应用的重试策略处理。RayService 管理的是集群和入口的切换,单个请求是否成功仍取决于这一整条请求路径。
小结:恢复依赖保存下来的状态
把全文收回到最初的问题:中断后能不能继续,取决于下一任执行者能否读到足够的外部状态。
| 中断位置 | 接手者 | 可重新读取的依据 | 恢复范围 |
|---|---|---|---|
| Operator leader | 获得 Lease 的备用实例 | Kubernetes 资源、status 与关联对象 | 继续资源协调;进程内调用和计时重新建立 |
| Kubernetes 控制面 | 恢复后的 API Server、etcd、调度器等组件 | etcd 中的 API 对象 | 继续创建、调度 Pod,并为 Operator 提供读写入口 |
| worker Pod | KubeRay、Ray 和应用 | RayCluster 期望、Ray 重试策略、应用检查点 | 补回 worker;计算按各自策略恢复 |
| head Pod 与 GCS | KubeRay、GCS、存活 worker | RayCluster 配置、Redis 或持久卷中的 GCS 元数据 | 重建 head、加载元数据并让 worker 重连 |
| RayService active 集群 | pending 集群与 Controller | Serve 应用状态、Service selector、RayService status | 将稳定入口切到准备完成的新集群 |
例如,备用 Operator 已取得 Lease,只能说明资源协调有人接手;若 head 同时丢失,还要继续核对 GCS 后端和计算进度的恢复来源。判断一次故障是否已经恢复,应沿受影响的那条路径检查,直到计算或服务请求能够继续。
第一至第五篇到这里完成了 KubeRay 管理、协调与恢复流程的分析。第六篇开始转向 Volcano,先独立追踪资源不足时 Pod 如何获得运行机会;第九篇再把调度流程接回 RayJob。调度器驱逐 Pod 后,计算恢复仍遵守本篇说明的 Ray 策略与应用状态边界。
参考资料
- KubeRay 源码
- Ray 源码
- 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