第 9 篇|KubeRay 与 Volcano:接入与生命周期协作

本文最后更新于:2 天前

前言

第一至第五篇沿 demo-job 讲清了 KubeRay 的创建、协调和恢复;第六至第八篇转到 Volcano,说明 Pod 怎样成组调度并竞争资源。现在把两条路径接起来:同一个 RayJob 在默认调度和 Volcano 调度下分别经过哪些步骤,接入点又落在哪些 Kubernetes 对象上?

主案例仍是 default/demo-job:K8sJobMode、一个 head Pod、compute 组两个 worker Pod,使用专用 RayCluster。启用 Volcano 时使用 Queue ray-batch。

先比较不开启和开启后的流程

不开启批调度

BatchScheduler 是 KubeRay Operator 内部的批调度适配接口,用来选择并调用具体的批调度器适配实现。没有配置它时,RayJob Controller 按原有流程工作:

  1. 为本次执行生成 RayCluster 名和 Ray 作业提交标识。
  2. RayJob Controller 把专用 RayCluster 写入 Kubernetes API。
  3. RayCluster Controller 观察到这个对象,进入自己的 Reconcile,创建相关 Service 和 head、worker Pod。
  4. Pod 使用 Kubernetes 默认调度器,各自寻找节点并启动。
  5. KubeRay 等待 RayCluster 进入 Ready。
  6. Ready 后创建 submitter Kubernetes Job,由它通过 Ray Jobs API 提交用户程序。

这些 Pod 可以先后获得资源。KubeRay 负责观察集群是否最终就绪,没有额外对象要求 head 和 worker 作为一组通过调度。

开启 Volcano

Operator 配置 --batch-scheduler=volcano 后,RayJob 的前半段多了 PodGroup 和调度元数据:

  1. RayJob Controller 先根据 RayCluster 模板计算最低成员数和最低资源量。
  2. Volcano 适配器创建或更新 PodGroup。
  3. PodGroup 写入 API 成功后,RayJob Controller 在同一轮协调中继续把 RayCluster 写入 Kubernetes API。
  4. RayCluster Controller 观察到 RayCluster,进入自己的 Reconcile,创建 head、worker Pod,并为它们填写 schedulerName: volcano、PodGroup 名和成员角色。
  5. Volcano Scheduler 观察 PodGroup 和成员 Pod,按 Gang 规则尝试分配和绑定。
  6. KubeRay 仍然等待 RayCluster Ready。
  7. Ready 后,KubeRay 创建 submitter Kubernetes Job;它的 Pod 也关联同一个 PodGroup。
  8. submitter Pod 运行后,再通过 Ray Jobs API 提交用户程序。

KubeRay 不会等待 PodGroup 进入某个 Ready 状态后才创建 RayCluster。它等待的是 PodGroup 的创建或更新请求成功写入 Kubernetes API;如果这一步报错,本轮协调返回,后续重试。写入成功后就继续创建 RayCluster。真正等待组内资源满足要求的是随后工作的 Volcano Scheduler。

RayJob 在未启用批调度和启用 Volcano 时的两条创建流程。启用后先写入 PodGroup,再创建 RayCluster;KubeRay 不等待 PodGroup 进入 Ready,RayCluster Ready 后才创建 submitter。
RayJob 在未启用批调度和启用 Volcano 时的两条创建流程。启用后先写入 PodGroup,再创建 RayCluster;KubeRay 不等待 PodGroup 进入 Ready,RayCluster Ready 后才创建 submitter。 查看原图

两条流程的主干没有改变:都是先准备 RayCluster,等集群 Ready,再创建 submitter。Volcano 接入增加的是创建 Pod 之前的组声明,以及 Pod 上选择调度器和关联 PodGroup 的元数据。

RayCluster 到 Pod 中间发生了什么

这里有两条彼此独立的 Controller 协调路径。RayJob Controller 的 getOrCreateRayClusterInstance 先构造 RayCluster,再调用 Kubernetes client 的 Create。这次调用只把 RayCluster 对象写入 API,不会顺带创建 Pod。

RayCluster Controller 在 SetupWithManager 中监听 RayCluster。它观察到新对象后,把 namespace/name 放入自己的工作队列;随后 Reconcile 读取 RayCluster,并进入 rayClusterReconcile。资源维护函数按固定顺序执行,其中 head Service 位于 reconcilePods 之前:

