第 10 篇|源码阅读报告:从 RayJob 到用户程序
本文最后更新于:7 分钟后
前言
从第零篇走到第九篇,我们先拆开 KubeRay 的资源模型、协调、并发、恢复和高可用,再进入 Volcano 的 Gang、Queue 与接入流程。这一篇不再展开新的机制,而是把这些结论放回同一次 RayJob 执行:每个阶段由谁推进,状态保存在哪里,某一层完成以后还要等待什么。
这份报告沿用全系列的固定源码基线:KubeRay 6bf05eb17a3e、Ray 3f785c0711b9、Volcano d8984501e4ad。文中的容量、故障和时序仍是源码推演,没有加入集群压测或故障注入结果。
从 RayJob 到用户程序
在本文的 Kubernetes 场景中,使用者先提交 RayJob。KubeRay 根据 RayJob 创建专用 RayCluster,再由 RayCluster Controller 创建 head、worker Pod 和 Service。集群进入 Ready 后,KubeRay 创建 submitter Kubernetes Job;submitter 连接 Ray Dashboard 的 Jobs API,把入口命令交给 Ray。程序从这一步开始进入 Ray 自己的任务、Actor 和资源调度体系。
启用 Volcano 时,这条主干增加了两组调度信息:创建 RayCluster 之前先写入 PodGroup;创建 Pod 时再写入 schedulerName、PodGroup 名和成员角色。Volcano 接管的是 Pod 获得节点的过程。它不会替 KubeRay 判断 RayCluster 是否 Ready,也不会替 Ray 执行用户程序。
把流程按阶段展开,可以得到下面的分工:
| 阶段 | 主要组件 | 关键对象或状态 | 向前推进的条件 |
|---|---|---|---|
| 声明作业 | 使用者、Kubernetes API | RayJob spec | 对象写入 API |
| 准备调度需求 | KubeRay | PodGroup,可选 | 创建或更新请求成功 |
| 创建集群 | RayJob Controller | RayCluster | 专用集群存在 |
| 创建成员 | RayCluster Controller | Service、head 和 worker Pod | 期望资源逐步建立 |
| 分配节点 | 默认调度器或 Volcano | Pod、PodGroup、Node | Pod 满足调度条件并完成绑定 |
| 判断集群可用 | KubeRay、Ray | RayCluster status、Ray 服务状态 | Controller 观察到就绪条件 |
| 提交程序 | submitter、Ray Jobs API | Kubernetes Job、Ray 作业标识 | submitter 能连接 Dashboard 并完成提交 |
| 执行与收尾 | Ray、KubeRay、Kubernetes | Ray 作业状态、RayJob status、删除策略 | 程序结束,并按策略保留或删除集群 |
这张表也解释了系列中几个名字相近的对象:RayJob 管理一次 Kubernetes 上的提交生命周期;submitter Kubernetes Job 运行提交程序;Ray Jobs API 接收真正交给 Ray 的作业;Volcano 的 JobInfo 则是 Scheduler 对一组 Pod 的内存表示。
三个控制循环怎样协作
完整流程中有三套节奏不同的循环。
KubeRay Controller 观察 RayJob、RayCluster 和 Pod。事件进入工作队列后,Reconcile 重新读取当前对象,比较期望状态与实际状态,再创建资源或写回 status。它处理的是 Kubernetes 资源怎样逐步收敛。
Volcano Scheduler 周期性建立 Session,把缓存中的 Node、Queue、JobInfo 和 TaskInfo 组织成一次调度快照。enqueue、allocate、backfill,以及按需启用的 preempt、reclaim,在 Session 中计算候选结果并提交绑定。它处理的是一组 Pod 能否获得节点,以及多个作业怎样竞争资源。
Ray 的运行时循环发生在集群内部。head、worker 建立连接后,Ray 根据可用资源执行 task 和 actor;Ray Jobs API 管理用户程序的提交与状态。它处理的是计算任务怎样运行。
三套循环没有共享一份进程内状态。KubeRay 与 Volcano 通过 Kubernetes 对象交接信息:PodGroup 表达最低成员和资源需求,Pod 上的字段表达调度器选择与组关系,RayCluster status 表达 KubeRay 观察到的集群状态。submitter 再通过 Ray Jobs API 跨入 Ray 的运行时边界。
这种协作方式允许各组件独立重试。某个 watch 事件重复到达,工作队列可以合并同一个对象键;某个事件没有对应一次独立执行,后续 Reconcile 仍会读取对象的最新状态。Scheduler 也根据当前 Session 重新计算候选,不依赖逐条重放此前发生的资源变化。
状态、并发与恢复
源码中最容易混在一起的是事件、状态和正在执行的工作。它们承担不同职责:
- Kubernetes API 中的 spec、status、owner reference 和对象是否存在,是 Controller 重启后仍可读取的事实。
- informer cache、工作队列、expectations 和正在运行的协程属于进程内状态,用于提高效率、限制并发或吸收缓存观察延迟。
- Ray 的 GCS、对象存储和运行中进程保存计算侧状态;它们的恢复条件与 Operator 是否重新选主不同。
由此可以归纳出几条贯穿前文的结论。
watch 提醒状态变化,Reconcile 决定现在该做什么
Controller 不需要让每个事件都对应一次完整处理。同一个对象键在队列中可以合并;处理开始以后再次发生变化,键还可以重新进入队列。Reconcile 读取的是执行当时的对象状态,因此它关心当前是否还需要创建、更新或删除资源。
管理并发与计算并发位于不同层次
多个 worker 允许 KubeRay 同时协调不同 RayCluster;同一个对象键仍要避免并行修改。expectations 用于等待已发出的创建或删除被缓存观察到,减少重复操作。Ray 能同时执行多少任务,则由集群资源和 Ray 调度决定,不受 Controller worker 数直接控制。
恢复依赖可重建的状态链
Operator 重启后会重新 list/watch Kubernetes 对象,再从 spec、status、label、owner reference 和实际子资源恢复协调。进程内队列和锁可以丢失,因为它们能从对象状态重新建立。若推进下一步所需的信息只存在于已丢失的进程内状态,恢复链就会中断;这也是阅读恢复逻辑时需要持续核对的问题。
高可用要按层拆开
Leader Election 解决多个 Operator 副本中谁写控制面;Kubernetes 控制面是否可用、Ray head 和 GCS 是否恢复、RayService 是否能切换服务集群,是另外几层条件。某一层恢复,不代表正在执行的计算或外部请求已经恢复。
调度结论放回完整系统
Volcano 的几个调度概念也可以放回同一条路径理解:
- PodGroup 把多个 Pod 表达为一组。
minResources在 enqueue 阶段参与资源准入,minMember则参与 Gang 的提交判断。 - Gang 判断发生在试分配过程中。条件不满足时可以撤销本轮 Statement 中的尝试,避免只绑定一部分成员。
- Queue 组织多组作业的资源竞争。weight、capability、request、allocated 和 deserved 共同影响 Queue 还能否继续使用资源。
- DRF 等策略在相应层次决定检查顺序;节点 predicates 最终仍要确认某个具体 Node 能否接纳 Pod。
- preempt 和 reclaim 选出需要释放的成员后,资源还要等待 Pod 实际退出,才能重新用于绑定。
这些机制提供的是 Pod 调度条件。PodGroup 满足最低成员数,不等于所有可能扩出的 worker 都已运行;Queue 得到份额,也不等于某组节点被永久划给它;RayCluster Ready 更不等于用户程序已经成功完成。每一项状态只回答它所在层次的问题。
这份源码报告能够支持哪些判断
基于固定版本源码,可以确认下面这些行为关系:
| 可以从源码确认 | 仍需部署或实验确认 |
|---|---|
| Controller 的 watch、队列、Reconcile 和重试路径 | 特定集群规模下的协调延迟与吞吐 |
| RayJob、RayCluster、PodGroup 和成员 Pod 的创建顺序 | 安装版本、CRD、Webhook 与 Kubernetes 版本的实际兼容性 |
| Leader Election、状态写回和对象重建所依赖的信息 | 控制面故障时的真实恢复时间 |
| Gang、Queue 份额、抢占与回收的源码条件 | 具体工作负载下的排队时间和资源利用率 |
| KubeRay 与 Volcano 通过哪些字段协作 | Admission 注入、Pod overhead 等环境差异造成的最终资源请求 |
| submitter 何时创建,以及它怎样进入同一个 PodGroup | 用户代码、依赖包、存储和结果提交的可靠性 |
这也是全系列一直保留源码版本和条件分支的原因。源码能够说明控制路径与判断条件;时间、容量和稳定性数据仍需要在目标环境中测量。
怎样继续使用这套阅读方法
这次阅读没有从文件列表开始,而是始终沿一个问题寻找下一次状态交接:谁观察对象,谁把键放进队列,谁重新读取状态,谁创建下级资源,谁写回结果。遇到并发和失败分支时,再分别检查持久化状态、进程内状态和外部组件状态。
把这种方法用于其他 Controller 或调度器时,可以保持下面的顺序:
- 从用户提交的顶层对象开始,画出它创建或引用的下级对象。
- 找到 watch 注册、队列键和 Reconcile 入口,确认事件怎样变成一次状态检查。
- 沿每次 API 写入继续追踪下一位观察者,直到请求进入真正执行工作的组件。
- 将 status、缓存、锁、队列和外部存储分开,判断重启后哪些信息仍然存在。
- 对版本条件和失败分支单独记录,不用当前版本的实现替代其他版本行为。
对于部署选择,也可以沿组件边界判断。Ray 可以独立运行;希望用 Kubernetes 管理 Ray 集群生命周期时再引入 KubeRay;需要最低成员共同启动、Queue 份额或批作业资源竞争时,再为相应工作负载接入 Volcano。每增加一层能力,也会增加一组需要维护的对象、状态和版本关系。
小结:沿状态交接理解整个系统
从 RayJob 到用户程序,系统没有一条贯穿所有组件的同步调用链。KubeRay、Kubernetes 调度器或 Volcano、Ray 依次观察属于自己的状态,并把结果写到下一层能够读取的位置。watch、队列和重试让协调可以重复发生;Kubernetes 对象让控制过程能够在重启后重新建立;PodGroup 和 Queue 为多个 Pod、多个作业补充调度约束;Ray 在资源就绪后接管计算。
读源码时抓住对象、状态和控制循环,就能判断一项能力由哪一层提供,也能看出某个状态成立之后,系统还需要经过哪些阶段。到这里,这组源码阅读形成了一条完整路径:从架构分工出发,经过协调、并发、恢复和调度,最后回到一次用户程序如何真正运行。