Kueue 批作业配额、排队与准入
周一早上,训练平台一次提交了几十个 GPU 作业。它们都能通过 Kubernetes API 校验,也都带着正确的 nodeSelector 和 GPU request,却把在线推理预留的设备迅速吃完。平台组临时把 Job 全部暂停,再按聊天记录手工决定谁先恢复;几个小时后,大家已经说不清某个作业为何获得容量、借了哪个团队的额度,又是谁允许高优先级任务中断低优先级训练。节点调度没有出错,出错的是“作业何时有资格进入调度”这一层根本没有可审计模型。
另一个集群恰好相反。大量 Job 长期保持 suspend: true,值班同学看到 Pod 数量为零,以为调度器失灵,于是扩了节点。新增节点空闲计费,Job 仍未启动,因为它们还没有通过队列配额准入。Kueue 解决的是这道准入门:先把作业的 PodSet 与 requests 变成 Workload,在 ClusterQueue 中完成配额、Flavor、借用和抢占判断,准入后才解除作业挂起。它不替代 kube-scheduler,也不负责把 Pod 绑定到某台 Node。
先分清配额准入与 Pod 调度
Kubernetes 原生调度器面对的是已经创建出来的 Pod。它逐个执行 Filter、Score、Reserve、Permit 和 Bind 等阶段,回答“这个 Pod 放在哪台节点”。这种模型适合在线服务,也能运行普通 Job,但它不会先替一个多 Pod 作业锁定组织级额度,更不知道两个 namespace 是否属于同一租户预算。
Kueue 位于 Job 提交与 Pod 创建之间,并通过对应的 Job framework integration 与原有 Job 控制器协作。受支持的 Job 被提交到 LocalQueue 后保持挂起,Kueue 集成控制器为它生成 Workload。Kueue 根据 Workload 的 PodSet 数量与每个 Pod 的 requests 计算总需求,在 ClusterQueue 里尝试 ResourceFlavor,必要时使用 Cohort 的空闲名义配额或触发已配置的抢占。成功后,Workload.status.admission 记录 ClusterQueue、PodSet、Flavor 与资源用量,Admitted=True 表示准入条件完成;Job 随后解除挂起,才会由 Job 控制器创建 Pod 并进入真正的调度链。
因此排障时有两条不能混用的证据。Job 仍是 suspended、Workload 没有 QuotaReserved 或 Admitted,应查队列、配额、Flavor、AdmissionCheck 与抢占;Job 已解除挂起而 Pod Pending,则应查 scheduler Event、节点标签、污点、拓扑、存储和节点供给。Kueue 的架构概览明确把 queueing 与 Pod-to-node scheduling 分开,这也是后续所有观测的第一条分界线。
从四类对象理解状态保存在哪里
LocalQueue 是 namespace 级入口,用户只需知道本租户的队列名;它通过 spec.clusterQueue 指向集群级容量池。ClusterQueue 是管理员对象,保存 namespace 选择器、队列策略、ResourceGroup、Flavor 顺序、各资源的 nominalQuota、借用上限和抢占策略。这样,应用团队可以提交作业,却不能自行扩大组织总额度。
ResourceFlavor 描述一种可计量容量,例如通用 CPU、按需 GPU 或 Spot GPU。它可以关联节点标签、污点与自动注入的容忍,从而让配额类别与实际硬件类别对齐。Flavor 不是节点库存:即使队列里还有 GPU 配额,节点池也可能尚未供给设备;反过来,节点空闲也不等于租户拥有使用额度。官方的 ResourceFlavor 说明给出了标签、污点和容忍的具体语义。
Workload 是准入账本。对 Job 集成而言,用户通常不直接创建它;控制器根据 Job 模板生成 PodSet,并用 owner reference 维持生命周期。排队状态、准入分配、重排队次数、驱逐原因和完成状态都能在它的 status.conditions 与 status.admission 中取证。删除或手改这个派生对象会破坏控制器约定,日常变更应回到 Job、LocalQueue 或 ClusterQueue 的所有者。
在可销毁集群安装并验证控制器
下面以 Kueue v0.18.3 发行版本为基线,该版本的核心 API 为 kueue.x-k8s.io/v1beta2。项目仍处于 0.x 发行线,升级不能只看控制器镜像,还要核对 CRD 转换、受支持框架、目标 Kubernetes 版本和 feature gate;跨 minor 升级还应逐个阅读经过版本的 .0 发行说明。实验需要 Kubernetes 集群、kubectl、安装 CRD、ClusterRole、Webhook 与 controller manager 的集群管理员权限;业务使用者只需要目标 namespace 中创建 Job 和读取 LocalQueue/Workload 的权限。
先在隔离集群固定版本安装:
kubectl apply --server-side -f https://github.com/kubernetes-sigs/kueue/releases/download/v0.18.3/manifests.yaml
kubectl -n kueue-system rollout status deployment/kueue-controller-manager --timeout=180s
kubectl get crd | grep kueue.x-k8s.io
kubectl api-resources --api-group=kueue.x-k8s.io预期 controller manager 完成 rollout,API 资源中至少出现 clusterqueues、localqueues、resourceflavors 与 workloads。如果 Pod Running 但创建对象报 webhook 超时,应继续检查 Service endpoints、证书 Secret、API Server 到 webhook 的网络和 MutatingWebhookConfiguration,而不是关闭 webhook 绕过校验。安装方式与 feature gate 应以对应发行线的官方安装页为准。
建立最小配额池并看懂字段影响
实验建立一个空 Flavor、一个 4 CPU/8Gi 的 ClusterQueue,以及 namespace 内的 LocalQueue。空 Flavor 适合不区分节点类别的同构实验;生产若有 GPU 型号、Spot 或隔离节点,应拆成独立 Flavor,并确保标签与真实节点供应一致。
apiVersion: v1
kind: Namespace
metadata:
name: kueue-lab
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
name: lab-default
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: lab-cq
spec:
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kueue-lab
queueingStrategy: BestEffortFIFO
resourceGroups:
- coveredResources: ["cpu", "memory"]
flavors:
- name: lab-default
resources:
- name: cpu
nominalQuota: "4"
- name: memory
nominalQuota: 8Gi
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
name: lab-queue
namespace: kueue-lab
spec:
clusterQueue: lab-cq保存为 /tmp/kueue-queues.yaml 后执行 kubectl apply -f /tmp/kueue-queues.yaml。接着运行:
kubectl get clusterqueue lab-cq -o wide
kubectl -n kueue-lab get localqueue lab-queue -o wide
kubectl get clusterqueue lab-cq -o jsonpath='{range .status.conditions[*]}{.type}{"="}{.status}{" reason="}{.reason}{"\n"}{end}'两个队列应进入 Active/Ready 类状态;若 ClusterQueue 不活跃,先检查 Flavor 是否存在、namespace selector 是否允许目标 namespace、coveredResources 是否与 quota 条目一致。nominalQuota 是该队列在此 Flavor 上的名义额度,不是节点预留;borrowingLimit 只有加入 Cohort 且其他成员存在未用额度时才有意义。ClusterQueue 的官方字段说明详细解释了 Flavor 搜索、借用和抢占的计算条件。
正向实验:观察作业从挂起到准入
提交一个由两个 Pod 组成、总 request 为 1 CPU/256Mi 的 Job。队列标签把它送入 lab-queue;不要手工把 suspend 改成 false,解除挂起应由准入控制链完成。
apiVersion: batch/v1
kind: Job
metadata:
name: admitted-job
namespace: kueue-lab
labels:
kueue.x-k8s.io/queue-name: lab-queue
spec:
parallelism: 2
completions: 2
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: registry.k8s.io/busybox:1.36.1
command: ["sh", "-c", "echo admitted; sleep 30"]
resources:
requests:
cpu: 500m
memory: 128Mi执行 kubectl apply -f /tmp/admitted-job.yaml 后连续观察,而不是只看最终 Completed:
kubectl -n kueue-lab get job admitted-job -w
kubectl -n kueue-lab get workloads -o wide
JOB_UID=$(kubectl -n kueue-lab get job admitted-job -o jsonpath='{.metadata.uid}')
kubectl -n kueue-lab get workload -l "kueue.x-k8s.io/job-uid=$JOB_UID" -o yaml
kubectl -n kueue-lab get pods -o wide在短暂的中间状态里,Job 会保持挂起,Workload 先出现;额度可用后,Workload 的 status.admission.clusterQueue 指向 lab-cq,PodSet assignment 记录 lab-default 与计入的 CPU、内存,conditions 出现 QuotaReserved=True 和 Admitted=True。随后 Job 的 spec.suspend 变为 false,Pod 才被创建并由默认调度器绑定。最后日志出现 admitted,Job 完成。这个顺序证明的是“获得配额后允许执行”,并不保证 Pod 一定能调度成功或业务结果正确。
反向实验:让总需求稳定超过额度
复制前一个 Job,改名为 over-quota-job,把 parallelism 与 completions 保持为 2,并把每个 Pod 的 CPU request 改为 3。Workload 总需求变成 6 CPU,超过 ClusterQueue 的 4 CPU 名义额度;队列没有 Cohort,因而没有可借额度。
kubectl apply -f /tmp/over-quota-job.yaml
kubectl -n kueue-lab get job over-quota-job -o jsonpath='suspend={.spec.suspend}{"\n"}'
kubectl -n kueue-lab get workloads
JOB_UID=$(kubectl -n kueue-lab get job over-quota-job -o jsonpath='{.metadata.uid}')
kubectl -n kueue-lab describe workload -l "kueue.x-k8s.io/job-uid=$JOB_UID"
kubectl -n kueue-lab get pods -l job-name=over-quota-job预期 Job 保持 suspend=true,Workload 处于 pending,事件或 condition message 指向额度不足,且没有该 Job 的 Pod。若出现 Pending Pod,先确认 Job 是否真的由 Kueue 管理、队列标签是否正确、对应 framework integration 是否启用;这时扩节点不能改变准入结果。恢复实验有两条合法路径:降低 Job requests/并行度,或由容量管理员提高经预算审核的 quota。直接手工解除 suspend 会绕过队列治理,应由准入策略禁止并审计。
Cohort、借用与抢占为何需要成对治理
在 v1beta2 中,管理员要先创建集群级 Cohort 对象,再让多个 ClusterQueue 通过同一个 spec.cohortName 引用它;只写一个相同字符串而没有 Cohort 对象,不应视为完成了共享池配置。对某个 Flavor/resource,Workload 先尝试本队列剩余 nominalQuota;不足时,只有 Cohort 树内可借额度足够,并且借入量没有超过本队列的 borrowingLimit,借用才成立。borrowingLimit 为空时不代表禁止借用,而是受 Cohort 实际可用额度约束。借用提高利用率,却把“空闲容量可能按策略被名义额度所有者收回”的风险带给长任务。
抢占不是借用的自动同义词。withinClusterQueue 控制同队列低优先级 Workload 能否被替换;reclaimWithinCohort 控制名义额度所有者能否从其他正在借用的队列回收;borrowWithinCohort 决定借用中的队列是否还能通过抢占扩张。默认保守策略能避免无意中断。启用前应验证 checkpoint、重试幂等和中断成本,并用低优先级、可恢复作业做演练。Kueue 的抢占文档说明了候选者和优先级条件。
FlavorFungibility 又增加一层选择:当前 Flavor 需要借用或抢占时,是立即接受,还是继续寻找下一个无需中断的 Flavor。Flavor 顺序因此会改变成本和中断率,而不只是展示顺序。GPU 型号不可互换、镜像依赖特定驱动时,不能靠 Flavor 搜索假装兼容;作业模板仍要表达硬件约束,并在准入后验证真实设备与驱动。
把故障证据按控制层分型
第一类是入口故障:LocalQueue 不存在、namespace 不被 ClusterQueue selector 接纳,或 Job 没有正确队列标签。证据集中在 Job/Workload 事件和 Kueue controller 日志。第二类是额度故障:Workload 已生成但未 QuotaReserved,查看 PodSet 总 requests、Flavor 次序、ClusterQueue usage、cohort 借用空间和抢占策略。
第三类是外部准入检查未完成。AdmissionCheck 可把节点供给、MultiKueue 或自定义审批加入准入条件;此时可能已经预留 quota,但仍没有 Admitted=True。要查看每个 check 的状态和消息,避免把“配额已留住”误判为“作业已运行”。检查控制器失联还会造成额度长时间占用,需要超时、重试和释放责任。
第四类才是调度与运行故障:Workload 已 Admitted、Job 已解除挂起,却出现 FailedScheduling、镜像拉取失败、PVC Pending 或应用退出。此时应执行 kubectl describe pod、读取 scheduler Event 与容器日志。Kueue controller 日志可以证明准入决策,却不能解释 kube-scheduler 的节点过滤结果。
权限、凭证与敏感容量事实
ClusterQueue、ResourceFlavor、Cohort 和 AdmissionCheck 是集群级治理对象,只应授予容量平台管理员;租户管理员管理自己 namespace 的 LocalQueue,开发者提交 Job 并读取自身 Workload。允许业务账号修改 ClusterQueue 等于允许它扩大额度、改变抢占或把作业导向敏感节点。RBAC 还应限制 jobs/status、workloads/status,避免用户伪造控制器状态。
Kueue 本身通常不需要云凭证,但 AdmissionCheck、MultiKueue 或节点供给集成可能持有远端集群、云 API 或队列凭证。凭证应由 workload identity 或受控 Secret 注入,限制 namespace 与 audience,禁止写入 Job annotation、Event、Git 和海报示例。队列名称、GPU 型号、额度、借用率、等待年龄和优先级也可能暴露业务规模,应对指标标签和只读权限做租户隔离。
容量与成本要同时观察等待和浪费
只追求 ClusterQueue 利用率会把所有队列塞满,失去故障、发布和高优先级任务所需余量;只追求零等待又会让昂贵节点长期空闲。容量评审至少关联四组趋势:pending Workload 数与等待年龄、admitted/evicted 速率、各 Flavor 的 nominal/borrowed usage、准入后从 Pod 创建到 Ready 的时间。具体指标名会随发行线变化,应从 controller 的 /metrics 暴露清单和对应版本文档确认,避免监控规则静默失效。
额度建模使用 requests,而不是历史平均 usage。短任务可通过提高并发改善吞吐,但会放大镜像拉取、存储 IOPS 和 API Server 扇出;长训练借用 Spot GPU 看似便宜,若不能 checkpoint,抢占重算可能更贵。生产阈值应由作业到达率、运行时长分布、恢复点成本、节点启动时间和业务 SLO 推导,示例中的 4 CPU 只是实验值。
什么时候选 Kueue,什么时候不要选
当团队已经使用 Kubernetes Job、Kubeflow、Ray、MPI 等受支持框架,希望在不替换底层调度器的前提下增加配额准入、多租户队列、Flavor 与 Cohort 治理时,Kueue 很合适。它保留工作负载原生 API,并把“先等额度、后创建 Pod”做成独立控制层。
如果核心诉求是 Gang Scheduling、DRF、拓扑感知或自定义插件直接决定 Pod 绑定,应该评估 Volcano 等批调度器;如果只是单 namespace 并发限制,原生 ResourceQuota、Job 并行度和简单控制器可能更低成本。Kueue 与 Volcano 可以集成,但责任仍要拆开:Kueue 先决定作业是否获得额度,Volcano 在作业解除挂起后决定其 Pod 如何整组调度。两个系统都配置队列、配额和抢占而没有明确主从,会形成双重等待或双重中断。
升级、回滚与退出要先释放控制权
升级前导出 CRD、KueueConfiguration、ClusterQueue、LocalQueue、ResourceFlavor、AdmissionCheck 和未完成 Workload,核对目标版本 API、conversion webhook、framework 支持与 feature gate。先在隔离集群回放“准入、超额等待、借用、抢占、完成后释放 quota”五条路径,再灰度控制器。只升级 Deployment、不升级 CRD,或先删旧 CRD 再装新 CRD,都会把存量对象和状态转换置于风险中。
控制器回滚只在旧版本仍能读取当前 CRD 与对象字段时安全。退出 Kueue 时,应先冻结新提交,把 ClusterQueue stopPolicy 设为保持新准入的模式或在入口停止流量,等待或有计划地终止已准入作业;随后把业务模板切换到明确的替代准入方式,确认不再依赖 Kueue 自动 suspend/unsuspend,再删除集成资源。直接卸载 controller 可能留下 suspended Job、finalizer、webhook 失败和无人释放的治理状态。
实验环境的清理顺序是先删 Job,等待 Workload 与 quota usage 消失,再删 LocalQueue、ClusterQueue、ResourceFlavor 和 namespace;最后才卸载控制器:
kubectl -n kueue-lab delete job admitted-job over-quota-job --ignore-not-found
kubectl -n kueue-lab wait --for=delete workload --all --timeout=120s
kubectl -n kueue-lab delete localqueue lab-queue --ignore-not-found
kubectl delete clusterqueue lab-cq --ignore-not-found
kubectl delete resourceflavor lab-default --ignore-not-found
kubectl delete namespace kueue-lab --ignore-not-found
kubectl delete -f https://github.com/kubernetes-sigs/kueue/releases/download/v0.18.3/manifests.yaml --ignore-not-found删除前记录仍在运行、借用或等待的 Workload,确认没有业务对象依赖 webhook,并核销 controller Pod、Service、证书 Secret、ClusterRoleBinding 和监控抓取。长期责任也应明确:平台组维护 CRD、控制器、Flavor 与观测,容量委员会批准 nominal quota、借用和抢占,业务组维护 requests、优先级、checkpoint 与重试幂等,安全团队审计集群级写权限和远端凭证。只有这些责任能沿 Workload 状态被追溯,队列才不是另一份无人负责的 YAML。
