Volcano 队列优先级(Queue Priority):让 Queue 排序从隐式 share 变为显式可控
【免费下载链接】volcanoA Cloud Native Batch System (Project under CNCF)项目地址: https://gitcode.com/GitHub_Trending/vol/volcano
Volcano 早期的队列排序依赖“share 越小、优先级越高”这一隐式规则,对多租户运维而言并不直观。Volcano 在Queue资源的 spec 中新增了priority字段(取值 0 到 int32 最大值),使得用户可以显式地为每个队列指定优先级:priority 高的队列在 capacity、proportion 插件的队列排序中排在前面、优先被服务;同时 reclaim(跨队列资源回收)也会遵循该优先级,优先从低优先级队列中回收资源。本文基于设计文档 queue-priority.md 与当前仓库源码,完整讲清这一机制的 API 定义、排序实现与回收语义。
动机:为什么需要显式的队列优先级
按设计文档 docs/design/queue-priority.md 的 Motivation 部分,Volcano 当前(即引入该字段前)的队列优先级判定基于如下原则:
share 值越低的队列,在调度时的资源分配优先级越高。
这种机制虽然“有效地”决定了队列顺序,但存在明显的使用问题:
- 不直观:用户无法直接表达“队列 A 比队列 B 优先”,只能靠调节
deserved/weight间接改变 share,进而影响排序; - 不即时:调整份额后,share 随实际申请量动态变化,队列的相对顺序会随负载波动;
- 管理成本高:在多租户场景下,运维希望有清晰、直接的控制手段来决定队列被服务的顺序。
因此,设计目标是在Queue的 spec 中提供一个直接的priority属性,让用户“直接分配并调整队列优先级,从而显著简化队列管理”。
API 变更:QueueSpec 新增 priority 字段
字段定义
设计文档给出的 API 变更为:在queues.scheduling.volcano.sh的 spec 中新增priority属性,类型为数字,取值范围为0 到 int32 最大值:
spec: ... priority: type: number ...在当前仓库源码中,该字段已经落地。v1beta1 的 API 类型定义在 types.go:
// Priority define the priority of queue. Higher values are prioritized // for scheduling and considered later during reclamation. // +kubebuilder:validation:Minimum=0 // +optional Priority int32 `json:"priority,omitempty" protobuf:"bytes,10,opt,name=priority"`几个关键事实可以从源码确认:
- 默认值为 0:字段标注
+optional且无默认值注解,int32零值为 0,即不设置时与所有“未显式配置”的队列等价; - 最小值校验:
+kubebuilder:validation:Minimum=0会由 CRD 的 OpenAPI schema 强制校验,负值会被 API Server 拒绝; - 注释语义:源码注释明确写出“值越高,调度时越优先;在回收(reclamation)时越靠后被考虑”——这与设计文档中“victim 选择时优先考虑低优先级队列”的规则一致;
- 与 Job 优先级区分:这里的
priority是队列级的调度顺序控制,与Queue.Spec.Capability、Queue.Spec.Deserved(资源配额维度)以及 Job/Pod 自身的 priority(抢占维度)是三个不同层次的概念,不要混用。
对应的 CRD 生成文件位于 config/crd/volcano/bases 下,client 侧的 apply configuration 也已包含该字段(见 queuespec.go 中的Priority *int32)。
配置示例
一个启用队列优先级的 Queue 示例(结合仓库中 deployment/queue.yaml 的基本结构补充):
apiVersion: scheduling.volcano.sh/v1beta1 kind: Queue metadata: name: prod spec: weight: 100 capability: cpu: 64 memory: 128Gi priority: 10 # 高优先级队列,排序靠前、回收时靠后考虑 --- apiVersion: scheduling.volcano.sh/v1beta1 kind: Queue metadata: name: dev spec: weight: 100 capability: cpu: 64 memory: 128Gi priority: 1 # 低优先级队列,资源紧张时优先被回收队列排序:QueueOrderFn 的实现逻辑
设计文档的 Queue Ordering 部分规定(假设 overused 队列不进入调度流程的前提下):
- 优先比较 priority:priority 高的队列排在 PriorityQueue 前面,先被处理;
- priority 相同时,回退到原有的 share 比较逻辑。
capacity 插件的 QueueOrderFn
capacity 插件中,排序函数通过ssn.AddQueueOrderFn注册(见 capacity.go):
ssn.AddQueueOrderFn(cp.Name(), func(l, r interface{}) int { lv := l.(*api.QueueInfo) rv := r.(*api.QueueInfo) if lv.Queue.Spec.Priority != rv.Queue.Spec.Priority { // return negative means high priority return int(rv.Queue.Spec.Priority) - int(lv.Queue.Spec.Priority) } return cp.compareShareWithDeserved(cp.queueOpts[lv.UID], cp.queueOpts[rv.UID]) })实现要点:
- 返回值约定“返回负值表示前者优先级更高”(即排序器中 l 应排在 r 前)。先比较
Spec.Priority,rv - lv的差值为负说明 lv 的 priority 更大、排前; - priority 相同时,委托
compareShareWithDeserved做原有的 share 比较(share 由 deserved 与 inqueue 申请量共同决定),保持了与旧行为完全兼容:未配置 priority 的集群,所有队列 priority 均为 0,排序仍按 share 进行。
在启用**分层队列(hierarchical queue)**的构建路径中,排序函数还叠加了“叶子队列优先于其父队列参与比较”以及“同层比较共享祖先 share”的逻辑(见 capacity.go),但第一判定条件依然是Spec.Priority,即队列优先级始终拥有最高话语权。
proportion 插件的 QueueOrderFn
proportion 插件采用相同的实现范式(见 proportion.go):
ssn.AddQueueOrderFn(pp.Name(), func(l, r interface{}) int { lv := l.(*api.QueueInfo) rv := r.(*api.QueueInfo) if lv.Queue.Spec.Priority != rv.Queue.Spec.Priority { // return negative means high priority return int(rv.Queue.Spec.Priority) - int(lv.Queue.Spec.Priority) } if pp.queueOpts[lv.UID].share == pp.queueOpts[rv.UID].share { return 0 } if pp.queueOpts[lv.UID].share < pp.queueOpts[rv.UID].share { return -1 } return 1 })即 proportion 插件中:priority 不同按 priority 排,priority 相同按 share 排(share 小者在前)。两个插件的实现完全对称,保证了无论集群使用 capacity 还是 proportion 做配额隔离,队列优先级的语义一致。
排序结果如何被消费
各调度 action(allocate、concurrent-gang、enqueue、reclaim、preempt 等)在构建“待处理队列”的 PriorityQueue 时,统一使用ssn.QueueOrderFn作为比较函数。以 allocate 为例,actions 目录 下各 action 通过util.NewPriorityQueue(ssn.QueueOrderFn)建队;框架层的PriorityQueue实现见 priority_queue.go。这意味着:只要 QueueOrderFn 前置了 priority 比较,所有按队列顺序遍历的调度动作都自动获得了“高优先级队列先服务”的行为。
回收(Reclaim):跨队列资源回收的优先级语义
设计文档 Reclaim 部分给出三条规则:
- 队列回收按
QueueOrderFn保证的顺序,从最高优先级队列到最低优先级队列有序处理; - 构建 victim PriorityQueue 时,对于 job 不同的两个 victim,队列优先级较低的一方应被选为 victim;
- 若某队列已回收完所有低优先级、可回收的资源仍不满足自身请求,可以继续从更高优先级的队列回收资源。
reclaim action 的实现
reclaim.go 中:
queues := util.NewPriorityQueue(ssn.QueueOrderFn) ... preemptorsMap[job.Queue] = util.NewPriorityQueue(ssn.JobOrderFn) ... victimsQueue := ssn.BuildVictimsPriorityQueue(victims, task)- preemptor 侧的队列遍历直接基于
ssn.QueueOrderFn,因此高优先级队列的“回收者”任务先被处理——对应规则 1; - victim 侧通过
ssn.BuildVictimsPriorityQueue构建受害者优先级队列。该函数定义在 session_plugins.go,其比较逻辑是:先比 job 优先级,再调用各插件注册的VictimQueueOrderFn(ssn.VictimQueueOrderFn),最后回退到TaskOrderFn,从而把“低队列优先级优先成为 victim”的语义注入 victim 排序——对应规则 2; - 规则 3(穷尽低优先级资源后允许向高优先级队列回收)由 reclaim 的可回收性判定(
ReclaimableFn)驱动:当低优先级队列无可回收资源时,高优先级队列仍会继续评估其他队列的可回收任务,从而在资源仍然不足时扩大回收范围。
测试用例印证
单测直接验证了上述行为。reclaim_test.go 中有一个名为 “sort reclaimees when reclaiming from overusing queues with different queue priority” 的用例:构造了两个带不同 priority 的队列(借助 test_utils.go 中的BuildQueueWithPriorityAndResourcesQuantity辅助函数设置queue.Spec.Priority),断言最终被驱逐的是低队列优先级队列中可抢占的 Pod(ExpectEvicted: []string{"c1/preemptee1-1"})。这与设计文档第 2 条规则一一对应。
此外,victim 队列排序的框架级行为还有专门的测试 session_plugins_victim_order_test.go,验证了BuildVictimsPriorityQueue在 job 级比较打平后继续用 task 级比较函数打破平局。
与既有机制的关系与适用前提
结合源码与设计文档,可以归纳出该特性的完整语义边界:
| 维度 | 无 priority(或相同值) | 配置 priority 后 |
|---|---|---|
| 队列调度顺序 | share 小的队列在前 | priority 大的队列在前;相同再比 share |
| 回收处理顺序 | share 顺序 | 高 priority 队列的回收者先处理 |
| victim 选择 | 按 job 优先级、task 顺序等 | 低队列 priority 的 victim 优先被选 |
| 资源不足兜底 | — | 低优先级可回收资源用尽后,允许向高优先级队列回收 |
需要注意的适用前提与限制:
- 仅在实现了 QueueOrderFn 的插件中生效。当前仓库中注册
AddQueueOrderFn的插件包括 capacity 与 proportion(capacity.go)、drf.go 等;DRF 类插件的排序语义仍以文档约定为准; - overused 队列不参与该排序带来的好处:设计文档明确“假设 overused 队列不进入调度流程”。从源码结构看,capacity/proportion 对超额队列的限制逻辑先于队列排序生效,priority 并不能让一个已超出 capability 的队列“插队”;
- 兼容性无副作用:由于零值为 0 且仅在值不同时改变比较结果,存量集群升级后行为不变,priority 是纯增量能力;
- 与 preempt 的边界:队列优先级影响 reclaim 的 victim 选择顺序,但不改变 preempt action 自身基于 job priority / priorityClass 的抢占判定(参见 preempt.go 同样使用
QueueOrderFn遍历队列)。
总结
Volcano 的队列优先级特性以极小的 API 增量(QueueSpec 中一个int32字段)换取了显著的可运维性提升:
- API 层:
spec.priority(0 ~ int32 最大值,最小值由 CRD 校验保证),默认 0; - 调度层:capacity 与 proportion 插件的
QueueOrderFn统一实现“先比 priority、再比 share”的排序,且该函数被 allocate、reclaim 等所有按队列遍历的 action 复用; - 回收层:reclaim 从低到高有序处理、victim 优先取低优先级队列、资源仍不足时可向上回收,三条规则均有源码与单测支撑。
对于需要在同一集群上区分在线保障队列与离线弹性队列、或为不同租户设定明确资源获取顺序的场景,这是比调节 weight/deserved 更直接、更稳定的控制手段。相关源码入口可按 API 定义、capacity 插件、proportion 插件、reclaim action 与 victim 排序测试 继续深入阅读。
【免费下载链接】volcanoA Cloud Native Batch System (Project under CNCF)项目地址: https://gitcode.com/GitHub_Trending/vol/volcano
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考