队列公平共享:配额、借用、抢占与饥饿防护
训练平台在夜间空闲时允许推荐团队借走推理团队的 GPU 配额。起初吞吐显著提高,直到推理团队提交紧急评测:队列显示资源总量足够,作业却一直等待;管理员临时提高 Pod 的 PriorityClass,结果在线推理 Pod 被 kube-scheduler 抢占,批作业仍没有拿回 Kueue 配额。两个优先级系统作用在不同阶段,单纯把数字调高只扩大了故障半径。
另一个集群把“先提交先运行”当成公平。一个租户持续提交许多小作业,另一个租户每天只提交一个大作业;小作业不断填满刚释放的碎片,大作业的等待年龄持续增长。平台总利用率很好看,租户体验却越来越差。真正需要治理的不是某一刻谁占了多少 CPU,而是名义权利、闲置借用、归还条件、历史消费、抢占代价和等待上限能否同时解释。
先分清队列准入和 Pod 调度
队列控制回答“这批作业现在是否有权消耗一份容量”,Pod 调度回答“已经获准的 Pod 能放到哪台节点”。Kueue 在作业创建后构造 Workload,计算整批 Pod 的资源请求;只有找到匹配的 ResourceFlavor 与可用配额,才写入 admission,并让集成的 Job 解除挂起。随后 kube-scheduler 才根据节点剩余 requests、亲和性、污点、拓扑和卷约束完成绑定。
因此,Kueue 的 WorkloadPriorityClass 或由 Job 传入的优先级用于队列排序和 Workload 抢占;Kubernetes PriorityClass 还可能影响 Pod 调度与节点级抢占。两个层次可以使用同一业务分级,但必须分别观察。Admitted=True 且 Pod Pending,通常是节点放置问题;QuotaReserved=False 或 Workload 仍排队,则节点再空闲也不会启动。Kueue 的 核心概念把 LocalQueue、ClusterQueue、Cohort、ResourceFlavor 与 Workload 的职责拆得很清楚。
LocalQueue 位于 namespace 内,是业务提交入口;ClusterQueue 是集群级容量与策略池;Cohort 让多个 ClusterQueue 共享闲置配额。nominalQuota 表示租户在竞争时应能取回的基准权利,不是静态硬上限。允许借用后,空闲权利可以暂时流向其他队列;borrowingLimit 限制最多借多少,lendingLimit 保留不外借的部分。没有归还路径的“借用”实际上是永久超卖。
固定 Kueue 发行线并启用公平策略
下面使用 Kueue 0.18.3,其安装入口要求 Kubernetes 1.29+,核心 API 使用 kueue.x-k8s.io/v1beta2,管理器配置使用 config.kueue.x-k8s.io/v1beta2。Kueue 仍处于 0.x 发行线,升级时必须把 CRD、控制器和对象迁移看成一组变更。正式安装前需要一个可销毁集群、Helm、kubectl,以及能创建 CRD、ClusterRole、Webhook、ClusterQueue 和 Cohort 的平台身份。
Helm 安装固定 OCI Chart 版本,安装后同时检查 Deployment、CRD 与 webhook,不把 Pod Running 当作控制器可用:
helm upgrade --install kueue oci://registry.k8s.io/kueue/charts/kueue \
--version 0.18.3 \
--namespace kueue-system --create-namespace \
--wait --timeout 300s
kubectl wait deploy/kueue-controller-manager \
-n kueue-system --for=condition=available --timeout=5m
kubectl api-resources | grep kueue.x-k8s.io
kubectl get validatingwebhookconfiguration,mutatingwebhookconfiguration | grep kueue
kubectl -n kueue-system logs deploy/kueue-controller-manager --tail=80预期能发现 resourceflavors、clusterqueues、localqueues、cohorts 和 workloads,控制器日志没有持续的证书、leader election 或 API discovery 错误。官方 安装文档同时提供固定 release manifest;选择 Helm 后,应由 Helm 持续拥有 CRD 之外的安装对象,不能再混用远程 manifest 写同一组资源。
公平共享通过管理器配置开启。先导出当前 ConfigMap,再修改 fairSharing.preemptionStrategies;不同安装方式的键名和现有配置可能不同,必须在原对象上合并,而不是用一段不完整 ConfigMap 覆盖健康、证书和 integration 设置:
kubectl -n kueue-system get configmap kueue-manager-config -o yaml \
> kueue-manager-config.before.yaml
kubectl -n kueue-system edit configmap kueue-manager-config
kubectl -n kueue-system rollout restart deploy/kueue-controller-manager
kubectl -n kueue-system rollout status deploy/kueue-controller-manager写入配置数据中的关键片段如下:
apiVersion: config.kueue.x-k8s.io/v1beta2
kind: Configuration
fairSharing:
preemptionStrategies:
- LessThanOrEqualToFinalShare
- LessThanInitialShareKueue 抢占说明解释了两种策略对目标和发起方 share 的约束。重启后要在日志中确认配置解析成功,并保存 ConfigMap 的 resourceVersion;字段拼错导致管理器 CrashLoop 时,立即恢复备份,而不是删除 webhook 绕过校验。
把名义权利、借用上限和权重写进对象
这个隔离实验创建两个 namespace、一个通用 ResourceFlavor、一个 Cohort 和两条 ClusterQueue。每条队列有 2 CPU 名义配额,最多再借 2 CPU;权重 1 表示双方竞争闲置资源时按相同地位计算 share。演示值只用于观察状态迁移,生产值必须来自作业画像、等待 SLO、节点故障预算和预算审批。
apiVersion: v1
kind: Namespace
metadata:
name: fair-a
---
apiVersion: v1
kind: Namespace
metadata:
name: fair-b
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
name: on-demand
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: Cohort
metadata:
name: shared-batch
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: team-a
spec:
cohortName: shared-batch
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: fair-a
queueingStrategy: BestEffortFIFO
fairSharing:
weight: "1"
preemption:
withinClusterQueue: LowerPriority
reclaimWithinCohort: LowerPriority
resourceGroups:
- coveredResources: [cpu, memory]
flavors:
- name: on-demand
resources:
- name: cpu
nominalQuota: "2"
borrowingLimit: "2"
- name: memory
nominalQuota: 4Gi
borrowingLimit: 4Gi
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: team-b
spec:
cohortName: shared-batch
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: fair-b
queueingStrategy: BestEffortFIFO
fairSharing:
weight: "1"
preemption:
withinClusterQueue: LowerPriority
reclaimWithinCohort: LowerPriority
resourceGroups:
- coveredResources: [cpu, memory]
flavors:
- name: on-demand
resources:
- name: cpu
nominalQuota: "2"
borrowingLimit: "2"
- name: memory
nominalQuota: 4Gi
borrowingLimit: 4Gi
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
name: batch
namespace: fair-a
spec:
clusterQueue: team-a
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
name: batch
namespace: fair-b
spec:
clusterQueue: team-b保存为 fair-queues.yaml 后执行 kubectl apply -f fair-queues.yaml。预期两条 ClusterQueue 的 Active=True,LocalQueue 能解析到目标队列。若 Active=False,先看 status conditions 和 Event;ResourceFlavor 不存在、资源组覆盖不一致、namespaceSelector 不匹配,都应在提交作业前修复。
正向实验:先借用,再让权利持有者取回容量
实验镜像只执行一段可终止的休眠,不访问业务数据。先让 A 提交一个总请求 3 CPU 的低优先级 Job,它会使用 2 CPU 名义配额并借用 1 CPU:
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch-low
value: 100
globalDefault: false
description: "可中断批作业"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
name: batch-high
value: 1000
globalDefault: false
description: "有等待预算的批作业"
---
apiVersion: batch/v1
kind: Job
metadata:
name: borrower-a
namespace: fair-a
labels:
kueue.x-k8s.io/queue-name: batch
spec:
parallelism: 6
completions: 6
template:
spec:
priorityClassName: batch-low
restartPolicy: Never
containers:
- name: task
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: 500m
memory: 128Mikubectl apply -f borrower-a.yaml
kubectl -n fair-a get job,pod,workload -w
kubectl get clusterqueue team-a -o yaml
kubectl get clusterqueue team-b -o yaml可接受的证据不是“Pod 最终 Running”一句话,而是 A 的 Workload 先获得 QuotaReserved=True、Admitted=True,admission 中记录 on-demand flavor,ClusterQueue 使用量超过自身 nominal quota,同时 weighted share 从零上升。实际 Pod 是否 Running 还取决于集群至少有 3 CPU 与相应内存可调度;物理容量不足时,准入成功和调度失败必须分别记录。
随后在 B 提交总请求 3 CPU 的高优先级 Job。B 的前 2 CPU 使用自己的 nominal quota,不应驱逐 A;额外的 1 CPU 需要取回被 A 借走的部分,因而进入 cohort reclaim:
apiVersion: batch/v1
kind: Job
metadata:
name: claimant-b
namespace: fair-b
labels:
kueue.x-k8s.io/queue-name: batch
spec:
parallelism: 6
completions: 6
template:
spec:
priorityClassName: batch-high
restartPolicy: Never
containers:
- name: task
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: 500m
memory: 128Mi保存为 claimant-b.yaml,应用后沿 Workload condition、ClusterQueue share 和 Event 观察回收过程:
kubectl apply -f claimant-b.yaml
kubectl -n fair-b get workload -w
kubectl get clusterqueue team-a team-b \
-o custom-columns=NAME:.metadata.name,ACTIVE:.status.conditions[0].status,SHARE:.status.fairSharing.weightedShare
kubectl get events -A --sort-by=.metadata.creationTimestamp | tail -n 40
kubectl -n fair-a describe workload
kubectl -n fair-b describe workload预期先出现目标 Workload 的 preemption condition 或 Event,被抢占 Job 暂停并释放 admission,B 获得配额后才启动。抢占不是瞬时资源转移:控制器要撤销 admission,Job integration 要停止 Pod,终止宽限与外部清理完成后,配额才真正可复用。若任务不能检查点续跑,公平回收会把已消耗计算全部变成浪费。
字段怎样改变公平、吞吐和中断
nominalQuota 决定竞争时的基准权利。它过高会让低活跃租户长期保留不可借容量,过低则让核心租户频繁依赖回收。borrowingLimit 控制队列在其他租户空闲时的突发上限;无限借用可以提高短期利用率,但会放大归还时的抢占规模。lendingLimit 适合保留冷启动或紧急任务水位,代价是闲置时仍有容量不能共享。Kueue 的 ClusterQueue 说明给出了这些字段按 resource 与 flavor 生效的对象关系。
fairSharing.weight 不是 CPU 数量,而是计算 dominant resource share 时的相对权重。CPU 密集队列和 GPU 密集队列不能只比较一个总百分比;fair sharing 会看各资源超出 nominal quota 的相对份额,并用最大一项代表 dominant share。权重越大,同样借用量对应的 weighted share 越低,队列获得闲置容量的机会越高。把权重当业务优先级会导致永久倾斜,权重应表达经过批准的长期份额,紧急性由优先级与等待策略表达。
BestEffortFIFO 允许后来的小作业绕过暂时装不下的队首大作业,提高吞吐,却可能让大作业饥饿;StrictFIFO 保住队首顺序,却可能因一个大作业阻塞后续所有小作业。选择时要同时记录等待年龄分位数、绕行次数、资源碎片和完成吞吐,不能只看集群利用率。
反向实验:制造一个永远无法准入的大作业
稳定暴露配额模型错误的办法,是提交一个最小 flavor 需求就超过“自身 nominal quota + 可借上限”的 Workload。下面的 Job 请求 5 CPU,而 team-a 最多只能获得 4 CPU;即使节点有空闲,它也应保持挂起:
apiVersion: batch/v1
kind: Job
metadata:
name: impossible-a
namespace: fair-a
labels:
kueue.x-k8s.io/queue-name: batch
spec:
parallelism: 5
completions: 5
template:
spec:
restartPolicy: Never
containers:
- name: task
image: registry.k8s.io/pause:3.10
resources:
requests:
cpu: "1"
memory: 128Mikubectl apply -f impossible-a.yaml
kubectl -n fair-a get job impossible-a -o jsonpath='{.spec.suspend}{"\n"}'
kubectl -n fair-a get workload -o wide
kubectl -n fair-a describe workload
kubectl get clusterqueue team-a -o yaml预期 Job 继续 suspend: true,Workload 没有 admission,Event 或 condition 指向配额不足,而不是产生一批 Pending Pod。这个中间状态证明 Kueue 在 Pod 创建之前拦住了超预算需求。错误修复有三种不同含义:缩小并行度是在需求侧降级;提高 borrowingLimit 是扩大共享风险;提高 nominalQuota 是改变长期权利。值班人员不能为了消掉告警随意选择其中一种。
若反例竟然获准,检查对象是否真的进入预期 LocalQueue、ClusterQueue 的资源组是否覆盖 cpu、是否存在另一个默认队列,以及 admission 是否来自实验前遗留 Workload。不要直接删除 controller Pod重试;调和会依据持久化对象恢复同样结论。
用状态、事件、指标和日志还原一次等待
排队故障先按时间线分四层。第一层看 Job 是否被 integration 识别并生成 Workload;第二层看 Workload 的 QuotaReserved、Admitted、Evicted 等 condition;第三层看 ClusterQueue 的 active、pending、reserving、admitted 状态与 flavor 使用;第四层才看 Pod 调度事件和节点供给。任一层没有成立,后层证据不能倒推前层正常。
kubectl get localqueue -A
kubectl get clusterqueue,cohort,resourceflavor
kubectl get workload -A -o wide
kubectl -n fair-a get workload <workload-name> -o yaml
kubectl get events -A --field-selector reason=PendingWorkload
kubectl -n kueue-system logs deploy/kueue-controller-manager \
--since=20m | grep -E 'admit|preempt|evict|fair|quota'指标至少关联 kueue_pending_workloads、准入等待时间、kueue_cluster_queue_weighted_share、资源 reservation、preemption 与 eviction 计数。资源级 nominal、borrowing 和 lending 指标需要在管理器配置中启用 metrics.enableClusterQueueResources;开启后标签基数会随 queue、flavor、resource 增长。Kueue 的 指标参考给出了准确名称与标签。生产判据应绑定趋势:某租户持续有可准入需求时,等待年龄不能在其他租户持续借用期间单调增长;抢占率升高时,完成吞吐与重算成本必须同步解释。
日志里可能出现 namespace、队列名、作业名和资源画像,这些都是组织结构与容量敏感数据。集中采集时按租户限制查询权限,避免把完整 Workload spec、环境变量或 Secret 内容放进日志。指标接口使用只读 ServiceAccount;普通租户只需在自己的 namespace 创建 Job、读取 LocalQueue 和 Workload,不应拥有修改 ClusterQueue、Cohort、ResourceFlavor 或控制器配置的权限。
抢占不是免费的公平修复器
Kueue 经典抢占优先选择正在借用、优先级更低且较新准入的 Workload,再尝试缩小目标集合;公平共享抢占还比较 preemption 前后的 weighted share。算法能避免明显的份额反转,却不知道任务已经计算了多少、检查点是否完整、外部许可证是否已占用。一个只差一分钟完成的大作业可能比刚启动的小作业更值得保留,这类业务代价必须进入优先级或不可中断队列设计。
抢占策略应与任务恢复能力绑定。可检查点训练任务要验证 checkpoint 写入成功、恢复版本兼容和对象存储带宽;不可重入数据任务应拆分为幂等分片,或放进不参与跨队列抢占的 ClusterQueue;外部 GPU、许可证、临时卷和云实例要在 Workload 释放 admission 后继续核对是否归还。只看 Kueue 配额下降,会漏掉集群外持续计费。
优先级反转常发生在高优先级作业依赖低优先级准备任务时。抢占准备任务会让高优先级作业永远等数据。解决办法是让依赖链共享一致的业务优先级、把准备阶段纳入同一 Workload,或为基础数据生成保留独立配额,而不是继续抬高最终 Job 的数字。Kubernetes Pod Priority 与抢占还提醒管理员用 ResourceQuota 限制高优先级 Pod 的创建,防止租户绕过治理。
从等待 SLO 反推配额,而不是平均分机器
队列容量模型至少保留五个量:租户稳态并发、峰值并发、单作业资源向量、可中断比例、可接受等待时间。nominal quota 应覆盖承诺的稳态权利与恢复底线;可借池承载可延迟突发;高优先级保留水位承载必须在等待预算内启动的任务。GPU 还要按型号、显存、拓扑和许可证建 flavor,不能把不同加速器折算成一个虚假的“GPU 数”。
成本也不只等于节点小时。借用提高利用率,可能增加回收时的重算、镜像拉取、数据重读和检查点 I/O;lendingLimit 提高确定性,却让保留容量空闲;StrictFIFO 减少绕行,却可能降低碎片利用;频繁抢占会让队列指标很好看、业务完成率变差。评审时同时展示节点成本、借用量、等待年龄、抢占损失计算时、成功完成量和外部资源残留。
容量不足与策略不公平也要分开。所有队列都在 nominal quota 内等待,通常是实际节点供给、调度约束或 flavor 供应不足;只有部分租户在其他租户借用时长期等待,才是借用、权重或回收策略问题。先分类,才能决定是增加节点、调整 requests,还是修改队列合同。
选型时比较控制层,而不是产品名字
单 namespace 的开发团队只需要限制对象总量时,ResourceQuota 更简单;它在 API 准入阶段拒绝超额创建,不提供批作业挂起、跨租户借用或完成后再准入。需要 Kubernetes 原生 Job 排队、配额借用、多 flavor 和队列可见性时,Kueue 更合适。需要 gang scheduling、拓扑与批调度插件直接参与 Pod 放置时,Volcano 能接管指定工作负载的调度,但它改变的是更靠后的控制层。
Kueue 与 Volcano 可以组合:Kueue 决定 Workload 何时获准,Volcano 决定 PodGroup 怎样成组放置。组合会增加两套队列、优先级和事件语义,必须明确唯一的配额真相源,并验证 Job integration 是否支持。在线服务通常不应进入会被批作业公平抢占的队列;它们的副本、PDB、HPA 与节点保留容量有不同的恢复目标。
清理实验时先停止新准入
先删除实验 Job,让 integration 释放 Workload 和 admission,再删除 LocalQueue、ClusterQueue、Cohort 与 ResourceFlavor。直接先删 ClusterQueue 会留下无法解释的挂起对象;直接卸载控制器会让被挂起 Job 永远保持 suspend 状态。
kubectl -n fair-a delete job borrower-a impossible-a --ignore-not-found
kubectl -n fair-b delete job claimant-b --ignore-not-found
kubectl delete priorityclass batch-low batch-high --ignore-not-found
kubectl delete -f fair-queues.yaml --ignore-not-found
kubectl get workload -A
kubectl get clusterqueue,cohort,resourceflavor若实验修改了管理器配置,使用备份恢复 ConfigMap,再滚动重启并检查日志。完整退出 Kueue 前先冻结新提交,列出所有受管 Workload,决定恢复、完成还是删除;把已挂起 Job 恢复为可运行状态;确认没有 admission、finalizer 和 webhook 依赖后再执行 helm uninstall kueue -n kueue-system。CRD 最后删除,因为删除 CRD 会同时删除所有对应对象,且 Helm 卸载通常不会替团队完成这项不可逆决策。
升级与责任治理要保住权利语义
升级前导出 CRD、Configuration、Cohort、ClusterQueue、LocalQueue、ResourceFlavor、WorkloadPriorityClass 和关键指标基线。跨 minor 发行线逐段阅读 upgrade notes,先更新 CRD,再让候选控制器在隔离队列验证准入、借用、抢占和清理;不要让两个版本同时调和同一组 Workload。回退可执行的前提,是旧控制器仍能读取当前存储版本,且新字段没有造成不可逆对象转换。
平台团队拥有 Cohort、ClusterQueue、ResourceFlavor 和管理器配置;租户管理员拥有 namespace 内 LocalQueue 与队列映射;业务团队拥有 Job 的 requests、并行度、幂等和检查点;值班团队根据状态与事件执行冻结、恢复和升级。修改 nominal quota、weight、borrowingLimit、lendingLimit 或 preemption policy 都属于容量权利变更,应经过双人评审并留下前后对象与影响队列。
长期门禁不是“平均利用率达到某个数字”,而是每条承诺都可验证:有需求时 nominal quota 能被取回;闲置时借用提高完成量;归还不会让不可中断任务丢失;等待年龄不在持续竞争中无限增长;控制器重启后 admission 与配额状态可恢复;退出后 Job、CRD、webhook、指标抓取和外部计费对象全部有明确归属。做到这些,公平才从一句口号变成可以审计、演练和回滚的容量合同。
