Volcano 源码阅读:从 Pod 到作业

前五篇讨论了 KubeRay 怎样把一个 RayJob 变成运行中的 Ray 集群。这里还留着一个问题:Operator 已经创建了 Pod,集群却没有足够的空闲资源,几套 Ray 集群该怎样分配这些资源?

第三篇里的协调并发数决定多少个 RayCluster 可以同时被检查。调大这个数字,Operator 可以更快地创建 Pod,却不会增加机器上的 CPU、内存和 GPU。接下来四篇沿着这条边界,阅读负责批调度的 Volcano:这一篇先看对象、进程和协作关系,第七篇跟踪一次成组调度,第八篇分析资源竞争,第九篇回到 KubeRay 的接入实现。

源码基线为 Volcano d8984501e4ad,KubeRay 仍为 6bf05eb17a3e。下面的资源数量用于源码推演,没有进行集群实验。主案例继续使用 default/demo-job,一个 head Pod、compute 组的两个 worker Pod,使用 K8sJobMode,关闭自动扩缩容。相对前五篇,这里新增的条件是启用 Volcano。

1. 为什么要把几个 Pod 放在一起考虑

假设有两个独立提交的 RayJob:demo-jobdemo-job-b。为了只看调度问题,先把每个作业所需的三个 Ray Pod 简化为三个等大的资源槽,并假定业务需要三个成员都到齐才能有效计算。submitter 稍后才创建,其资源要求在第九篇单独计算。

机器一共能放下四个这样的 Pod。如果两个作业各拿到两个槽,资源已经用满,双方却都还在等第三个成员。先让一组拿到三个槽,就有机会完成一次计算,释放资源后再安排另一组。

两个作业都需要三个成员。四个资源槽被各占两个时,双方都可能等待;先满足一组则能推进计算。这里只演示一种可能的分配结果。
两个作业都需要三个成员。四个资源槽被各占两个时,双方都可能等待;先满足一组则能推进计算。这里只演示一种可能的分配结果。 查看原图

这里先考虑未配置成组策略、逐个 Pod 寻找节点的调度路径。调度器不会从 Ray 程序里自动推导出这三个 Pod 必须配合。Volcano 用 PodGroup 表达这种关系,再由 Gang 插件检查一组成员的最低运行要求。Gang 在这里指成组调度:尝试分配资源时,要检查作业整体是否满足门槛。本文分析这一种实现,不据此判断其他版本或扩展调度器是否提供类似能力。

这个前提不能反过来理解为“所有 Ray 程序都要等全部 worker”。Ray task 可以按可用资源逐步执行,部分应用也允许弹性成员数。是否需要成组调度,要看应用对最低资源量和成员数量的要求。配置过高的门槛会延长等待,甚至让原本可以逐步运行的程序始终无法启动。

此外,四个槽只是便于推演。实际资源要同时记录 CPU、内存、GPU 等多个数量,下文称为资源向量:某台节点空闲 CPU 很多,也可能没有 GPU;集群总量够,也可能被分散在几台放不下单个 Pod 的节点上。组的需求和单 Pod 的放置条件都需要检查。

2. Job、PodGroup 和 Queue 分别表达什么

读 Volcano 时经常遇到 Job。这个名字至少出现在三个不同的位置,先把它们放回各自的 API 里。表中的 Volcano task 指一类 Pod 的模板和副本要求;进入调度器后,一个 TaskInfo 通常对应一个具体 Pod。它与 Ray 中的一次函数调用不是同一层对象。

对象 保存什么 由谁使用
Kubernetes Job,batch/v1 运行到完成的 Pod 模板、重试等要求 Kubernetes Job Controller;本系列的 submitter 就采用它
Volcano Job,batch.volcano.sh/v1alpha1 多类 task 的 Pod 模板、副本数、生命周期策略等 Volcano Job Controller
RayJob,ray.io/v1 Ray 集群配置、程序入口、提交方式和结束策略 KubeRay RayJob Controller

Volcano Job 是一条原生提交入口。若工作负载已经由 KubeRay 这样的 Operator 管理,可以继续使用原有资源,通过 PodGroup 把调度要求交给 Volcano。

PodGroup 是命名空间级资源,主要描述哪些 Pod 作为一组参与调度,以及组的最低要求。它不包含一套用来创建 Ray head、worker 的模板。下面是 scheduling/v1beta1/types.go 中几个字段的节选,省略了注释和其他字段:

