第 10 篇|总结:从 KubeRay 与 Volcano 看 Operator 设计
本文最后更新于:2 天前
前言
前面十篇从一次 RayJob 提交出发,跟踪了 KubeRay 的资源管理、并发和恢复,再进入 Volcano 的调度与接入流程。沿着这些实现读下来,我们也逐渐看清了另一个问题:如果要为自己的系统编写 Operator,应该把哪些逻辑放进去,又能依靠 Kubernetes 完成哪些工作?
这篇回到这个问题。把资源关系定义清楚,用协调循环推进状态,再处理并发、中断和多副本接管,便能看出一个 Operator 是怎样逐步构成的。
一个 Operator 要写哪些逻辑
KubeRay 先用 RayCluster、RayJob 和 RayService 表达不同的管理需求。RayCluster 描述运行环境,RayJob 在集群之上增加程序提交与收尾,RayService 则管理持续服务。这样的划分让集群维护逻辑可以被两种上层入口复用。
对自己的 Operator,也可以先问:使用者声明什么,哪些对象由它创建,管理过程需要保存哪些进展?CRD 定义这些资源的结构,spec 接收期望配置,status 记录管理阶段和观测结果。Controller 再把声明变成具体动作。
以 RayJob 为例,它先保存集群名和提交标识,再创建 RayCluster;等集群就绪后创建 submitter Job,随后查询作业进度、处理结束状态。每一步都能从当前资源判断是否已经完成。这给出了一种组织协调逻辑的方法:
| 设计内容 | KubeRay 中的例子 |
|---|---|
| 划分资源职责 | RayJob 管理提交生命周期,RayCluster 管理集群 |
| 定义拥有关系 | RayJob 拥有专用 RayCluster 和 submitter Job,RayCluster 拥有成员 Pod |
| 确定观察入口 | 监听主要资源,也通过拥有关系关注下级资源变化 |
| 编写一轮协调 | 读取对象、检查已有进展、执行当前能做的操作、写回状态 |
| 安排后续处理 | 资源事件触发检查,等待外部进展时延时重查,错误按规则重试 |
| 定义结束与删除 | 记录完成时间,按策略清理,通过 finalizer 处理删除前工作 |
其中最需要业务设计的是“当前应该做什么”。创建请求成功后,Pod 还要经历调度和启动;提交程序开始运行后,用户作业还要在 Ray 中执行。Operator 要为这些交接选好观察条件。第一篇和第二篇分别说明了资源怎样划分,以及一轮协调怎样推进它们。
Kubernetes 和框架提供了什么
Operator 能以这种方式工作,背后有 Kubernetes 的资源接口、持续观察和各类控制器,也有 controller-runtime 对缓存、队列和协程的组织。
| 机制 | 为 Operator 提供的能力 |
|---|---|
| API Server 与 etcd | 接收并保存资源对象,使声明和状态可以被其他进程读取 |
| list/watch 与 informer | 同步已有对象,持续接收变化,维护本地缓存并通知事件处理器 |
| 工作队列 | 保存待检查的对象标识,合并请求,安排延时与重试 |
| Manager 与 Controller | 组织客户端、缓存、监听和协调协程,调用业务 Reconcile |
| resourceVersion | 在对象更新时检查写入是否仍基于有效版本 |
| owner reference 与垃圾回收 | 表达拥有关系,在删除时回收相应下级资源 |
| Scheduler、kubelet 与容器运行时 | 为 Pod 选择节点,并在节点上启动和管理容器 |
这些机制通过 API 中的对象接起来。RayJob Controller 创建 RayCluster,RayCluster Controller 观察到它后创建 Pod;调度器写入节点绑定,kubelet 启动容器并回报状态,Controller 再据此判断集群是否就绪。
这也解释了为什么事件可以合并:事件提醒 Controller 再看一次,具体动作由执行时读到的状态决定。两个 worker 接连更新,可以合成一次集群检查;已经开始处理后又有变化,则留下下一轮请求。第 0 篇中的部署流程和第二篇的事件路径,在这里连成了同一套机制。
并发控制要分清作用范围
第三篇把场景扩大到 100 个 RayCluster。多个协调协程可以同时处理不同对象;同一队列中的同一个对象,要等本轮结束后才能再次交出。提高并发数,增加的是同时协调不同对象的机会。
因此,写 Operator 时需要分别处理三个位置的并发:
- 队列安排:合并同一对象的待处理请求,避免同一队列把它同时交给两个协调协程。
- 资源观察:API 创建成功后,本地缓存可能尚未更新。KubeRay 用 expectations 记录已发出、尚待观察确认的操作,减少重复创建或删除。
- 资源写回:用户或其他 Controller 也可能修改对象。更新遇到版本冲突时,后续协调需要重新读取、计算,再尝试写回。
同键串行只约束本 Controller 的处理。不同 Controller、不同 Operator 进程和使用者仍可能访问同一个资源,进程内共享数据也需要自己的同步保护。设计时先列出这些访问者,再确定每个位置用什么机制协调写入。
运行后,再沿队列积压、单轮耗时、错误重试和 API 请求耗时观察瓶颈。许多对象都在等待且 API 响应稳定时,可以评估增加协调协程;如果等待集中在同一个对象,或 API 已经限流,就要回到对应环节处理。Ray 的计算并发则由 Ray 与集群资源决定。
恢复与高可用要从保存的状态设计
把进程中断放到任意两次操作之间,就能检查恢复逻辑是否接得上:资源已经创建,状态还没写回怎么办?请求已经生效,响应却丢失了怎么办?新的进程需要用什么信息辨认已有进展?
让下一轮能识别已经完成的动作
第四篇中的 RayJob 在创建资源前先保存身份。假设 submitter Job 已创建、管理状态尚未写回,Operator 就退出了,新进程仍能按原名查询到 Job,接着写回状态。保存下来的开始时间、结束时间和失败次数,也让重试与清理能够沿原来的进度继续。
由此设计自己的协调逻辑时,每次写入都要考虑重复执行,以及只完成一部分后怎样继续。Kubernetes 同名资源约束帮助识别已存在的对象,Ray 的 submission ID 帮助查询已有提交。程序向外部数据库写入结果时,则需要业务自己的唯一键、事务或去重记录。
进程内缓存、队列和 expectations 会在重启后重新建立。需要跨进程延续的身份、阶段和时间,应放在后续能够重新读取的位置;删除和清理也要能够根据对象的当前状态反复推进。
让备用实例接手管理工作
第五篇进一步分析了多副本接管。多个 Operator 通过同一个 Lease 选出 leader,备用实例取得领导权后,读取已有资源继续协调。副本数决定有没有接手者,选主决定谁负责写入,它们是两项配置。
管理接管还依赖 Kubernetes 控制面可用。计算侧则有自己的恢复过程:KubeRay 补建 Pod,Ray 按重试策略处理 task 和 actor,GCS 从配置的后端恢复元数据,应用从检查点恢复业务进度。持续服务还涉及 RayService 的接替集群和入口切换。
因此,设计高可用时,要先确定准备恢复的是管理能力、计算进度还是服务入口,再为相应状态安排保存位置和接手方式。这样才能把备用副本、存储后端和应用恢复逻辑放到各自的位置。
调度让 Operator 的职责边界更清楚
第六篇到第八篇把问题转向了资源竞争。Operator 可以创建期望的 Pod,但这些 Pod 何时获得节点,还要由调度器根据集群资源和策略决定。
Volcano 用 PodGroup 表达组要求,用 Queue 组织多组作业的资源竞争。minResources 参与 enqueue 资源准入,minMember 参与 Gang 提交判断;Queue 的份额允许继续分配以后,仍要为具体 Pod 找到节点。试分配达到组要求后提交,随后逐个完成绑定和容器启动,这正是第七篇展开的执行顺序。
接回 KubeRay 时,Operator 先写入 PodGroup,并在成员 Pod 上填写调度器与组信息,Volcano 据此完成调度。KubeRay 继续观察集群是否就绪,再安排提交程序。第九篇说明了这种协作:组件把需求和进展写在约定的资源字段里,再由各自的控制过程接续。
这对 Operator 设计同样有用。业务管理逻辑需要表达资源需求和推进条件,调度与容器运行可以交给相应组件。Ray 可以独立运行;选择 Kubernetes 部署场景后引入 KubeRay,有组调度和队列共享需求时再接入 Volcano。
小结:从资源声明走到可持续运行的控制过程
回看这个系列,编写 Operator 的过程可以归纳为:先定义资源和拥有关系,再围绕当前状态编写一轮协调;随后检查重复执行、并发写入和中途退出时怎样接续;最后为管理进程与被管理系统分别安排恢复方式。
Kubernetes 和 controller-runtime 提供了持久化对象、事件通知、队列与执行框架。Operator 把业务需要补在这些机制之上,决定观察什么、执行什么、等待什么。KubeRay 与 Volcano 的实现,让我们看到这些选择如何共同推进一次作业,也为设计自己的控制过程提供了具体参照。