1
2
3
4
5
6
7
8
9
10
11
12
reconcileFuncs := []reconcileFunc{
r.reconcileAutoscalerServiceAccount,
r.reconcileAutoscalerRole,
r.reconcileAutoscalerRoleBinding,
r.reconcileIngress,
r.reconcileAuthSecret,
r.reconcileHeadService,
r.reconcileHeadlessService,
r.reconcileServeService,
r.reconcileGCSStoragePVC,
r.reconcilePods,
}

进入 reconcilePods 后,Controller 先列出现有 head、worker Pod,再与 RayCluster spec 中的期望数量比较。head 不存在时调用 createHeadPod;某个 worker 组缺少 diff 个成员时,循环调用 createWorkerPod。这两个创建函数的步骤相同:

  1. buildHeadPod 或 buildWorkerPod 根据 RayCluster 中的 Pod 模板构造 Pod,并把 RayCluster 设为 owner。
  2. 启用 Volcano 时,AddMetadataToChildResource 在 Pod 上补充 schedulerName、PodGroup 名和成员角色。
  3. RayCluster Controller 调用 Kubernetes client 的 Create,把 Pod 对象写入 API,并记录对应的 expectation。
  4. 默认调度器或 Volcano 观察尚未绑定的 Pod,为它选择 Node;节点上的 kubelet 再启动容器。
从 RayJob Controller 写入 RayCluster,到 RayCluster Controller 创建 head、worker Pod,再由调度器绑定节点和 kubelet 启动容器的执行链。每一步由不同组件完成。
从 RayJob Controller 写入 RayCluster,到 RayCluster Controller 创建 head、worker Pod,再由调度器绑定节点和 kubelet 启动容器的执行链。每一步由不同组件完成。 查看原图

到这里,RayCluster Controller 已把 Pod 对象写入 API,调度器接着填写节点绑定结果,kubelet 再根据绑定到本节点的 Pod 启动容器。后续 Pod 状态变化通过 Owns(Pod) 触发 RayCluster 的新一轮协调,Controller 据此更新集群状态。

对象或动作 负责组件
创建 RayCluster 对象 RayJob Controller
创建 head、worker Pod 对象 RayCluster Controller
为 Pod 选择并绑定 Node Kubernetes 默认调度器或 Volcano Scheduler
在节点上启动 Pod 容器 kubelet 与容器运行时
创建 submitter Kubernetes Job RayJob Controller
根据 Job 创建 submitter Pod Kubernetes Job Controller

KubeRay 在哪里接入 Volcano

Volcano 适配器运行在 KubeRay Operator 进程内。它不负责选择节点,只负责创建 PodGroup,并把调度信息写到 RayCluster、Pod 模板和 submitter 模板。独立运行的 Volcano Scheduler 观察这些 Kubernetes 对象,再执行准入、Gang 判断和绑定。

Operator 可通过 --batch-scheduler=volcano 启用适配器;Helm 配置对应 batchScheduler.name: volcano。集群中还需要安装 Volcano CRD、Scheduler,并提前创建要使用的 Queue。

KubeRay 的 BatchScheduler 适配器在 Operator 进程内。它创建和更新 Kubernetes 资源;独立的 Volcano Scheduler 观察这些对象并绑定 Pod。两者之间没有直接的作业提交 RPC。
KubeRay 的 BatchScheduler 适配器在 Operator 进程内。它创建和更新 Kubernetes 资源;独立的 Volcano Scheduler 观察这些对象并绑定 Pod。两者之间没有直接的作业提交 RPC。 查看原图

适配器入口同时接受 RayJob 和直接创建的 RayCluster:

1
2
3
4
5
6
7
8
switch obj := object.(type) {
case *rayv1.RayCluster:
return v.handleRayCluster(ctx, obj)
case *rayv1.RayJob:
return v.handleRayJob(ctx, obj)
default:
return fmt.Errorf("unsupported object type %T, only RayCluster and RayJob are supported", object)
}

主案例由 RayJob 创建专用 RayCluster,因此 PodGroup 名为 ray-demo-job-pg,owner reference 指向 RayJob。RayCluster Controller 后续也会调用适配器,但它识别到这个 RayCluster 来自 RayJob 后直接返回,不再创建第二个 PodGroup。直接创建 RayCluster 时,PodGroup 才按 RayCluster 名命名并由它拥有。

