Volcano 源码阅读:多个作业怎样共享与竞争资源
上一篇的 Gang 检查回答了一组 Pod 能否满足最低要求。现在有两个作业都在等资源:demo-job 和独立提交的 demo-job-b。它们应该按提交时间排队,按优先级选择,还是按已经占用的资源量轮流获得机会?
Volcano 把这些选择放在不同层次:Queue 表达资源策略,作业和成员各有排序,节点还有自己的放置约束。这篇沿 Volcano d8984501e4ad 的 proportion、drf 和传统 preempt、reclaim 路径阅读。默认配置仍承接第七篇;分析驱逐时会显式增加对应 Action。数字例子只演示算法,不表示主案例已经修改配置或完成压测。
1. Queue 里配置的值,和本轮算出的份额
先看两个作业都提交到 ray-batch 的情况。它们共享该 Queue 的资源策略;作业之间如何排序,稍后再看。讨论跨队列分配时,另设一个对比场景:第二个作业在提交时就指定新建的 ray-batch-b。这是两个提交条件不同的例子,没有把运行中的作业迁移到另一队列。
以 proportion 插件为例,它为每个参与计算的 Queue 构造一份 queueAttr。下面几个量看起来都在描述“队列有多少资源”,来源和用途却不同。
| 名称 | 来源与含义 |
|---|---|
weight |
Queue 配置的权重,用于计算有需求队列的相对份额 |
capability |
Queue 配置的资源上限;插件还会结合总资源和其他队列保障量计算 realCapability |
guarantee.resource |
配置的保障资源,参与份额下限及其他队列可用上限的计算 |
request |
插件从本轮可见的已分配和 Pending 成员汇总的需求 |
deserved |
proportion 在本轮依据权重、需求和上限算出的份额 |
allocated |
调度模型中已经分配的资源量,按成员资源请求记账,不是实时 CPU 使用率 |
因此,“业务当前 CPU 使用率很低”和“调度器认为队列还占着很多资源”可以同时成立。只要相应 Pod 仍占用资源请求,节点或队列的调度账目就不会因为进程暂时空闲而自动归零。
guarantee 也需要结合资源条件理解。它参与资源分配策略,并不让没有 GPU 的集群凭空满足 GPU 请求,也不会消除节点亲和性造成的限制。
2. 用一组数字推演 proportion
为看清权重的作用,先只算 CPU:本轮可分配总量为 12 核,没有其他占用;两个 Queue 优先级相同,权重分别为 1 和 2,保障量为零,上限不构成限制。假设 Pod 请求可以组成所需份额,内存和放置约束都已满足。
如果两个队列的需求都足够大,第一轮份额计算得到 4 核和 8 核。这里是在计算后续分配要遵循的额度,还没有把 Pod 放到节点上。源码中对应的动作,是把剩余资源乘以权重比例,加入各自 deserved。proportion.go 的连续节选如下:
1 | |
这里紧接着做了上限和需求裁剪。若 ray-batch 实际只需要 2 核,分给它的 4 核就会裁到 2 核;另一队列先得到 8 核。剩余 2 核继续分配给尚未满足需求、也未达到上限的队列,最终可以得到 2 核和 10 核。
| 本轮条件 | ray-batch 的 deserved |
ray-batch-b 的 deserved |
未计入两队列份额的量 |
|---|---|---|---|
| 双方需求均充足,上限不限制 | 4 核 | 8 核 | 0 |
| 第一队列只需 2 核,第二队列至少需 10 核 | 2 核 | 10 核 | 0 |
| 第一队列只需 2 核,第二队列上限为 7 核 | 2 核 | 7 核 | 3 核 |
第三种情况下,余下的 3 核不会继续计入这两个队列的份额:一个已经满足需求,另一个受到上限限制。只有后续 Pod 成功分配,份额才可能转化为实际占用;节点条件或 Gang 门槛还可能让其中一部分暂时用不上。权重 1:2 不是任何时刻都必须维持的占用比例,也不是永久固定的配额。
此外,deserved 的变化不会立即改变运行中的 Pod。假如第二个队列此前已经使用 10 核,现在第一队列新增需求,重新计算可能得到 4 核和 8 核,但第二个队列当前仍占着 10 核。如何让占用向新的份额靠拢,还需要等待资源自然释放或执行符合策略的回收。
3. 排得靠前,和还能拿资源,是两件事
proportion 注册的 Queue 排序函数先比较 Queue.spec.priority,相同时再比较资源份额使用程度。其 share 按各资源维度的 allocated / deserved 计算,取最大值。份额使用程度较低的队列,在这个比较中排在前面。
但从优先队列取出一个 Queue 后,还要执行资源可分配性检查。插件会计算分配候选 Pod 后的占用量,检查它是否超过该队列本轮的 deserved:
1 | |
这两行来自 queueAllocatable 的连续节选。排序决定先检查谁,可分配性决定这个候选还能不能继续拿资源。随后节点过滤仍可能失败,因此“队列有额度”和“Pod 能放下”还隔着一步。
Queue 内部再按 JobOrder 回调选择作业。第七篇的配置把 priority 放在 drf 前面;Session.JobOrderCompareFn 按启用回调的顺序,遇到非零比较结果就返回。两个作业优先级不同时,前面的 priority 已经作出选择,不会再把 DRF 算出的份额与它加权平均。比较仍相同时,框架才使用创建时间和 UID 等后备规则。
这里有两个不同的优先级:Queue.spec.priority 参与队列排序,PodGroup 引用的 PriorityClass 参与作业优先级。成员 Pod 还有自己的优先级字段。在调试“为什么它先运行”时,应先确认当前比较的是哪一层对象。
4. CPU 和 GPU 混用时,怎样衡量份额
只数 Pod 个数无法比较资源占用:一个 Pod 请求 1 核 CPU,另一个可能请求 8 核和一张 GPU。DRF,即 Dominant Resource Fairness,用一个作业占用各类资源的最大比例表示它的主导资源份额。
单独构造一个数学例子:总资源为 16 核 CPU、4 张 GPU,两个作业同优先级,前面的排序回调没有分出先后。这里只看 DRF JobOrder 回调的比较,不声称这是主案例的实际配置。
| 作业 | 已分配资源 | CPU 份额 | GPU 份额 | 主导份额 |
|---|---|---|---|---|
demo-job |
8 核、1 张 GPU | 8/16 = 50% | 1/4 = 25% | 50% |
demo-job-b |
2 核、2 张 GPU | 2/16 = 12.5% | 2/4 = 50% | 50% |
两个作业占用的形状不同,主导份额却相同。若第二个作业只有 2 核、1 张 GPU,它的主导份额就是 25%,DRF 的这个比较会让它排在前面。实际能否再分配,仍然取决于它所在队列的份额、剩余资源和 Pod 约束。
源码 drf.calculateShare 逐一计算资源维度的 share,保留最大值。每次分配或撤销分配,插件通过事件回调调整已分配量,后续比较就能反映本轮发生的变化。
proportion 和这里的 DRF 作用层次也不同:前者计算 Queue 的份额并约束队列分配,后者在当前配置中参与 Job 排序等判断。Volcano 还提供 capacity 插件,直接读取 Queue.spec.deserved 作为配置份额。注意这个同名字段:proportion 的 deserved 是本轮算出的内存值,capacity 这里读取的是 Queue 声明中的配置值。更换插件后,不能继续套用前面的权重计算过程。另有按队列层级组织公平分配的层级 DRF,当前源码明确拒绝它与 proportion 的冲突组合;本篇不采用这种配置。
5. preempt 与 reclaim 怎样释放资源
默认的 enqueue, allocate, backfill 不包含主动驱逐。这里的驱逐指让选中的已有 Pod 退出,把资源释放给等待者;被选中的 Pod 在源码中常称为 victim,下文称为受害成员。以下分析假设配置已启用对应 Action,等待中的作业也已经通过前置准入。单纯提高优先级,不足以越过所有准入和资源约束。
传统 preempt 处理队列内部的竞争。源码先寻找同一 Queue 中其他作业的候选成员,另外还有同一作业内成员竞争的分支。跨作业候选的核心条件是:
1 | |
候选还要满足可抢占状态、成员允许被抢占等条件。priority 插件在不同作业间比较作业优先级,在同一作业内比较成员优先级;Gang 等插件继续筛选受害者,节点约束也要验证。作业只是排得靠前,并不代表一定能驱逐任意一个正在运行的 Pod。
传统 reclaim 处理跨 Queue 的资源回收。它从其他队列里寻找允许被回收的运行中成员,检查来源队列的 reclaimable 设置,再交给策略回调判断。在 proportion 中,超过 deserved 的占用是筛选回收对象的重要依据。
| 路径 | 主要竞争范围 | 还要检查什么 |
|---|---|---|
preempt |
同 Queue 的作业之间,以及同作业内成员之间 | 优先级、可抢占性、Gang、节点条件等 |
reclaim |
不同 Queue 之间 | 来源队列允许回收、资源份额、成员可抢占性、Gang、节点条件等 |
传统路径下,Gang 回调有一项很具体的限制:它按候选依次减少受害作业的可用成员计数,只有计数高于 MinAvailable 时才允许继续选成员。例如某组正好只有最低要求的三个成员,不能把其中一个当作不影响成组要求的冗余成员回收。
当前源码另有 gangpreempt、gangreclaim,支持以组为单位组织驱逐决策,采用不同的候选组合模型。上面的限制针对本篇跟踪的传统 Action,不能推广成“Volcano 永远不会驱逐一个最低运行组”。这些新路径不在默认配置中,本篇不把它们与传统回调混写。
preempt 和 reclaim 同样使用 Statement。它们可以先推演受害成员退出后是否放得下等待者,并将等待者记为 Pipelined。没有满足要求时撤销尝试;提交驱逐以后,资源也不会在同一瞬间可用。Pod 终止、缓存观察到变化、等待成员重新分配和绑定,都有自己的时间窗口。
对 Ray 应用来说,被驱逐的 worker 上可能有用户任务、actor 或内存中的对象。后续 Pod 被补回,不代表这些计算必然无损恢复。Ray 的重试设置、检查点以及应用处理重复执行的方式,仍需按第四、五篇的故障边界分析。
6. 有空闲资源,为什么作业还在排队
把本篇的判断按执行顺序放回现场,可以缩小排查范围:
- 查看实际加载的 Action、插件和参数。没有启用驱逐,就先分析自然释放条件;作业未准入,则先查 enqueue。
- 查看 Queue 是否 Open、PodGroup 的队列引用是否正确,再比较需求、上限、保障量、deserved 和 allocated。不要用实时 CPU 利用率替代资源请求账目。
- 查看单个 Pod 的放置失败原因。资源碎片、GPU 型号、节点亲和性和存储条件,都可能让总量足够的资源无法使用。还要看节点污点:这是节点上用来排斥特定 Pod 的标记,相应 Pod 需要配置容忍条件才能通过这类检查。
- 查看 Gang 的最低要求与可用成员。一个候选放不下,可能使本轮已试分配的部分被撤销。
- 若已触发驱逐,检查受害 Pod 是否真正退出,以及等待成员的绑定和启动是否完成。
回到本系列的问题,KubeRay 创建了多少 Pod,与 Volcano 当前允许哪一组占用资源,由不同的循环决定。下一篇会逐字段核对 KubeRay 如何生成 PodGroup,并解释 submitter 还不存在时,为什么已经出现在组的最低资源量里。