1
2
3
4
MinMember int32 `json:"minMember,omitempty" protobuf:"bytes,1,opt,name=minMember"`
Queue string `json:"queue,omitempty" protobuf:"bytes,2,opt,name=queue"`
PriorityClassName string `json:"priorityClassName,omitempty" protobuf:"bytes,3,opt,name=priorityClassName"`
MinResources *v1.ResourceList `json:"minResources,omitempty" protobuf:"bytes,4,opt,name=minResources"`

minMember 表达最低成员数,minResources 表达最低资源量。queue 指定这组 Pod 属于哪个 Queue,priorityClassName 引用 Kubernetes PriorityClass。PriorityClass 是把一个名称映射为数值优先级的集群级对象,供调度器比较先后次序。字段怎样参与判断取决于启用的插件,第七、八篇会沿调用位置展开。

Queue 是集群级资源,用来组织共享资源的工作负载。与按 namespace 区分的 PodGroup 不同,Queue 在整个 Kubernetes 集群里按名称识别,一个队列可以被多个命名空间中的 PodGroup 引用。它有权重、资源上限、保障资源和状态等信息;资源策略插件读取这些配置,决定哪些作业可以进入调度、哪个队列应先获得资源。

后文会检查 Queue 是否处于 Open,这是允许接纳新作业的开放状态。它说明队列可接受工作,至于资源是否足够,还要继续计算。

原生 Volcano Job 和 RayJob 是两条提交路径。各自的控制器维护工作负载,并通过 PodGroup 表达调度要求;Queue 可被多个 PodGroup 引用。实线表示管理关系,虚线表示调度关联。
原生 Volcano Job 和 RayJob 是两条提交路径。各自的控制器维护工作负载,并通过 PodGroup 表达调度要求;Queue 可被多个 PodGroup 引用。实线表示管理关系,虚线表示调度关联。 查看原图

主案例的 Queue 命名为 ray-batch,PodGroup 命名为 ray-demo-job-pg。Pod 通过 scheduling.k8s.io/group-name annotation 指向组。这个关联与 owner reference 是两回事:前者供调度器归组,后者表达 Kubernetes 对象的拥有关系,影响事件映射和垃圾回收。

Volcano Scheduler 内部还有一个 JobInfo。它是调度器组织 PodGroup 和成员信息的内存模型,不能看到变量名 job 就认定来源一定是 Volcano Job。KubeRay 创建的 PodGroup 同样可以形成这里的 JobInfo。

3. 核心模块分别运行在哪里

把基本部署拆开看,有三个主要的常驻服务角色:Scheduler、Controller Manager 和 Admission Webhook。Webhook 是供 API Server 调用的 HTTP 回调服务;这里的 Admission 指资源写入前的准入处理,可以校验请求或补全字段。这三个角色可以分别部署,也可以有多个副本。这里数的是职责与进程角色,实际 Pod 数量由安装配置决定。

角色 核心功能 源码入口
Scheduler 观察 Pod、PodGroup、Queue、Node;维护调度缓存;执行资源分配与绑定 cmd/schedulerpkg/scheduler
Controller Manager 运行 Job、Queue、PodGroup 等控制器,维护对应对象的生命周期和关联资源 cmd/controller-managerpkg/controllers
Admission Webhook 处理 API Server 转发的匹配请求,校验或补全资源字段 cmd/webhook-managerpkg/webhooks

客户端工具用于提交、查询和管理资源,不是每次作业都需要附带运行的常驻服务。当前源码还有拓扑、分片等扩展组件,这四篇先分析基本调度路径,不把所有 cmd 目录都算成必装进程。

Controller Manager 内部的多个控制器也不对应多个独立进程。cmd/controller-manager/app/server.gostartControllers 遍历已注册且启用的控制器,初始化后启动各自的运行循环。关键动作是:

1
go c.Run(ctx.Done())

它们可以在同一进程里共享客户端和 informer。informer 持续从 API Server 接收资源变化,维护本地缓存,再通知感兴趣的控制器。这与前面读 KubeRay 时看到的观察、排队和协调思路相通,不过具体框架和实现需要分别阅读。

基本部署中的进程边界。控制器和调度器通过 Kubernetes API 观察与更新对象;Admission 是 API Server 对匹配请求发起的同步调用。图中没有展开主备副本。
基本部署中的进程边界。控制器和调度器通过 Kubernetes API 观察与更新对象;Admission 是 API Server 对匹配请求发起的同步调用。图中没有展开主备副本。 查看原图