KubeRay 写出 Kubernetes 对象,Volcano 据此调度 Pod;RayCluster Ready 后,submitter 再通过 Ray Jobs API 提交程序。

参数怎样传到 PodGroup 和成员 Pod

接入信息分成两类:一类写进 PodGroup,描述整组需要什么;另一类写进 Pod,说明它属于哪一组并由哪个调度器处理。

最低成员数和最低资源量

没有自动扩缩容时,主案例的 PodGroup 参数如下。用 H、W、S 分别表示一个 head、一个 worker 和 submitter 的资源请求;它们都可以包含 CPU、内存等多个资源量,按相同维度相加:

字段 来源 本例结果
minMember 1 个 head 加全部期望 worker 3
minResources head、worker 和 submitter 的资源请求之和 H + 2W + S

submitter 属于同一个 PodGroup,但不计入 minMember。原因来自创建顺序:KubeRay 必须先等 RayCluster Ready,之后才创建 submitter。如果 minMember 要求 submitter 同时存在,三个 Ray Pod 会等第四个成员,第四个成员又要等前三个成员组成的集群 Ready,流程无法继续。

左侧展示把 submitter 计入 minMember 后的等待环;右侧是当前实现:先让三个 Ray Pod 满足最低成员要求,集群就绪后再创建 submitter。资源记账仍提前包含 S。
左侧展示把 submitter 计入 minMember 后的等待环;右侧是当前实现:先让三个 Ray Pod 满足最低成员要求,集群就绪后再创建 submitter。资源记账仍提前包含 S。 查看原图

适配器在已经算好的 Ray 集群资源后面追加 submitter 资源:

1
2
3
4
submitterResource := getSubmitterResource(rayJob)
totalResourceList = append(totalResourceList, submitterResource)
_, err := v.syncPodGroup(ctx, rayJob, minMember, utils.SumResourceList(totalResourceList))
return err

因此,minMember 只要求 head 和 worker 先组成可运行集群,minResources 却会提前把 submitter 的资源需求计入组需求。它只是一项资源约束,不会提前创建 submitter Pod,也不会为它选定节点。

开启自动扩缩容时,适配器使用最低副本数;关闭时使用期望副本数:

1
2
3
4
5
6
rayCluster := &rayv1.RayCluster{Spec: *rayClusterSpec}

if !utils.IsAutoscalingEnabled(rayClusterSpec) {
return utils.CalculateDesiredReplicas(rayCluster) + 1, utils.CalculateDesiredResources(rayCluster)
}
return utils.CalculateMinReplicas(rayCluster) + 1, utils.CalculateMinResources(rayCluster)

worker 组的副本数还要乘以 numOfHosts。例如两个副本、每个副本四个 host,会生成八个 worker Pod。暂停的 worker 组不计入当前最低需求。本例使用 numOfHosts=1,关闭自动扩缩容,所以两个副本对应两个 worker Pod。

Queue、优先级和成员角色

PodGroup 创建时会从 RayJob 的 label 读取 Queue 和 PriorityClass:

RayJob 输入 写入位置 作用
volcano.sh/queue-name: ray-batch PodGroup spec.queue 选择 Volcano Queue
ray.io/priority-class-name PodGroup spec.priorityClassName 设置组的调度优先级
RayCluster 模板中的副本和资源请求 PodGroup minMember、minResources 表达组的最低成员和资源需求

构造子资源时,适配器再写入 PodGroup 关联和角色。group-name 填组名,task-spec 填成员角色:本例中 headgroup 表示 head,compute 沿用 Ray worker 组名,submittergroup 表示提交程序。它们作为元数据随 Pod 进入调度器:

1
2
annotations[volcanoschedulingv1beta1.KubeGroupNameAnnotationKey] = getAppPodGroupName(parent)
annotations[volcanobatchv1alpha1.TaskSpecKey] = groupName
子资源 group-name task-spec schedulerName
head Pod ray-demo-job-pg headgroup volcano
worker Pod ray-demo-job-pg compute volcano
submitter Pod 模板 ray-demo-job-pg submittergroup volcano

group-name 把 Pod 关联到 PodGroup;task-spec 只是区分组内角色,不会另外创建名为 headgroup、compute 或 submittergroup 的 Kubernetes 资源。

