Volcano 资源比较机制设计解析:多维度资源比较的语义、defaultValue 机制与实现落地
【免费下载链接】volcanoA Cloud Native Batch System (Project under CNCF)项目地址: https://gitcode.com/GitHub_Trending/vol/volcano
本文基于 Volcano 调度器的设计文档 resource-comparison.md,系统讲解 Volcano 调度器中资源比较函数的设计动机、九个推荐比较函数的语义矩阵、defaultValue(缺失维度按零或无穷处理)机制,并结合 pkg/scheduler/api/resource_info.go 的源码实现与 allocate、preempt、reclaim、proportion 等插件的真实调用点,帮助读者理解调度器如何在多资源维度(CPU、内存、GPU 等标量资源)下正确判断"资源是否足够",避免 proportion、preempt 等场景中反复出现的比较类缺陷。
背景与问题:资源比较函数的两大根因
在 Volcano 的调度流程中,大量逻辑依赖"两个资源向量的比较":节点空闲资源能否满足任务请求、队列已分配量是否超过应得量(deserved)、高优作业能否从低优作业处抢占/回收资源等。设计文档在复盘 proportion 插件与 preempt action 的相关缺陷时发现,大多数问题的根源集中在两处:
根因一:对缺失维度缺少考虑。两个资源列表的维度常常不完全相同。例如L = {cpu:1c, memory:1G}、R = {cpu:2c, memory:2G, gpu:2},L没有gpu维度。此时缺失维度的默认取值可以理解为zero或infinity两种语义:
- 默认值取zero:
L等价于{cpu:1c, memory:1G, gpu:0},此时各维度上L均小于R,可以认为L < R; - 默认值取infinity:
L等价于{cpu:1c, memory:1G, gpu:max},gpu维度上L大于R,因此不能认为L < R,但L仍然在部分维度上小于R。
设计文档指出,改造前的资源比较函数默认一律把缺失维度当作zero处理,这在不同语义的场景中会得出相反的结论。
根因二:资源比较函数覆盖不全。原有函数无法覆盖全部比较场景,导致后续开发者在关键位置误用。文档给出的典型例子是判断"节点空闲资源能否满足任务请求"——正确语义是任一维度的资源量不满足请求,节点即不可用,代码应写成if node.idle.LessPartly(task.request) { break },而不是用"全维度小于"语义的函数去判断。
为此,设计文档梳理了所有资源比较场景,给出了完整的函数族设计,并已在当前仓库中落地。
推荐函数矩阵:九个比较函数与转换关系
设计文档给出的推荐函数表如下(完整继承自 docs/design/resource-comparison.md):
| 函数名 | 语义 | 示例 | 原始函数 | 使用方 | 转换关系 |
|---|---|---|---|---|---|
l.Less(r, defaultValue) | l的所有维度值均小于r | L{cpu:1c, memory:2G} < R{cpu:2c, memory:4G} | Less(rr) | proportion | 独立实现 |
l.LessEqual(r, defaultValue) | l的所有维度值均小于或等于r | L{cpu:1c, memory:2G} <= R{cpu:1c, memory:4G} | LessEqual(rr)/LessEqualStrict(rr) | allocate/preempt/reclaim/overcommit/proportion/reservation/topology | 独立实现 |
l.LessPartly(r, defaultValue) | l的部分维度值小于r | L{cpu:4c, memory:2G} <| R{cpu:2c, memory:4G} | 无 | topology | 独立实现 |
l.LessEqualPartly(r, defaultValue) | l的部分维度值小于或等于r | L{cpu:4c, memory:2G} <=| R{cpu:2c, memory:2G} | 无 | 无 | 独立实现 |
l.Equal(r, defaultValue) | l全维度等于r且r全维度等于l | L{cpu:1c, memory:2G} = R{cpu:1c, memory:2G} | 无 | 无 | 独立实现 |
l.Greater(r, defaultValue) | l的所有维度值均大于r | L{cpu:2c, memory:4G} > R{cpu:1c, memory:2G} | 无 | 无 | !l.LessEqualPartly(r, defaultValue) |
l.GreaterEqual(r, defaultValue) | l的所有维度值均大于或等于r | L{cpu:2c, memory:4G} >= R{cpu:2c, memory:2G} | 无 | 无 | !l.LessPartly(r, defaultValue) |
l.GreaterPartly(r, defaultValue) | l的部分维度值大于r | L{cpu:4c, memory:2G} >| R{cpu:2c, memory:4G} | 无 | 无 | !l.LessEqual(r, defaultValue) |
l.GreaterEqualPartly(r, defaultValue) | l的部分维度值大于或等于r | L{cpu:2c, memory:2G} >=| R{cpu:2c, memory:4G} | 无 | 无 | !l.Less(r, defaultValue) |
表中的<|、<=|、>|、>=|是设计文档自定义的数学符号,表示"部分维度满足关系"。文档还说明:Part 系列函数之间存在语义重叠,但在具体应用中有其意义;设计当时 Part 系列函数尚未被任何插件使用,是面向后续扩展的预定义。
从转换关系可以看出设计思想:以 Less 家族为基元,Greater 家族通过取反派生(例如Greater = !LessEqualPartly),从而用最小的一组函数覆盖全序关系的九种组合,避免为每种关系单独维护一套缺失维度处理逻辑。
defaultValue 参数:缺失维度按 zero 还是 infinity 处理
推荐函数统一携带defaultValue参数,用于指定 L、R 中"空白维度"应取什么值,取值只能是zero或infinity之一。
源码实现:类型化的 DimensionDefaultValue
在 pkg/scheduler/api/resource_info.go 中,该语义由专门的枚举类型承载:
// DimensionDefaultValue means default value for black resource dimension type DimensionDefaultValue int const ( // Zero means resource dimension not defined will be treated as zero Zero DimensionDefaultValue = 0 // Infinity means resource dimension not defined will be treated as infinity Infinity DimensionDefaultValue = -1 )可以看到,设计文档中提议的defaultValue string("zero"/"infinity" 字符串)在最终实现中被类型化为DimensionDefaultValue整型枚举,从源码结构看这是为了避免裸字符串传参带来的拼写错误与隐式比较问题。
Resource结构本身将资源分为固定维度与动态维度(pkg/scheduler/api/resource_info.go):
type Resource struct { MilliCPU float64 Memory float64 // ScalarResources ScalarResources map[v1.ResourceName]float64 // MaxTaskNum is only used by predicates; ... MaxTaskNum int }CPU 与内存是固定字段,GPU、扩展资源、临时存储等动态维度放在ScalarResourcesmap 中——"缺失维度"正是指 map 中不存在的 key,这正是defaultValue要解决的问题。
defaultValue 在不同函数中的行为差异
对比 Less 实现 与 LessPartly 实现 可以发现,defaultValue并非一个全局开关,而是与各函数的语义耦合的:
- 在
Less/LessEqual(全维度语义)中:当defaultValue == Infinity时,只要右向量rr中存在左向量r缺失的标量维度,立即返回false——因为该缺失维度在左向量中被视为无穷大,不可能"小于/小于等于"右向量的有限值。而遍历r自身存在的维度时,若rr中查不到该维度且defaultValue == Infinity,则continue跳过(视为相等方向不产生否决)。 - 在
LessPartly/LessEqualPartly(部分维度语义)中:当defaultValue == Zero时,只要rr存在r缺失的标量维度,立即返回true——因为r的该维度按 0 处理,必然小于rr的正值。
这两种分支正是"zero 与 infinity 两种默认语义"在代码层面的直接体现,也是修复文档中根因一的关键。
另外值得注意的是LessEqual的容差设计(pkg/scheduler/api/resource_info.go):
lessEqualFunc := func(l, r, diff float64) bool { if l < r || math.Abs(l-r) < diff { return true } return false }比较使用minResource(定义为0.1,见 pkg/scheduler/api/resource_info.go)作为浮点容差。从源码结构看,这与设计文档中"原始函数"列里的LessEqual/LessEqualStrict两个函数的合并相呼应:带容差的LessEqual吸收了过去宽松比较的语义,而严格比较的场景则通过Less或按维度取值(Get/ResourceNames)来保证。
三个典型案例:defaultValue 对比较结果的影响
设计文档给出了三组对照案例,完整继承如下,可直接用于理解两种默认语义的差异。
案例一:L = {cpu: 1c, memory:1G},R = {cpu: 2c, memory:2G, gpu:2}(L 缺少 gpu 维度)
| L | R | |
|---|---|---|
| defaultValue = zero | {cpu: 1c, memory:1G, gpu:0} | {cpu: 2c, memory:2G, gpu:2} |
| defaultValue = infinity | {cpu: 1c, memory:1G, gpu:max} | {cpu: 2c, memory:2G, gpu:2} |
| Less | LessEqual | LessPartly | LessEqualPartly | |
|---|---|---|---|---|
| defaultValue = zero | true | true | true | true |
| defaultValue = infinity | false | false | true | true |
案例二:L = {cpu: 1c, memory:1G, gpu: 1},R = {cpu: 2c, memory:2G}(R 缺少 gpu 维度)
| L | R | |
|---|---|---|
| defaultValue = zero | {cpu: 1c, memory:1G, gpu:1} | {cpu: 2c, memory:2G, gpu:0} |
| defaultValue = infinity | {cpu: 1c, memory:1G, gpu:1} | {cpu: 2c, memory:2G, gpu:max} |
| Less | LessEqual | LessPartly | LessEqualPartly | |
|---|---|---|---|---|
| defaultValue = zero | false | false | true | true |
| defaultValue = infinity | true | true | true | true |
案例三:L = {cpu: 1c, memory:1G},R = {gpu: 2}(两者维度完全不重叠)
| L | R | |
|---|---|---|
| defaultValue = zero | {cpu: 1c, memory:1G, gpu:0} | {cpu: 0c, memory:0G, gpu:2} |
| defaultValue = infinity | {cpu: 1c, memory:1G, gpu:max} | {cpu: max c, memory: max G, gpu:2} |
| Less | LessEqual | LessPartly | LessEqualPartly | |
|---|---|---|---|---|
| defaultValue = zero | false | false | true | true |
| defaultValue = infinity | false | false | true | true |
三组案例放在一起可以看到规律:缺失维度的默认语义只影响"全维度"函数(Less/LessEqual)的结论方向(案例一中 zero 为 true、infinity 为 false;案例二恰好相反),而 Part 系列函数只关心"是否存在一个维度满足关系",因此在这些案例中结论稳定为 true。这也是文档中注释"Part scenarios are overlapped in part functions, but it makes sense when deal with specific applications"的含义。
源码实现纵深:函数如何被实现与相互转换
除前文分析的Less/LessEqual/LessPartly/LessEqualPartly外,当前实现还包含设计表中其余函数:
- Equal:全维度相等判断,同样带
minResource容差; - GreaterPartly:按设计表的转换关系实现,直接复用
LessEqualWithResourcesName并取反,还额外返回超出维度的资源名列表,便于上层输出"哪些资源超了"的可读信息; LessEqualWithResourcesName(pkg/scheduler/api/resource_info.go):LessEqual的"带诊断"变体,返回不足的资源名切片,供 reclaim/抢占等逻辑生成人类可读的原因。
一个能体现"文档修复误用"的实例是资源减法。Sub 的实现 在相减前先断言:
func (r *Resource) Sub(rr *Resource) *Resource { assert.Assertf(rr.LessEqual(r, Zero), "resource is not sufficient to do operation: <%v> sub <%v>", r, rr) return r.sub(rr) }即"被减数在每一个维度上都足够(缺失维度按 0 语义比较)才允许相减",这正是设计文档根因二所述"函数误用"问题被收敛后的表现——关键路径上明确声明了比较语义,而不是隐式依赖旧函数的默认行为。此外 Diff 实现 也会根据defaultValue为缺失维度填充默认值后再求差,Infinity 场景下差值以特殊标记表示,进一步印证defaultValue是贯穿比较、求差、缩放(MinDimensionResource)整套运算的统一语义。
调用点实战:哪些 action 与插件在用哪些函数
设计文档的"Used plugins/actions"一列标注了LessEqual被 allocate/preempt/reclaim/overcommit/proportion/reservation/topology 使用。当前仓库源码可以逐一印证这些调用点,且全部显式传入api.Zero作为defaultValue:
| 位置 | 调用 | 语义 |
|---|---|---|
| pkg/scheduler/actions/allocate/allocate.go | task.InitResreq.LessEqual(n.Idle, api.Zero) | 节点空闲资源(任一维度)能否满足任务初始请求 |
| pkg/scheduler/actions/preempt/preempt.go | preemptor.InitResreq.LessEqual(node.FutureIdle(), api.Zero) | 抢占后节点的"未来空闲"能否容纳抢占者 |
| pkg/scheduler/actions/reclaim/reclaim.go | resreq.LessEqual(availableResources, api.Zero) | 回收得到的资源能否满足请求 |
| pkg/scheduler/plugins/proportion/proportion.go | attr.request.LessEqual(attr.deserved, api.Zero) | 队列请求量是否未超过应得量 |
| pkg/scheduler/plugins/capacity/capacity.go | attr.deserved.LessPartly(totalDeserved, api.Zero) | deserved 与总额的部分维度比较(队列水位判断) |
| pkg/scheduler/plugins/task-topology/topology.go | maxResource.LessPartly(req, api.Zero) | 桶内剩余资源是否在某维度上小于任务请求 |
| pkg/scheduler/plugins/network-topology-aware/network_topology_aware.go | minResource.LessEqual(hnResourceStatus.idle, api.Zero) | 拓扑层级聚合空闲能否满足最小资源 |
两点值得注意:
- 调用方全部显式声明 defaultValue。改造前缺失维度默认按 zero 处理是隐式的;改造后每个调用点都必须写明
api.Zero或api.Infinity,语义被"提到阳光下",这正是设计文档"make it clear for later developers"的目标。 - 文档预言的 Part 系列函数已在当前代码中被启用。设计文档写作时"
LessPartly仅 topology 使用、其余 Part 函数暂无使用方",而当前仓库中LessPartly已用于 capacity 插件(比较 deserved/guarantee 与总额的水位)和 task-topology 插件,与设计表一致并有所扩展,印证了"预定义 Part 函数供后续使用"的设计意图。
测试验证:两种默认语义的行为被用例固定
pkg/scheduler/api/resource_info_test.go 对同一组资源用例分别以两种默认值驱动比较函数,将设计表中的布尔矩阵固化为回归保障:
- TestLess 相关用例:同一组 L/R 分别调用
test.resource1.Less(test.resource2, Zero)与Less(test.resource2, Infinity),验证缺失维度两种语义下结论不同; - TestLessEqual 相关用例:同样以
Zero与Infinity双路调用LessEqual断言。
这类"同输入、双默认值"的测试结构,与上文三个案例的表格形式一一对应,是阅读该设计文档时最有价值的交叉验证材料。
总结:如何选择正确的比较函数
结合设计文档与源码实现,可以在调度器开发中按如下原则选函数:
- 判断"资源 A 能否完全容纳请求 B"(节点空闲 vs 任务请求、队列 allocated vs deserved 等),使用
A.LessEqual(B, Zero)——任一维度不足即不满足,缺失维度按 0 处理;这也是 allocate/preempt/reclaim 等 action 的统一写法。 - 判断"A 是否在某个维度上小于/不足 B"(节点不可用早停、队列水位),使用
A.LessPartly(B, Zero)——文档中if node.idle.LessPartly(task.request) { break }即属此类。 - 缺失维度"无该资源"应视为 0 还是无穷,取决于业务语义:资源"请求/持有量"比较一般选
Zero;而当缺失维度表示"未声明即不受限"的场景(如某些额度/上限比较),从Less/LessEqual的 Infinity 分支行为看,设计已为此预留了完整的处理路径。 - 需要可诊断输出时,优先选用带资源名返回的变体(
LessEqualWithResourcesName、GreaterPartly),可直接生成"哪些资源超出/不足"的调度原因信息。
总体而言,Volcano 的资源比较机制以"九个函数 + 显式 defaultValue"为骨架,把原本散落在各插件中的隐式比较语义收敛为可枚举、可测试、可诊断的函数族;docs/design/resource-comparison.md 中的设计矩阵与 pkg/scheduler/api/resource_info.go 的实现、各 action 插件的调用点形成了完整的设计—实现—调用闭环,是理解 Volcano 调度器资源语义的必读材料。
【免费下载链接】volcanoA Cloud Native Batch System (Project under CNCF)项目地址: https://gitcode.com/GitHub_Trending/vol/volcano
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考