Kueue 学习第二篇:搞懂配额借用、Workload 准入、优先级与抢占
前言
在上一篇文章中,我们介绍了 Kueue 的四个基础对象:
Job
↓
Workload
↓
LocalQueue
↓
ClusterQueue
↓
ResourceFlavor
其中:
-
Workload 是 Kueue 实际排队和准入的对象;
-
LocalQueue 是 Namespace 中的任务入口;
-
ClusterQueue 管理资源配额和排队策略;
-
ResourceFlavor 描述资源类型和节点约束。
但是,只理解这些基础对象还不够。
在实际的多租户 Kubernetes 集群中,还会遇到以下问题:
-
一个 ClusterQueue 的配额暂时用不完,能不能借给其他团队;
-
Workload 已经获得配额,为什么仍然没有运行;
-
高优先级任务能不能让低优先级任务释放配额;
-
多个团队长期竞争资源时,如何避免某个团队一直占用资源;
-
Workload 获得配额后,能不能继续执行其他容量或合规检查。
这些问题分别涉及:
-
Cohort;
-
borrowingLimit 和 lendingLimit;
-
Quota Reservation;
-
Admission;
-
WorkloadPriorityClass;
-
Preemption;
-
Fair Sharing;
-
AdmissionCheck。
本文继续以 Kueue v0.18.2 和 kueue.x-k8s.io/v1beta2 API 为基础,讲清楚一条 Workload 从排队、借用配额到最终准入的完整过程。
一、Cohort:多个 ClusterQueue 如何共享配额
1.1 为什么需要 Cohort
假设集群中有两个团队,每个团队都有自己的 ClusterQueue:
team-a-cq:
CPU nominalQuota = 8
team-b-cq:
CPU nominalQuota = 4
某个时刻:
team-a 只使用了 2 CPU
team-b 想要运行一个需要 6 CPU 的任务
team-b 自己的名义配额只有 4 CPU。如果两个 ClusterQueue 完全隔离,即使 team-a 还有 6 CPU 配额未使用,team-b 的任务仍然只能等待。这会造成资源利用率下降。
Kueue 通过 Cohort 将多个 ClusterQueue 组织到同一个配额共享范围中:
team-a-cq ─────┐
├── team-ab-cohort
team-b-cq ─────┘
加入同一个 Cohort 后,ClusterQueue 可以在限制范围内借用其他 ClusterQueue 当前未使用的名义配额。
1.2 Cohort 是什么资源
在 Kueue v0.18.2 的 v1beta2 API 中,Cohort 是一个独立的、集群级 CRD:
apiVersion: kueue.x-k8s.io/v1beta2
kind: Cohort
metadata:
name: team-ab-cohort
Cohort 不属于任何 Namespace。ClusterQueue 通过以下字段加入 Cohort:
spec:
cohortName: team-ab-cohort
完整关系为:
ClusterQueue.spec.cohortName
↓
Cohort
↓
同一 Cohort 中的 ClusterQueue 可以共享未使用配额
一个 ClusterQueue 只能加入一个直接 Cohort。Kueue 还支持通过 parentName 构建分层 CohortTree,但本文先以单层 Cohort 为主。
1.3 Cohort 共享的不是物理设备
Cohort 共享的是:
ResourceFlavor + Resource 对应的逻辑配额
例如:
default-flavor / cpu
a100-flavor / nvidia.com/gpu
t4-flavor / nvidia.com/gpu
Cohort 不直接共享:
-
物理 Node;
-
已经分配给 Pod 的 GPU;
-
某一张具体的显卡;
-
kube-scheduler 的 Node 绑定结果;
-
Device Plugin 已经完成的设备分配。
例如,team-b 从 Cohort 中借到了 1 个 nvidia.com/gpu 配额,只代表 Kueue 的逻辑账本允许其 Workload 申请这个资源。后续 Pod 能否真正运行,仍取决于:
-
节点是否存在可用 GPU;
-
ResourceFlavor 的节点约束是否匹配;
-
Pod Scheduler 是否能找到合适节点;
-
NVIDIA Device Plugin 是否正常;
-
其他 Pod 是否已经占用物理设备。
因此:
Cohort 解决的是逻辑配额共享问题,不是物理设备分配问题。
1.4 一个基础 Cohort 配置
下面创建一个 Cohort,并让两个 ClusterQueue 加入其中:
apiVersion: kueue.x-k8s.io/v1beta2
kind: Cohort
metadata:
name: team-ab-cohort
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: team-a-cq
spec:
cohortName: team-ab-cohort
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: team-a
resourceGroups:
- coveredResources:
- cpu
flavors:
- name: default-flavor
resources:
- name: cpu
nominalQuota: "8"
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: team-b-cq
spec:
cohortName: team-ab-cohort
namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: team-b
resourceGroups:
- coveredResources:
- cpu
flavors:
- name: default-flavor
resources:
- name: cpu
nominalQuota: "4"
逻辑上:
team-a 自有配额:8 CPU
team-b 自有配额:4 CPU
Cohort 中该资源维度的名义配额总量:
8 + 4 = 12 CPU
当 team-a 没有使用全部 8 CPU 时,team-b 可以尝试借用 team-a 当前未使用的部分。
但“Cohort 总配额是 12 CPU”并不代表任意一个 ClusterQueue 永远都可以使用 12 CPU。实际可用量还取决于:
-
其他 ClusterQueue 的当前使用量;
-
borrowingLimit;
-
lendingLimit;
-
是否存在等待使用自身 nominalQuota 的 Workload;
-
Preemption 和 Fair Sharing 配置。
二、borrowingLimit 与 lendingLimit
2.1 nominalQuota 是自己的基础配额
假设配置:
resources:
- name: cpu
nominalQuota: "4"
表示这个 ClusterQueue 在当前 ResourceFlavor 和 Resource 维度上拥有 4 CPU 名义配额。
可以把 nominalQuota 理解为:
当前 ClusterQueue 在配额账本中拥有的基础份额。
当 ClusterQueue 加入 Cohort 后,它可能借用其他 ClusterQueue 的未使用 nominalQuota,也可能把自己暂时未使用的 nominalQuota 借给其他 ClusterQueue。
2.2 borrowingLimit:最多允许借入多少
示例:
resources:
- name: cpu
nominalQuota: "4"
borrowingLimit: "2"
含义是:
自有 nominalQuota:4 CPU
最多借入:2 CPU
理论最大使用量:6 CPU
计算公式为:
理论最大配额
= nominalQuota + borrowingLimit
但能够使用到 6 CPU 的前提是,Cohort 中确实存在至少 2 CPU 的可借配额。
如果其他 ClusterQueue 已经使用了自己的全部 nominalQuota,即使当前 ClusterQueue 配置了:
borrowingLimit: "2"
也没有实际配额可以借入。borrowingLimit 省略或设置为 null 时,表示没有额外的借入上限,但仍然不能超过 Cohort 中真实存在的空闲配额。
2.3 lendingLimit:最多允许借出多少
示例:
resources:
- name: cpu
nominalQuota: "4"
lendingLimit: "1"
含义是:
自有 nominalQuota:4 CPU
最多允许借出:1 CPU
至少保留给自己:3 CPU
可以使用下面的公式理解:
保留给当前 ClusterQueue 的最小名义配额
= nominalQuota - lendingLimit
需要注意,这里的“保留”仍然是 Kueue 的逻辑配额保留,不是把物理 Node 上的 CPU 直接锁定。
如果 lendingLimit 省略或设置为 null,表示当前 ClusterQueue 的全部未使用 nominalQuota 都可以被同一 Cohort 中的其他 ClusterQueue 借用。
2.4 borrowingLimit 和 lendingLimit 的区别
可以直接记成:
| 字段 | 作用 |
|---|---|
| borrowingLimit | 当前 ClusterQueue 最多能从 Cohort 中借入多少 |
| lendingLimit | 当前 ClusterQueue 最多允许其他 ClusterQueue 借走多少 |
这两个字段都配置在具体的:
ResourceFlavor + Resource
维度下,而不是对整个 ClusterQueue 设置一个统一数字。例如:
resources:
- name: cpu
nominalQuota: "8"
borrowingLimit: "4"
lendingLimit: "2"
- name: memory
nominalQuota: 32Gi
borrowingLimit: 16Gi
lendingLimit: 8Gi
CPU 和内存分别拥有自己的借入、借出限制。
2.5 一个完整借用示例
假设:
team-a-cq:
nominalQuota = 4 CPU
lendingLimit = 2 CPU
team-b-cq:
nominalQuota = 2 CPU
borrowingLimit = 2 CPU
当前状态:
team-a 已使用 1 CPU
team-b 已使用 2 CPU
team-b 新任务需要 1 CPU
team-a 当前未使用的名义配额为:
4 - 1 = 3 CPU
但 team-a 的 lendingLimit 是 2 CPU,所以最多只能借出 2 CPU。
team-b borrowingLimit 是 2 CPU,因此 team-b 可以借入 1 CPU,满足新任务需求。
准入后:
team-b 总使用量:3 CPU
team-b 自有配额:2 CPU
team-b 借用配额:1 CPU
2.6 ClusterQueue 必须声明要借用的资源维度
假设 Cohort 中存在:
default-flavor / cpu
对应的公共或可借用配额。
某个 ClusterQueue 想借用该资源,仍然必须在自己的 resourceGroups 中声明:
resources:
- name: cpu
nominalQuota: "0"
即使自己的 nominalQuota 是 0,也必须定义这个 ResourceFlavor 和 Resource 组合。
否则,Kueue 不会认为该 ClusterQueue有权使用这个配额维度。
2.7 Cohort 自己也可以提供公共配额
除了共享各个 ClusterQueue 未使用的 nominalQuota,Cohort 自己也可以配置 resourceGroups:
apiVersion: kueue.x-k8s.io/v1beta2
kind: Cohort
metadata:
name: team-ab-cohort
spec:
resourceGroups:
- coveredResources:
- cpu
flavors:
- name: default-flavor
resources:
- name: cpu
nominalQuota: "8"
这里的 8 CPU 是 Cohort 自己提供的公共配额。假设:
team-a-cq nominalQuota:4 CPU
team-b-cq nominalQuota:4 CPU
Cohort nominalQuota:8 CPU
那么逻辑配额总量为:
4 + 4 + 8 = 16 CPU
其中 Cohort 的 8 CPU 是 ClusterQueue 自有配额之外的额外共享池,而不是对两个 ClusterQueue 配额的重复统计。
配置 Cohort 公共配额时,同样需要保证逻辑配额总量与集群实际可提供能力相匹配,否则 Workload 可能已经通过 Kueue 准入,但 Pod 在真实调度阶段仍然因为资源不足而 Pending。
2.8 借出的配额不会自动立即收回
一个常见误解是:
team-b 借用了 team-a 的配额
↓
team-a 一提交新任务
↓
team-b 的任务立即被终止
实际并不是这样。Cohort 只提供配额共享和借用能力,本身不会因为原配额所有者提交了新任务,就自动终止已经运行的 Workload。
如果没有配置合适的 Preemption:
-
team-a 的新 Workload 可能继续等待;
-
team-b 已经准入的 Workload 可以继续运行;
-
等 team-b 的任务自然结束后,配额才被释放。
如果希望 team-a 能够主动收回被借用的 nominalQuota,需要配置后文介绍的:
spec:
preemption:
reclaimWithinCohort: ...
三、Quota Reservation 与 Admission
3.1 两者不是同一个状态
Kueue 的准入过程不是简单的一次判断。官方将其拆成至少两个重要阶段:
Quota Reservation
↓
Admission
Quota Reservation 表示:
Kueue 已经为 Workload 找到了 ClusterQueue、ResourceFlavor 和足够的逻辑配额,并完成配额预留。
Admission 表示:
Workload 已经满足所有必要的准入条件,可以开始执行。
Kueue 的 Admission 流程会先检查逻辑配额并创建 Quota Reservation,然后等待全部 AdmissionCheck 通过;所有必要条件满足后,Workload 才会进入 Admitted 状态。
3.2 Quota Reservation 是什么
Quota Reservation 不是一个独立的 Kubernetes CRD,也不是用户创建的资源。
它是 Kueue Scheduler 为 Workload 执行的配额预留过程:
Workload 进入 LocalQueue
↓
LocalQueue 找到 ClusterQueue
↓
计算 Workload 总资源请求
↓
选择 ResourceFlavor
↓
检查 nominalQuota
↓
必要时检查 Cohort 可借配额
↓
必要时等待 Preemption 释放配额
↓
创建 Quota Reservation
Quota Reservation 成功后,Workload 中通常会出现:
QuotaReserved=True
并且:
status:
admission:
不再为空。
3.3 status.admission 中记录什么
下面是一个简化后的 Workload 状态:
status:
admission:
clusterQueue: ai-cluster-queue
podSetAssignments:
- name: main
count: 2
flavors:
cpu: default-flavor
memory: default-flavor
resourceUsage:
cpu: "4"
memory: 8Gi
这里记录了:
-
为 Workload 预留配额的 ClusterQueue;
-
每个 PodSet 选择的 ResourceFlavor;
-
准入计算时使用的 Pod 数量;
-
Workload 实际预留的资源总量。
需要注意:
status.admission不为空,主要代表已经形成配额分配和预留结果,并不一定代表 Workload 已经最终 Admitted。
v0.18.2 的 Workload API 将配额分配记录在 status.admission,并将 AdmissionCheck 状态记录在 status.admissionChecks。
3.4 Admission 是什么
当 Workload 满足全部准入条件后,Kueue 会设置:
Admitted=True
对于受 Kueue 管理的 Kubernetes Job,随后通常会发生:
Workload Admitted=True
↓
Kueue 解除 Job 的 suspend 状态
↓
Job Controller 创建 Pod
如果 ClusterQueue 没有配置 AdmissionCheck,那么:
QuotaReserved=True
↓
Admitted=True
这两个状态通常会很快连续出现。如果配置了 AdmissionCheck,则可能出现:
QuotaReserved=True
AdmissionCheck=Pending
Admitted=False
这表示:
-
逻辑配额已经预留;
-
但附加检查尚未完成;
-
Job 仍然不能开始运行。
3.5 QuotaReserved 和 Admitted 的区别
可以用下面的表格理解:
| 状态 | 表示什么 | Job 是否可以开始 |
|---|---|---|
| QuotaReserved=False | 尚未获得配额 | 不可以 |
| QuotaReserved=True,Admitted=False | 已获得配额,但还有其他条件未通过 | 不可以 |
| QuotaReserved=True,Admitted=True | 配额和附加检查均已通过 | 可以 |
| Evicted=True | 已被驱逐,需要释放配额并重新排队或停用 | 不可以 |
因此,当 Job 仍然处于暂停状态时,不能只看 QuotaReserved,还需要检查 Admitted。
四、WorkloadPriorityClass:Kueue 如何判断任务优先级
4.1 WorkloadPriorityClass 是什么
WorkloadPriorityClass 是独立的、集群级 Kueue CRD:
apiVersion: kueue.x-k8s.io/v1beta2
kind: WorkloadPriorityClass
metadata:
name: high-priority
value: 1000
description: "生产环境高优先级任务"
其中:
value 越大,Workload 优先级越高
WorkloadPriorityClass 可以被任意 Namespace 中的 Job 使用。需要注意,修改 WorkloadPriorityClass 对象的 value,不会自动修改已经创建的 Workload 中的 spec.priority。
4.2 Job 如何引用 WorkloadPriorityClass
Job 通过以下 Label 指定:
metadata:
labels:
kueue.x-k8s.io/queue-name: training-queue
kueue.x-k8s.io/priority-class: high-priority
完整示例:
apiVersion: batch/v1
kind: Job
metadata:
name: high-priority-job
namespace: team-a
labels:
kueue.x-k8s.io/queue-name: training-queue
kueue.x-k8s.io/priority-class: high-priority
spec:
template:
spec:
containers:
- name: worker
image: registry.k8s.io/e2e-test-images/agnhost:2.53
command:
- /bin/sh
args:
- -c
- sleep 300
resources:
requests:
cpu: "2"
restartPolicy: Never
Kueue 为该 Job 创建 Workload 后,会把优先级信息记录到:
spec:
priorityClassRef:
group: kueue.x-k8s.io
kind: WorkloadPriorityClass
name: high-priority
priority: 1000
v1beta2 使用 priorityClassRef 的 group、kind 和 name 区分 Kueue 的 WorkloadPriorityClass 与 Kubernetes 的 PriorityClass。
4.3 Workload 优先级用在哪里
Workload 优先级主要影响:
-
ClusterQueue 中 Workload 的排队顺序;
-
Workload 是否可以抢占其他 Workload;
-
同一 Cohort 中多个借用配额的 Workload 的处理顺序。
默认情况下,优先级更高的 Workload 会排在前面。
ClusterQueue 中的 Workload 首先按优先级排序,优先级相同时再按创建时间排序。StrictFIFO 和 BestEffortFIFO 使用相同的基础排序方式,区别在于:StrictFIFO 中较早但无法准入的 Workload 会阻塞后续 Workload;BestEffortFIFO 允许后续能够满足配额的 Workload先被准入。
4.4 WorkloadPriorityClass 与 Pod PriorityClass 的区别
| 对比项 | WorkloadPriorityClass | Kubernetes PriorityClass |
|---|---|---|
| 主要作用对象 | Workload | Pod |
| 主要作用阶段 | Kueue 排队、准入和抢占 | kube-scheduler 调度和 Pod 抢占 |
| 是否直接改变 Pod 调度优先级 | 否 | 是 |
| 是否影响 Kueue 排队顺序 | 是 | 未单独指定 WorkloadPriorityClass 时,可能作为 Workload 优先级来源 |
如果 Job 同时指定:
-
WorkloadPriorityClass;
-
Kubernetes PriorityClass;
那么:
WorkloadPriorityClass
用于 Kueue 的 Workload 排队和抢占
Kubernetes PriorityClass
用于 Pod Scheduler 的 Pod 调度和抢占
如果只配置 WorkloadPriorityClass,它不会自动改变 Pod 的调度优先级。
五、Preemption:配额不足时如何让任务让位
5.1 Preemption
Preemption 不是一个独立 CRD。它是一套配置在 ClusterQueue 中的抢占策略:
ClusterQueue.spec.preemption
发起抢占的是:
正在等待准入的 Workload
被抢占的是:
一个或多个已经准入的 Workload
Preemption 的目标是释放足够的 Kueue 逻辑配额,让等待中的 Workload 能够完成准入。
官方将 Preemption 定义为:驱逐一个或多个已经准入的 Workload,以容纳另一个 Workload。
5.2 基础配置
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: team-a-cq
spec:
preemption:
withinClusterQueue: LowerPriority
reclaimWithinCohort: Any
borrowWithinCohort:
policy: LowerPriority
maxPriorityThreshold: 100
这三个字段分别控制不同范围的抢占:
| 字段 | 抢占范围 |
|---|---|
| withinClusterQueue | 当前 ClusterQueue 内部 |
| reclaimWithinCohort | 同一 Cohort 中其他正在借用配额的 ClusterQueue |
| borrowWithinCohort | 为了继续借用 Cohort 配额,抢占其他 ClusterQueue 的低优先级 Workload |
5.3 withinClusterQueue:同一队列内部抢占
示例:
team-a-cq nominalQuota:4 CPU
low-job:
优先级 100
已经使用 4 CPU
high-job:
优先级 1000
正在申请 4 CPU
如果配置:
preemption:
withinClusterQueue: LowerPriority
那么 high-job 可以抢占同一个 ClusterQueue 中的 low-job。可选值包括:
| 值 | 含义 |
|---|---|
| Never | 不允许当前 ClusterQueue 内部抢占 |
| LowerPriority | 只能抢占优先级更低的 Workload |
| LowerOrNewerEqualPriority | 可以抢占优先级更低的 Workload,也可以抢占优先级相同但创建时间更晚的 Workload |
默认值是:
Never
5.4 reclaimWithinCohort:收回自己的名义配额
假设:
team-a-cq nominalQuota:4 CPU
team-b-cq nominalQuota:4 CPU
team-a 当前没有任务,team-b 借用了 team-a 的 4 CPU:
team-b 当前总使用量:8 CPU
team-b 自有 nominalQuota:4 CPU
team-b 借用:4 CPU
此时 team-a 提交一个需要 4 CPU 的 Workload。如果没有 Preemption,team-a 可能需要等待 team-b 的任务自然完成。如果 team-a 配置:
preemption:
reclaimWithinCohort: Any
team-a 可以抢占同一 Cohort 中正在超出自身 nominalQuota 使用配额的 Workload,从而收回自己的名义配额。可选值包括:
| 值 | 含义 |
|---|---|
| Never | 不主动收回被借用的配额 |
| LowerPriority | 只抢占优先级低于当前 Workload 的借用方 Workload |
| Any | 可以抢占符合条件的借用方 Workload,不要求其优先级更低 |
reclaimWithinCohort 的核心含义是:
当前 ClusterQueue 为了在自己的 nominalQuota 范围内运行 Workload,收回被同一 Cohort 其他 ClusterQueue 借用的配额。
5.5 borrowWithinCohort:为了继续借用而抢占
另一种场景是:
team-a 自己的 nominalQuota 不够
team-a 需要借用 Cohort 配额
Cohort 中已经没有空闲配额
其他 ClusterQueue 正在借用这些配额
如果配置:
preemption:
reclaimWithinCohort: LowerPriority
borrowWithinCohort:
policy: LowerPriority
team-a 可以为了借用配额,抢占同一 Cohort 中较低优先级的 Workload。
这比 reclaimWithinCohort 更激进:
reclaimWithinCohort:
为了使用自己的 nominalQuota 而收回配额
borrowWithinCohort:
自己的 nominalQuota 仍然不够,
为了继续借用共享配额而执行抢占
maxPriorityThreshold 可以进一步限制被抢占对象:
borrowWithinCohort:
policy: LowerPriority
maxPriorityThreshold: 100
表示只有优先级不高于 100、并且满足 LowerPriority 条件的 Workload,才可能成为该类抢占的目标。需要特别注意:
borrowWithinCohort只适用于 Classic Preemption,不能与基于 Preemption 的 Fair Sharing 抢占同时使用。
5.6 三种抢占策略对比
| 策略 | 解决的问题 | 候选 Workload 位于哪里 |
|---|---|---|
| withinClusterQueue | 本队列高优先级任务需要配额 | 同一个 ClusterQueue |
| reclaimWithinCohort | 自己的 nominalQuota 被别人借走 | 同一 Cohort 中超出自身 nominalQuota 的 ClusterQueue |
| borrowWithinCohort | 自己还想继续借配额,但 Cohort 没有空闲配额 | 同一 Cohort 中符合优先级限制的借用方 Workload |
Kueue 的源码将这三类场景分别定义在 ClusterQueuePreemption 中,Kueue 先选择能够释放足够配额的抢占目标,然后执行最小化步骤,尽量移除不必要的目标,从而减少被抢占 Workload 的数量。
5.7 Workload 被抢占后会发生什么
被抢占的 Workload 状态中通常会出现:
Evicted=True
Reason=Preempted
除了 Evicted=True、Reason=Preempted,Workload 通常还会出现 Preempted=True Condition,其 Reason 用于说明更具体的抢占原因,例如:
InClusterQueue:被同一个 ClusterQueue 中的 Workload 抢占。InCohortReclamation:因为同一 Cohort 中的其他 ClusterQueue 收回 nominalQuota 而被抢占。InCohortFairSharing:因为 Fair Sharing 抢占而被驱逐。
对于支持暂停和恢复的 Kueue Job 集成,通常会发生:
Workload 被标记为 Evicted
↓
对应 Job 被暂停
↓
已经创建的 Pod 被终止
↓
Quota Reservation 被清除
↓
Workload 重新排队,等待再次准入
Kueue Job Reconciler 在发现 Workload 被驱逐后,会停止对应 Job,并在 Job 停止后清理配额预留。需要注意,抢占发生在 Workload 层,而不是 Kueue 直接选择某一个 Pod 进行 Kubernetes Pod Preemption。
六、Fair Sharing:如何让多个租户更公平地使用资源
仅依靠 FIFO 和优先级,仍然可能出现资源长期分配不公平的问题。例如:
team-a 持续提交大量小任务
team-b 偶尔提交任务
如果只按照提交时间处理,team-a 可能长期占据更多资源,team-b 的任务很难获得机会。
Kueue 中需要区分两套 Fair Sharing 机制:
-
Admission Fair Sharing;
-
Preemption-based Fair Sharing。
它们的作用对象、统计方式和配置位置并不相同。
6.1 Admission Fair Sharing
Admission Fair Sharing 主要解决:
多个 LocalQueue
↓
指向同一个 ClusterQueue
↓
如何公平竞争该 ClusterQueue 的可用配额
例如:
team-a/training-queue ─────┐
├── shared-cq
team-b/training-queue ─────┘
Kueue 会记录每个 LocalQueue 的历史资源使用量,并优先准入历史使用量较少的 LocalQueue 中的 Workload。它的核心不是简单轮询,而是:
记录历史资源消费
↓
对历史用量进行衰减
↓
计算不同 LocalQueue 的相对用量
↓
优先选择资源消费较少的 LocalQueue
Admission Fair Sharing 在 Kueue v0.18.2 中是 Beta 功能,并默认启用。
6.2 Admission Fair Sharing 的配置位置
它由三层配置共同完成。Kueue 全局配置
apiVersion: config.kueue.x-k8s.io/v1beta2
kind: Configuration
admissionFairSharing:
usageHalfLifeTime: 168h
usageSamplingInterval: 5m
resourceWeights:
cpu: 1
memory: 1
这里是 Kueue Controller 的全局配置片段,不是普通业务 Namespace 中直接创建的队列资源。主要字段含义:
| 字段 | 含义 |
|---|---|
| usageHalfLifeTime | 历史用量衰减到一半所需时间 |
| usageSamplingInterval | Kueue 更新资源消费统计的周期 |
| resourceWeights | 不同资源在历史用量计算中的权重 |
这些字段定义在 Kueue v0.18.2 的 Configuration API 中。
ClusterQueue 配置
spec:
admissionScope:
admissionMode: UsageBasedAdmissionFairSharing
表示当前 ClusterQueue 启用基于历史用量的 Admission Fair Sharing。
LocalQueue 配置
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
name: team-a-queue
namespace: team-a
spec:
clusterQueue: shared-cq
fairSharing:
weight: "2"
权重越大,该 LocalQueue 在相同实际资源使用量下计算得到的相对份额越低,因此在竞争时具有更大优势。官方示例中,权重为 2 可以近似理解为:在公平计算时,该 LocalQueue 的资源使用量按照一半参与比较。
6.3 Admission Fair Sharing 不是固定配额
假设:
team-a LocalQueue weight:2
team-b LocalQueue weight:1
这不代表任意时刻必须严格按照:
team-a:66.7%
team-b:33.3%
切分资源。Fair Sharing 的目标是在资源竞争和长期运行过程中,让资源使用逐渐趋向符合权重,而不是把物理资源永久切割成固定比例。
当 team-b 没有任务时,team-a 仍然可以使用更多空闲资源。
6.4 Preemption-based Fair Sharing
Preemption-based Fair Sharing 主要解决:
同一个 Cohort 或 CohortTree 中
多个 ClusterQueue
如何公平竞争共享和借用配额
Kueue 会为 ClusterQueue 或 Cohort 计算 Share Value。这个值主要与以下因素有关:
-
超出 nominalQuota 的资源使用量;
-
Cohort 中可借出的资源量;
-
当前 ClusterQueue 或 Cohort 的权重;
-
多种资源中的主导资源份额。
简化理解:
Share Value
≈ 超出 nominalQuota 的主导资源使用份额 ÷ weight
Kueue倾向于:
准入 Share Value 较低的 ClusterQueue 的 Workload
从 Share Value 较高的 ClusterQueue 中选择抢占候选
如果 ClusterQueue 的使用量没有超过 nominalQuota,其 Share Value 通常为 0。
6.5 Preemption-based Fair Sharing 的配置
Kueue 全局配置:
apiVersion: config.kueue.x-k8s.io/v1beta2
kind: Configuration
fairSharing:
preemptionStrategies:
- LessThanOrEqualToFinalShare
- LessThanInitialShare
两个策略的作用是限制一次公平共享抢占是否合理:
-
LessThanOrEqualToFinalShare:抢占完成后,发起方的 Share 不能高于被抢占方释放目标 Workload 后的 Share; -
LessThanInitialShare:接纳新 Workload 后,发起方的 Share 必须低于被抢占方抢占前的 Share。
Kueue 会按照配置顺序尝试这些策略。ClusterQueue 可以配置权重:
spec:
fairSharing:
weight: "2"
Cohort 也可以配置权重:
apiVersion: kueue.x-k8s.io/v1beta2
kind: Cohort
metadata:
name: ai-department
spec:
parentName: root-cohort
fairSharing:
weight: "0.75"
权重越大,在相同借用量下计算出的 Share Value 越小,因此具有更高的共享资源竞争优势。
6.6 两种 Fair Sharing 的区别
| 对比项 | Admission Fair Sharing | Preemption-based Fair Sharing |
|---|---|---|
| 主要对象 | LocalQueue | ClusterQueue 和 Cohort |
| 主要范围 | 同一个 ClusterQueue | 同一个 CohortTree |
| 主要依据 | LocalQueue 的历史资源用量 | 当前超出 nominalQuota 的共享资源份额 |
| 主要动作 | 调整待准入 Workload 的顺序 | 调整准入顺序并参与公平抢占 |
| 是否依赖抢占 | 不一定 | 是 |
| 权重配置位置 | LocalQueue | ClusterQueue 或 Cohort |
可以这样记:
Admission Fair Sharing:
解决同一 ClusterQueue 下多个 LocalQueue 的历史使用公平。
Preemption-based Fair Sharing:
解决同一 Cohort 中多个 ClusterQueue 的共享配额公平。
七、AdmissionCheck:配额之外的附加准入条件
7.1 为什么获得配额后还需要检查
Kueue 的逻辑配额足够,并不代表外部运行条件一定已经满足。例如:
-
云节点还没有扩容完成;
-
需要等待 ProvisioningRequest 创建容量;
-
需要将任务分配到远端集群;
-
需要通过企业内部审批;
-
需要执行合规或安全检查;
-
需要由外部 Controller 修改 PodSet 配置。
AdmissionCheck 允许 Kueue 在完成 Quota Reservation 后,等待内部或外部 Controller 给出额外准入结果。
7.2 AdmissionCheck 是什么资源
AdmissionCheck 是独立的、集群级 Kueue CRD:
apiVersion: kueue.x-k8s.io/v1beta2
kind: AdmissionCheck
metadata:
name: provisioning-check
spec:
controllerName: kueue.x-k8s.io/provisioning-request
controllerName 表示负责处理该检查的 Controller 标识,并不一定等于某个 Deployment 或 Pod 的名称。AdmissionCheck 还可以通过 parameters 引用额外配置对象:
apiVersion: kueue.x-k8s.io/v1beta2
kind: AdmissionCheck
metadata:
name: provisioning-check
spec:
controllerName: kueue.x-k8s.io/provisioning-request
parameters:
apiGroup: kueue.x-k8s.io
kind: ProvisioningRequestConfig
name: provisioning-config
AdmissionCheck 的核心字段在 v0.18.2 源码中定义为 controllerName 和可选的 parameters。
7.3 ClusterQueue 如何引用 AdmissionCheck
在 Kueue v0.18.2 的 v1beta2 API 中,应通过:
ClusterQueue.spec.admissionChecksStrategy
引用 AdmissionCheck。示例:
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
name: ai-cluster-queue
spec:
namespaceSelector: {}
admissionChecksStrategy:
admissionChecks:
- name: provisioning-check
resourceGroups:
- coveredResources:
- cpu
flavors:
- name: default-flavor
resources:
- name: cpu
nominalQuota: "8"
未配置 onFlavors 时,该 AdmissionCheck 会作用于进入该 ClusterQueue 的全部 Workload。
也可以只对某些 ResourceFlavor 执行:
admissionChecksStrategy:
admissionChecks:
- name: provisioning-check
onFlavors:
- a100-flavor
此时只有使用 a100-flavor 的 Workload 需要执行该检查。v0.18.2 的 ClusterQueue API 中实际字段是 admissionChecksStrategy,内部列表字段是 admissionChecks。
7.4 AdmissionCheck 在准入流程中的位置
ClusterQueue 配额满足
↓
Quota Reservation 创建
↓
QuotaReserved=True
↓
执行全部 AdmissionCheck
↓
全部 AdmissionCheck=Ready
↓
Admitted=True
多个 AdmissionCheck 可以并行执行,但 Workload 只有在全部检查都返回 Ready 后,才能最终被准入。
7.5 AdmissionCheck 的四种状态
Workload 中每个 AdmissionCheck 可以处于以下状态:
| 状态 | 含义 |
|---|---|
| Pending | 检查尚未完成,或者 Controller 尚未给出结果 |
| Ready | 检查已经通过 |
| Retry | 当前暂时不能通过,需要退出本轮准入并在之后重试 |
| Rejected | 短期内无法通过,不值得继续自动重试 |
这些状态定义在 Kueue v0.18.2 API 中。
7.6 Workload 中的状态字段
v0.18.2 中,AdmissionCheck 的 Workload 状态记录在:
status.admissionChecks
示例:
status:
admission:
clusterQueue: ai-cluster-queue
admissionChecks:
- name: provisioning-check
state: Pending
message: "Waiting for capacity"
conditions:
- type: QuotaReserved
status: "True"
- type: Admitted
status: "False"
这表示:
逻辑配额已经预留
↓
Provisioning Check 尚未通过
↓
Workload 还没有最终准入
v0.18.2 生成的 API Reference 明确将该字段定义为 status.admissionChecks。
7.7 Retry 和 Rejected 的区别
Retry 表示:
当前暂时不能通过
但后续可能恢复
例如:
-
云服务暂时没有容量;
-
外部接口短暂失败;
-
等待一段时间后可以再次尝试。
AdmissionCheck Controller 可以通过:
requeueAfterSeconds: 300
指定至少等待多久后重新参与准入。
Rejected 表示:
当前检查在短期内无法通过
继续自动重试没有意义
例如:
-
配置不合法;
-
目标资源类型永久不可用;
-
企业审批明确拒绝;
-
外部条件无法满足。
Workload 通常会被驱逐并停用,需要修复问题后重新激活,才能再次进入准入流程。
7.8 AdmissionCheck 不可用会影响 ClusterQueue
如果 ClusterQueue 引用的 AdmissionCheck:
-
不存在;
-
Controller 尚未将其标记为 Active;
-
配置不合法;
ClusterQueue 可能显示:
Active=False
因为 Kueue 无法保证进入该 ClusterQueue 的 Workload 能够完成必要检查。
八、从创建 Job 到 Pod 运行的完整流程
现在把两篇文章中的全部基础概念串起来。
第一阶段:Job 进入 Kueue
1. 用户创建 Job
2. Job 通过 Label 指定 LocalQueue:
kueue.x-k8s.io/queue-name
3. Kueue Webhook 管理 Job 的 suspend 状态
4. Kueue 为 Job 创建或维护 Workload
5. Workload.spec.queueName 指向同一 Namespace 中的 LocalQueue
第二阶段:寻找 ClusterQueue 并计算资源
6. LocalQueue.spec.clusterQueue 找到 ClusterQueue
7. Kueue 检查 Job 所在 Namespace
是否符合 ClusterQueue.spec.namespaceSelector
8. Kueue 根据 Workload.spec.podSets
计算整个 Workload 的资源总请求
9. 在 ClusterQueue.resourceGroups 中
查找需要管理的 Resource
10. 为每个资源选择合适的 ResourceFlavor
第三阶段:检查和准备配额
11. 检查 ClusterQueue 自己的 nominalQuota
12. 如果自有配额不足,检查是否加入 Cohort
13. 检查 Cohort 中是否存在可借配额
14. 检查 borrowingLimit 和 lendingLimit
15. 如果仍然无法满足,并且启用了 Preemption,
寻找可以被抢占的 Workload
16. 等待被抢占 Workload 释放配额
第四阶段:创建 Quota Reservation
17. 为 Workload 创建 Quota Reservation
18. Workload.status.admission
记录 ClusterQueue、Flavor 和 resourceUsage
19. Workload QuotaReserved=True
第五阶段:执行附加准入检查
20. 如果 ClusterQueue 配置了 AdmissionCheck,
启动或等待相关 Controller 执行检查
21. 所有 AdmissionCheck 必须变为 Ready
22. 如果检查为 Retry,Kueue 释放当前 Quota Reservation;
如果 Workload 已经运行,则先将其驱逐,之后经过退避时间重新排队
23. 如果检查为 Rejected,Kueue 将 Workload 的 spec.active 设置为 false;
如果已经准入或预留配额,则执行驱逐并释放 Quota Reservation
第六阶段:最终准入和创建 Pod
24. 所有准入条件满足
25. Workload Admitted=True
26. Kueue 解除 Job suspend
27. Job Controller 创建 Pod
28. kube-scheduler 或 Volcano 为 Pod 选择 Node
29. Device Plugin 分配 GPU、NPU 等设备
30. kubelet 启动容器
Kueue Scheduler 源码中的主要处理顺序也是先从队列中获取待处理 Workload、构建配额快照、计算 Flavor 和借用方案,然后执行抢占或准入;Job Reconciler 再根据 Workload 的 Admitted 状态决定启动或暂停 Job。
九、如何判断 Workload 卡在哪个阶段
9.1 首先检查 Job 是否暂停
kubectl -n <namespace> get job <job-name> \
-o jsonpath='{.spec.suspend}{"\n"}'
如果结果是:
true
说明 Job 还没有被允许开始,需要继续检查 Workload。
9.2 检查 Workload
kubectl -n <namespace> get workloads
查看详细状态:
kubectl -n <namespace> describe workload <workload-name>
重点关注:
QuotaReserved
Admitted
Evicted
Finished
status.admission
status.admissionChecks
9.3 QuotaReserved=False
如果:
QuotaReserved=False
Admitted=False
通常说明问题发生在配额阶段。检查:
-
LocalQueue 是否存在;
-
LocalQueue 是否指向正确的 ClusterQueue;
-
ClusterQueue 是否 Active;
-
Namespace 是否符合 namespaceSelector;
-
ResourceFlavor 是否存在;
-
nominalQuota 是否足够;
-
Cohort 是否存在;
-
borrowingLimit 是否足够;
-
lendingLimit 是否限制了可借配额;
-
当前是否需要等待其他 Workload 释放配额;
-
Preemption 是否允许执行。
9.4 QuotaReserved=True,但 Admitted=False
如果:
QuotaReserved=True
Admitted=False
说明逻辑配额已经预留,但还有附加准入条件没有完成。重点检查:
status.admissionChecks
可能看到:
Pending
Retry
Rejected
还要检查对应 AdmissionCheck 是否 Active,以及处理该 AdmissionCheck 的 Controller 是否正常运行。
9.5 Admitted=True,但 Pod 一直 Pending
如果:
QuotaReserved=True
Admitted=True
Job suspend=false
Pod Pending
问题通常已经不在 Kueue 配额层,而在 Kubernetes Pod 调度层。检查:
kubectl -n <namespace> describe pod <pod-name>
常见原因包括:
-
节点真实 CPU 或内存不足;
-
GPU Device Plugin 未暴露资源;
-
ResourceFlavor 注入的 nodeSelector 没有匹配节点;
-
Pod 缺少节点污点对应的 toleration;
-
PVC NodeAffinity 不匹配;
-
RuntimeClass 不存在;
-
Volcano Scheduler 异常;
-
镜像拉取失败。
十、最终总结
Kueue 的高级配额和准入机制可以总结如下。
| 概念 | 准确归属 | 主要作用 |
|---|---|---|
| Cohort | 集群级 CRD | 组织多个 ClusterQueue 共享配额 |
| borrowingLimit | ClusterQueue 中某个 ResourceQuota 字段 | 限制最多借入多少配额 |
| lendingLimit | ClusterQueue 中某个 ResourceQuota 字段 | 限制最多借出多少配额 |
| Quota Reservation | Kueue 配额预留阶段 | 为 Workload 锁定 ClusterQueue 配额 |
| Admission | Workload 准入状态 | 表示 Workload 可以开始执行 |
| WorkloadPriorityClass | 集群级 CRD | 定义 Workload 的排队和抢占优先级 |
| Preemption | ClusterQueue 中的策略 | 驱逐已准入 Workload,为其他 Workload 释放配额 |
| Admission Fair Sharing | 全局配置、ClusterQueue 和 LocalQueue 共同实现 | 公平排序同一 ClusterQueue 下的 LocalQueue |
| Preemption-based Fair Sharing | 全局配置、ClusterQueue、Cohort 和 Preemption 共同实现 | 公平分配 Cohort 中的共享配额 |
| AdmissionCheck | 集群级 CRD | 在配额预留后执行附加准入检查 |
完整链路为:
Job
↓
Workload
↓
LocalQueue
↓
ClusterQueue
↓
ResourceFlavor
↓
检查 nominalQuota
↓
必要时从 Cohort 借用配额
↓
必要时执行 Preemption
↓
Quota Reservation
↓
AdmissionCheck
↓
Admission
↓
Job 解除暂停
↓
Pod 创建
↓
kube-scheduler / Volcano
↓
Node 和设备
最后需要记住三句话:
Cohort 决定多个 ClusterQueue 如何共享未使用配额,但共享的是 Kueue 的逻辑账本,不是具体的物理设备。
QuotaReserved=True 只代表配额已经预留,只有 Admitted=True 才代表 Workload 已经满足全部准入条件。
Preemption 配置在 ClusterQueue 中,真正被排队、准入和抢占的对象始终是 Workload。
更多推荐


所有评论(0)