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-job 和 demo-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 | |
minMember 表达最低成员数,minResources 表达最低资源量。queue 指定这组 Pod 属于哪个 Queue,priorityClassName 引用 Kubernetes PriorityClass。PriorityClass 是把一个名称映射为数值优先级的集群级对象,供调度器比较先后次序。字段怎样参与判断取决于启用的插件,第七、八篇会沿调用位置展开。
Queue 是集群级资源,用来组织共享资源的工作负载。与按 namespace 区分的 PodGroup 不同,Queue 在整个 Kubernetes 集群里按名称识别,一个队列可以被多个命名空间中的 PodGroup 引用。它有权重、资源上限、保障资源和状态等信息;资源策略插件读取这些配置,决定哪些作业可以进入调度、哪个队列应先获得资源。
后文会检查 Queue 是否处于 Open,这是允许接纳新作业的开放状态。它说明队列可接受工作,至于资源是否足够,还要继续计算。
主案例的 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/scheduler、pkg/scheduler |
| Controller Manager | 运行 Job、Queue、PodGroup 等控制器,维护对应对象的生命周期和关联资源 | cmd/controller-manager、pkg/controllers |
| Admission Webhook | 处理 API Server 转发的匹配请求,校验或补全资源字段 | cmd/webhook-manager、pkg/webhooks |
客户端工具用于提交、查询和管理资源,不是每次作业都需要附带运行的常驻服务。当前源码还有拓扑、分片等扩展组件,这四篇先分析基本调度路径,不把所有 cmd 目录都算成必装进程。
Controller Manager 内部的多个控制器也不对应多个独立进程。cmd/controller-manager/app/server.go 的 startControllers 遍历已注册且启用的控制器,初始化后启动各自的运行循环。关键动作是:
1 | |
它们可以在同一进程里共享客户端和 informer。informer 持续从 API Server 接收资源变化,维护本地缓存,再通知感兴趣的控制器。这与前面读 KubeRay 时看到的观察、排队和协调思路相通,不过具体框架和实现需要分别阅读。
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。
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 判断通过以后仍然需要处理绑定失败。