前言

在上一篇文章中,我们介绍了 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 使用 priorityClassRefgroupkindname 区分 Kueue 的 WorkloadPriorityClass 与 Kubernetes 的 PriorityClass。


4.3 Workload 优先级用在哪里

Workload 优先级主要影响:

  • ClusterQueue 中 Workload 的排队顺序;

  • Workload 是否可以抢占其他 Workload;

  • 同一 Cohort 中多个借用配额的 Workload 的处理顺序。

默认情况下,优先级更高的 Workload 会排在前面。

ClusterQueue 中的 Workload 首先按优先级排序,优先级相同时再按创建时间排序。StrictFIFOBestEffortFIFO 使用相同的基础排序方式,区别在于: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。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