Scheduler 的 Action 和 Plugin 则运行在 Scheduler 进程内。Action 组织调度阶段,Plugin 注册排序、过滤、资源准入和就绪判断等函数。它们通常通过 Go 函数调用协作,不是每个插件起一个远程服务。

这里还要区分两种“准入”。Admission Webhook 检查这次 API 请求能否接受;Scheduler 的 enqueue 阶段检查一组工作负载是否可以进入调度。RayJob 被 API Server 保存,只说明声明已经写入,后面仍可能因为资源不足而等待。

4. 一次提交如何经过这些组件

先看 Volcano Job 原生路径。用户提交 Job 后,Volcano Job Controller 维护对应的 PodGroup。源码 job_controller_actions.go 会观察 PodGroup 的 phase;在这个版本中,组仍处于空状态或 Pending 时,不进入正常的 task 同步创建流程。Scheduler 通过 enqueue 推进准入,Controller 再据此继续创建 Pod。

这个先准入、后创建成员的过程,可以减少原生作业在资源不足时生成大量等待中的 Pod。也说明两端靠资源状态协作:Controller 写出需求,Scheduler 更新调度状态,Controller 从后续观察中得知下一步可以继续。

KubeRay 的路径有所不同。RayJob Controller 创建 PodGroup 和 RayCluster,RayCluster Controller 随后维护 head、worker Pod。它不会复用 Volcano Job Controller 的那套“等待 PodGroup 准入再创建 task”的流程。因此,接入 Volcano 后仍可能看到已经创建、尚未调度的 Ray Pod。

KubeRay 路径的一次提交。箭头表示经 API 的请求或观察,以及节点启动动作;Ray 作业提交发生在 RayCluster 就绪以后。Volcano Job Controller 不参与创建这些 Ray Pod。
KubeRay 路径的一次提交。箭头表示经 API 的请求或观察,以及节点启动动作;Ray 作业提交发生在 RayCluster 就绪以后。Volcano Job Controller 不参与创建这些 Ray Pod。 查看原图

Pod 出现后,Scheduler 根据 schedulerName 处理属于自己的待调度 Pod,再通过 annotation 找到 PodGroup。Queue 策略和 Gang 约束参与资源分配;选好节点后,通过 Kubernetes API 把 Pod 与该 Node 的关系确定下来,这一步称为绑定(Bind)。绑定完成只说明确定了运行位置,节点上的 kubelet 还要观察结果、调用容器运行时启动容器。

KubeRay 继续观察集群是否就绪。等到条件满足,RayJob Controller 创建 submitter 的 Kubernetes Job,后者生成 submitter Pod。它也要经过调度、启动,才有进程调用 Ray Jobs API 提交程序。Volcano 在这里安排 Pod;用户函数和 actor 在 Ray 集群内部怎样执行,仍由 Ray 负责。

5. 后面读代码时,先看清对象属于哪一层

现在可以给几个容易混用的词划出范围。

代码或文章中的词 这里实际指什么
KubeRay 工作队列 等待协调的 Kubernetes 对象标识,例如一个 RayCluster 的 namespace/name
Volcano Queue 持久化的资源策略对象,例如集群级的 ray-batch
Scheduler 内部优先队列 一轮调度中,按照回调比较结果排列 Queue、JobInfo 或 TaskInfo 的内存结构
Volcano 调度器里的 task 通常对应一个 Pod;Volcano Job 的 task 模板还可以展开多个 Pod 副本
Ray task 用户提交给 Ray 执行的函数调用,与一个 Pod 不是一一对应关系
Controller 插件与 Scheduler 插件 前者扩展工作负载管理,后者参与调度决策,调用位置不同

如果 demo-job 的 Pod 一直没有运行,可以先检查对象在哪一步停下:PodGroup 是否创建,成员 Pod 是否存在,Queue 是否允许进入调度,Pod 是否已经绑定 Node。绑定后还停在启动阶段,就应继续查镜像、存储、网络和容器,而不能只调队列权重。

下一篇从 Scheduler 的 runOnce 开始,跟踪 ray-demo-job-pg 怎样在一次 Session 里试分配,以及为什么 Gang 判断通过以后仍然需要处理绑定失败。

参考资料


Volcano 源码阅读:从 Pod 到作业
https://tanxinyu.work/kuberay-volcano-06-volcano-architecture/
作者
谭新宇
发布于
2026年9月15日
许可协议