主案例的拥有关系与调度关联。实线表示 owner 链,虚线表示 PodGroup 关联。submitter Pod 由 Kubernetes Job Controller 创建;三类 Pod 的 task-spec 值各不相同。
主案例的拥有关系与调度关联。实线表示 owner 链,虚线表示 PodGroup 关联。submitter Pod 由 Kubernetes Job Controller 创建;三类 Pod 的 task-spec 值各不相同。 查看原图

两边的概念怎样对应

KubeRay 或 Kubernetes 对象 Volcano 看到的含义
RayJob 管理整次集群准备和程序提交;主案例中的 PodGroup 由它拥有
RayCluster 生成 head、worker Pod;来自 RayJob 时沿用已有 PodGroup
PodGroup 一组成员的最低数量、最低资源、Queue 和优先级声明
head、worker、submitter Pod Volcano Scheduler 中参与调度的成员,缓存中表示为 TaskInfo
同一 group-name 的一组 Pod Scheduler 中归入同一个 JobInfo,并接受 Gang 约束
Queue 多个 JobInfo 竞争资源时使用的策略边界
Ray 的 worker group 通过 task-spec 保留角色名称;它本身不是 Volcano Queue

这里最容易混淆的是“Job”。RayJob 是 KubeRay 的 Kubernetes 自定义资源;submitter Kubernetes Job 用来运行提交程序;Volcano 的 JobInfo 是 Scheduler 对一组待调度成员的内存表示;真正提交给 Ray 的作业又由 Ray Jobs API 管理。它们位于不同阶段。

联合使用时需要注意什么

PodGroup 写入成功后就继续创建 RayCluster

RayJob Controller 的顺序是先调用 DoBatchSchedulingOnSubmission,再调用 getOrCreateRayClusterInstance。前者返回成功表示 PodGroup 已经创建、更新或确认无变化;Controller 不读取 PodGroup phase,也不等待 Volcano 完成调度。后续 head、worker Pod 是否能启动,由 Volcano 根据 PodGroup 和节点资源决定。

RayJob 只支持专用 RayCluster 的这条接入路径

主案例使用 spec.rayClusterSpec 创建专用集群。Volcano 适配器不支持 RayJob 通过 clusterSelector 引用已有 RayCluster 来建立 Gang 调度需求。直接创建 RayCluster 可以单独使用适配器,但对象拥有关系和 PodGroup 名会改为以 RayCluster 为准。

模板资源与最终 Pod 需要保持一致

适配器从模板容器的 requests 计算 minResources;某个资源没有 request 时,再取对应 limit。Admission 阶段后来注入的 sidecar、Pod overhead 或模板没有覆盖的容器资源,可能让最终 Pod 请求与提前计算的组需求不同。使用时应以最终 Pod 和 PodGroup 两边的资源字段一起核对。

完成和重试会更新同一个 PodGroup

K8sJobMode 完成后,适配器根据仍然存活的 RayCluster 重新计算最低成员和资源,不再追加已经完成的 submitter。worker 组被暂停时,只保留 head 等仍需运行的部分;集群已经不存在或进入删除流程时,组需求更新为空。

更新 PodGroup 为零不等于节点资源已经释放。终止中的 Pod 仍要等 kubelet 退出容器,并由 SchedulerCache 观察状态变化。整次执行重试时,新的 RayCluster 名和 submission ID 可以改变,RayJob 名生成的 PodGroup 名保持不变,初始化阶段会重新填写需求。

小结:接入改变了哪一段流程

开启 Volcano 没有改变 RayJob“创建集群、等待 Ready、再提交程序”的主干。变化发生在创建 RayCluster 之前和创建 Pod 时:KubeRay 先写入 PodGroup,再让 head、worker 和 submitter 携带相同的组名并交给 Volcano 调度。

理解这条顺序以后,参数传递也可以按对象查看:副本和资源请求进入 PodGroup 的最低需求,Queue 和优先级进入 PodGroup 策略,group-name、task-spec 和 schedulerName 进入成员 Pod。Volcano 负责这些 Pod 怎样获得节点,KubeRay 继续负责 RayCluster 与 RayJob 的生命周期,Ray Jobs API 最后负责提交用户程序。

第十篇的系列总结会从这些实现回到 Operator 设计,讨论资源建模、并发、恢复与高可用,以及 Kubernetes 提供的支撑机制。

参考资料


第 9 篇|KubeRay 与 Volcano:接入与生命周期协作
https://tanxinyu.work/kuberay-volcano-09-kuberay-volcano/
作者
谭新宇
发布于
2026年9月15日
许可协议