终于搞懂 Kueue:5 个核心对象一次讲透

kueue-p2-core.jpg

很多人刚接触 Kueue 时最大的困惑,不是 YAML 怎么写,而是看着一堆 CRD:ResourceFlavor、ClusterQueue、LocalQueue、Cohort、Workload,不知道它们之间到底是什么关系。本文不会逐个照着 API 文档介绍字段,而是把这五个对象放到同一条资源准入链路中,一次讲清楚它们各自负责什么、为什么要存在,以及它们之间如何协作。

上一篇文章中分析了 Kueue 的完整工作流:

kueue-workflow.png

涉及到多个核心对象,下面逐个拆开讲。

1. ResourceFlavor:资源长什么样

Kueue 要管资源,第一步得知道集群里有哪些"种类"的资源。集群里的 CPU、GPU 通常不是同构的:

  • 价格和可用性 :竞价型 vs 按需型虚拟机
  • 架构 :x86 vs ARM CPU
  • 品牌和型号 :Nvidia A100 vs T4 GPU

ResourceFlavor 就是干这个的——给资源分个类,贴个标签,后面 ClusterQueue 按这个分类来管配额。

Kueue 在 Admission 阶段,会根据 ClusterQueue 中配置的 Flavor 顺序、Quota 是否满足、Flavor 是否匹配 Pod 等因素,选择一个可用的 ResourceFlavor。

空 Flavor

如果集群资源是同构的,或者不需要为不同资源规格分别管理配额,那直接创建一个不包含任何标签或污点的空 ResourceFlavor 即可:

1
2
3
4
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: default-flavor

这类 ResourceFlavor 不承担节点筛选或污点控制的职责,仅作为统一的资源抽象存在,方便后续扩展不同资源类型。

只分类不加污点

1
2
3
4
5
6
7
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: "gpu"
spec:
  nodeLabels:
    instance-type: gpu

新增参数:

  • spec.nodeLabels :通过 label 关联到对应的节点。具体实现上则是通过将 flavor 中 nodeLabels 部分自动注入到 Pod Spec 中的 nodeSelector,从而达到只将 Pod 调度到该 flavor 关联的节点上。

这类 Flavor 实现了根据 label 将节点进行分类,算是名副其实。管理员先给节点打上对应 label 即可实现分类:

1
2
root@lixd-dev-4:~# kubectl label node lixd-dev-4 instance-type=gpu
node/lixd-dev-4 labeled

但也只是简单做了分类,不是该 Flavor 中的 Job 也能手动调度到这些 GPU 节点。比较推荐的做法是给节点再打上污点,这样就不是随便能调度上去了:

1
kubectl taint node xxx nvidia.com/gpu=true:NoSchedule

节点打上污点后,Flavor 中的 Job 也不能调度了。Kueue 提供了两种模式来解决这个问题:

  1. 自动容忍模式 :将污点容忍信息写到 ResourceFlavor,Kueue 自动给使用该 Flavor 的 Pod 加上容忍
  2. 手动容忍模式 :将污点信息写到 ResourceFlavor,Kueue 做匹配拦截,只允许带了对应容忍的 Pod 使用该 Flavor

带污点

自动污点容忍调度

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: "spot"
spec:
  nodeLabels:
    instance-type: spot
  tolerations:
  - key: "spot-taint"             # 节点上已有污点的 key
    operator: "Exists"
    effect: "NoSchedule"          # 支持 NoSchedule 和 NoExecute

新增参数:

  • spec.tolerations :声明该 flavor 关联节点上的污点容忍信息。Kueue 准入时自动注入到 Pod 的 .spec.template.spec.tolerations,确保 Pod 能容忍节点污点从而正常调度。

手动污点容忍

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: "spot"
spec:
  nodeLabels:
    instance-type: spot
  nodeTaints:
  - effect: NoSchedule            # 只支持 NoSchedule 和 NoExecute,PreferNoSchedule 会被忽略
    key: spot
    value: "true"

新增参数:

  • spec.nodeTaints :在 flavor 上定义准入门槛。Kueue 不会自动注入容忍度,Pod 必须自己带对应 toleration 才能通过准入、拿到配额。

用户提交 Job 时必须自己带上 toleration,否则 Kueue 不批配额:

1
2
3
4
5
6
7
spec:
  template:
    spec:
      tolerations:
      - key: spot
        operator: Exists
        effect: NoSchedule

注意spec.nodeTaints 通常应与节点上的真实污点保持一致。否则可能出现 Pod 过了 Kueue 准入、拿到配额,但 K8s 调度器发现节点有真污点而 Pod 没对应 toleration,最终调度失败——白白占了配额。本质就是把调度层面的拦截提前到 Kueue 准入层面

两种模式怎么选

自动模式(spec.tolerations手动模式(spec.nodeTaints
谁给 Pod 加 tolerationKueue 自动注入用户自己写
用户需要关心节点污点吗不需要,提交 Job 就行必须自己写 toleration
用在哪生产默认,对用户最友好保护昂贵资源,强制用户显式声明

选型建议:优先用自动模式;只有当你需要强制用户显式声明才能使用某种昂贵资源时,才用手动模式。

一句话记住:ResourceFlavor 定义资源的属性(标签、污点、容忍等),真正决定资源配额和公平性的仍然是 ClusterQueue。

2. ClusterQueue:谁是配额的真正管家

如果 ResourceFlavor 是给资源分类,那真正决定谁能用、用多少的是谁?答案就是 ClusterQueue。它才是 Kueue 配额管理的核心,定义了使用上限和公平共享规则。

基础用法

一个基础的 ClusterQueue 示例如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: "cluster-queue"
spec:
  namespaceSelector: {} # 匹配所有命名空间。
  resourceGroups:
  - coveredResources: ["cpu", "memory", "pods"]
    flavors:
    - name: "default-flavor"
      resources:
      - name: "cpu"
        nominalQuota: 9
      - name: "memory"
        nominalQuota: 36Gi
      - name: "pods"
        nominalQuota: 5

字段解析:

  • spec.namespaceSelector :指定该 ClusterQueue 可以在哪些 namespace 被使用。当前 ClusterQueue 可以被任意 Namespace 使用。
  • resourceGroups :资源组,定义 ClusterQueue 的资源额度,可以有多个,每个组都是独立的。
    • coveredResources :管哪些资源,同一个 group 里面的资源必须在同一个 flavor 里面分配。
    • flavors :可以有多个,分配是按顺序尝试。
      • name :使用名称引用前面创建的 ResourceFlavor。
      • resources :在这个 flavor 下,每种资源的配额上限。
        • nominalQuota :保底配额,表示该 ClusterQueue 的名义配额,优先保障自身使用;未使用的部分可以按照 borrowing/lending 规则被其他 Queue 借用。
        • borrowingLimit :你最多能从 Cohort 里借入多少,所以你最多能用 nominalQuota + borrowingLimit。
        • lendingLimit :你最多允许别人从你这借走多少(当你空闲时),需要时可通过预抢占收回。

以 cpu 为例,基于上面三个配额字段,你可以使用的范围是 nominalQuota ~ (nominalQuota + borrowingLimit)。

高级用法

在基础配置之上,ClusterQueue 还支持以下进阶字段,按用途分组:

调度顺序:排队策略

1
2
spec:
  queueingStrategy: BestEffortFIFO
  • BestEffortFIFO(默认) :前面的 Job 拿不到配额,后面的能插队先跑,资源利用率高。
  • StrictFIFO :前面的 Job 拿不到配额,后面的必须等,适合有顺序依赖的 Pipeline。

资源共享:加入 Cohort

1
2
spec:
  cohortName: research-cohort
  • cohortName :关联到一个 Cohort,同一个 Cohort 里的 ClusterQueue 可以互相共享资源。

抢占相关

当 ClusterQueue 或其 Cohort 中没有足够的配额时,新进入的 Workload 可以触发预抢占,挤掉低优先级的 Workload。涉及两个配置项:

预抢占策略

1
2
3
4
5
6
7
spec:
  preemption:
    reclaimWithinCohort: Any                # 可抢占 Cohort 中超过名义配额的 Workload
    borrowWithinCohort:
      policy: LowerPriority                 # 借用时只抢占低优先级(不能与 Fair Sharing 一同使用)
      maxPriorityThreshold: 100             # 且仅抢占优先级 ≤ 100 的 Workload
    withinClusterQueue: LowerPriority       # 同队列内,低优先级让路给高优先级
  • withinClusterQueue :同队列内,当待处理 Workload 不适合配额时,是否可抢占同队列中的活动 Workload。Never(默认)不抢占;LowerPriority 仅抢占低优先级;LowerOrNewerEqualPriority 抢占低优先级或同优先级的。
  • reclaimWithinCohort :是否可抢占 Cohort 中使用了超过其名义配额的 Workload。Never(默认)不抢占;LowerPriority 仅抢占低优先级;Any 可抢占任意优先级。
  • borrowWithinCohort :当需要借用配额时是否触发抢占。Never(默认)不触发;LowerPriority 仅抢占 Cohort 中低优先级的 Workload(需同时启用 reclaimWithinCohort)。注意:只能配置经典抢占,不能与 Fair Sharing 一同使用

优先抢占还是借用

当 ClusterQueue 有多个 flavor 时,Kueue 按顺序尝试。当当前 flavor 配额用完时,你可以影响 Kueue 的行为:

  • whenCanBorrow :能从 Cohort 借的时候。MayStopSearch(默认):借了就停,不再试下一个;TryNextFlavor:不借,先试下一个 flavor。
  • whenCanPreempt :能抢占低优先级 Job 的时候。TryNextFlavor(默认):先试下一个 flavor;MayStopSearch:不试了,直接抢占。
1
2
3
4
spec:
  flavorFungibility:
    whenCanBorrow: TryNextFlavor
    whenCanPreempt: MayStopSearch

运维控制:停止策略

控制队列的运行状态,ClusterQueue 和 LocalQueue 都支持。

  • None(默认) :正常运行,新 Job 正常准入,已运行的不受影响。
  • Hold :停止新的准入,已准入的不受影响,适合维护、配额调整等临时场景。
  • HoldAndDrain :停止新的准入 + 触发已准入工作负载的驱逐,适合紧急情况需要立刻清场。

维护完恢复为 None 或直接删掉这个字段即可。这是运维操作,不是常态配置。

1
2
spec:
  stopPolicy: Hold

把上面的都放在一起

一个包含所有字段的完整 ClusterQueue:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: ai-team-cq
spec:
  # 1. 加入 Cohort,允许和其他 ClusterQueue 共享资源
  cohortName: research-cohort

  # 2. 排队策略
  queueingStrategy: BestEffortFIFO  # 默认。StrictFIFO 会阻塞

  # 3. 命名空间选择器(谁能用这个 ClusterQueue)
  namespaceSelector:
    matchLabels:
      team: ai

  # 4. 抢占策略
  preemption:
    reclaimWithinCohort: Any                 # 可抢占 Cohort 中超过名义配额的 Workload
    borrowWithinCohort:
      policy: LowerPriority                  # 借用时只抢占低优先级(不能与 Fair Sharing 一同使用)
      maxPriorityThreshold: 100              # 且仅抢占优先级 ≤ 100 的 Workload
    withinClusterQueue: LowerPriority        # 同队列内,低优先级让路给高优先级

  # 5. 配置是优先抢占还是借用
  flavorFungibility:
    whenCanBorrow: TryNextFlavor
    whenCanPreempt: MayStopSearch

  # 6. 停止策略
  # stopPolicy: Hold

  # 7. 资源组定义
  resourceGroups:
  - coveredResources: ["cpu", "memory"]      # 第一组:计算资源
    flavors:
    - name: default-flavor
      resources:
      - name: cpu
        nominalQuota: 16                     # 保底配额
        borrowingLimit: 8                    # 最多能从 Cohort 借多少
        lendingLimit: 8                      # 最多允许别人借多少
      - name: memory
        nominalQuota: 64Gi
        borrowingLimit: 32Gi
        lendingLimit: 32Gi

  - coveredResources: ["nvidia.com/gpu"]     # 第二组:GPU 资源
    flavors:
    - name: a100
      resources:
      - name: nvidia.com/gpu
        nominalQuota: 4
        borrowingLimit: 2
    - name: t4
      resources:
      - name: nvidia.com/gpu
        nominalQuota: 8

一句话记住:ClusterQueue 才是真正管配额和公平性的地方。 第一次看会和 LocalQueue 搞混,其实记住一句就够了——ClusterQueue 管资源,LocalQueue 管用户。

3. LocalQueue:用户怎么进队列

ClusterQueue 是集群级别的,但用户不能直接往里塞 Job。中间还需要一层 LocalQueue——它是一个命名空间对象,指向一个 ClusterQueue,作为用户提交 Job 的入口。

1
2
3
4
5
6
7
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
  namespace: team-a
  name: team-a-queue
spec:
  clusterQueue: cluster-queue

注意:kubectl get queueskubectl get localqueues 的别名,更方便日常使用。

LocalQueue vs ClusterQueue

维度LocalQueueClusterQueue
作用域命名空间集群
谁创建团队自己批处理管理员
职责将同一租户的工作负载分组,指向一个 ClusterQueue管理资源池的配额和公平共享规则
一对一?多个 LocalQueue 可以指向同一个 ClusterQueue
1
2
3
4
5
命名空间 team-a:
  LocalQueue: training-queue  ──┐
  LocalQueue: inference-queue ──┼──→ ClusterQueue: ai-team-cq
命名空间 team-b:                │
  LocalQueue: batch-queue ──────┘

一句话记住:LocalQueue 就是 Namespace 访问 ClusterQueue 的入口。 它不管配额,只管"我这个 namespace 的 Job 往哪个 ClusterQueue 送"。

4. Cohort:队列之间怎么借资源

如果每个 ClusterQueue 只能用自己的保底配额,那 team-a 闲着 3 核、team-b 想多跑 1 核也借不到,资源就浪费了。Cohort 就是解决这个的——可以理解成一个"资源共享联盟",加进同一个 Cohort 的 ClusterQueue 可以互相借配额。

第一次看 Cohort 我也没太理解。后来发现,它本身还能定义 resourceGroups(共享配额池),这些资源是管理员额外划拨给整个联盟的公共池,而不是把各个 ClusterQueue 的 nominalQuota 自动汇总得到的。

ClusterQueue 与 Cohort 的关系

基本用法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 创建 Cohort
apiVersion: kueue.x-k8s.io/v1beta2
kind: Cohort
metadata:
  name: hello-cohort
spec:
  resourceGroups:
  - coveredResources: ["cpu"]
    flavors:
    - name: default-flavor
      resources:
      - name: cpu
        nominalQuota: 12    # Cohort 级别的共享配额(额外资源)
---
# ClusterQueue A 加入 Cohort
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: team-a-cq
spec:
  cohortName: hello-cohort
  resourceGroups:
  - coveredResources: ["cpu"]
    flavors:
    - name: default-flavor
      resources:
      - name: cpu
        nominalQuota: 4
        borrowingLimit: 4    # 最多借 4 核
        lendingLimit: 2      # 最多出借 2 核
---
# ClusterQueue B 也加入同一个 Cohort
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: team-b-cq
spec:
  cohortName: hello-cohort
  resourceGroups:
  - coveredResources: ["cpu"]
    flavors:
    - name: default-flavor
      resources:
      - name: cpu
        nominalQuota: 4
        borrowingLimit: 4
        lendingLimit: 2

上面的配置意味着:

  • 两个队列各有 4 核保底,Cohort 自己配置的一组共享配额 12 核。(总计可用 20 核)
  • 空闲时可以互相借用
  • 如果 ClusterQueue 想从 Cohort 借用资源,它必须为该资源和 Flavor 定义 nominalQuota(即使值为 0)

分层 Cohort(Hierarchical Cohorts)

Cohort 可以组织成树形结构(CohortTree),适合大型组织:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 根 Cohort
apiVersion: kueue.x-k8s.io/v1beta2
kind: Cohort
metadata:
  name: root-cohort
---
# AI 部门 Cohort
apiVersion: kueue.x-k8s.io/v1beta2
kind: Cohort
metadata:
  name: ai-dept
spec:
  parentName: root-cohort           # 父节点
  fairSharing:
    weight: "0.75"
---
# 大数据部门 Cohort
apiVersion: kueue.x-k8s.io/v1beta2
kind: Cohort
metadata:
  name: bigdata-dept
spec:
  parentName: root-cohort           # 父节点
  fairSharing:
    weight: "0.25"
1
2
3
4
5
6
7
root-cohort(总资源池)
├── ai-dept(权重 0.75 → 趋向使用 75% 公共资源)
│   ├── team-a-cq
│   └── team-b-cq
└── bigdata-dept(权重 0.25 → 趋向使用 25% 公共资源)
    ├── spark-cq
    └── flink-cq

同一个 CohortTree 中的 ClusterQueue 可以使用其中的资源,遵循为 Cohort 和 ClusterQueue 指定的借用和借出限制。

一句话记住:Cohort 让多个 ClusterQueue 可以互相借资源。

5. Workload:Kueue 真正在调度什么

前面四个都是"配置",那 Kueue 真正调度、准入的对象是什么?是 Workload——一个要运行至完成的应用,由一个或多个 Pod 组成。Kueue 的 Admission、Quota Accounting、Preemption 全是围绕它展开的。

通常你不需要手动创建 Workload,Kueue 会为每个 Job 自动创建。但理解它的结构有助于排查问题。

关键字段

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: kueue.x-k8s.io/v1beta2
kind: Workload
metadata:
  name: sample-job
  namespace: team-a
spec:
  active: true               # 设为 false 可停止 Workload(已运行的会被驱逐且不重新排队)
  queueName: team-a-queue    # 指向 LocalQueue
  podSets:                   # Pod 组定义(Kueue 从 Job 自动提取)
  - name: main
    count: 3                 # Pod 数量
    template:
      spec:
        containers:
        - name: container
          image: registry.k8s.io/e2e-test-images/agnhost:latest
          resources:
            requests:
              cpu: "1"
              memory: 200Mi
  maximumExecutionTimeSeconds: 3600   # Workload 处于 Admitted 状态超过此秒数后自动停用

优先级

Workload 的优先级影响准入顺序,有两种设置方式:

  • Pod 优先级 :对于 batch/v1.Job,Kueue 根据 Job 的 Pod 模板的 Pod 优先级设置 Workload 的优先级
  • WorkloadPriorityClass :独立管理工作负载的排队和抢占优先级,与 Pod 调度优先级分离
1
2
3
4
5
6
apiVersion: kueue.x-k8s.io/v1beta2
kind: WorkloadPriorityClass
metadata:
  name: high-priority
value: 1000
description: "高优先级训练任务"

使用方式:在 Job 的 label 中指定:

1
2
3
metadata:
  labels:
    kueue.x-k8s.io/priority-class: high-priority

资源请求计算

Kueue 将 Workload 的总资源使用量计算为每个 podSet 资源请求的总和: podSet 的资源使用量 = Pod 规格的资源请求 × count

Kueue 会根据 Limit Ranges 和 Runtime Class Overhead 调整资源使用量。

一句话记住:Workload 才是 Kueue 真正调度和准入的对象,前面四个对象都是给它配规则的。

6. Demo:串联五大核心对象

理论讲完了,下面用一个 demo 把五大核心对象串起来,验证配额管控和 Cohort 借用能力。环境是一个单节点 K8s 集群(8 核 CPU)+ Kueue v0.18.1。

1
2
3
4
5
Cohort(team-ab)
├── team-a-cq(保底 4 核,最多借到 6 核)
│     └── team-a-queue
└── team-b-cq(保底 2 核,最多借到 5 核)
      └── team-b-queue

Step 1:给节点打 label 和 taint

1
2
3
4
5
# 打 label,标记节点类型
kubectl label node lixd-dev-4 node-type=cpu

# 加 taint,防止没有容忍的 Pod 调度上来
kubectl taint node lixd-dev-4 dedicated=team:NoSchedule

Step 2:创建 ResourceFlavor(tolerations 自动模式)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: cpu-flavor
spec:
  nodeLabels:
    node-type: cpu           # 匹配节点
  tolerations:               # Kueue 自动注入 Pod,容忍节点污点
  - key: dedicated
    operator: Exists
    effect: NoSchedule

Step 3:创建 Cohort + 两个 ClusterQueue + 两个 LocalQueue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# Cohort:让两个队列共享配额
apiVersion: kueue.x-k8s.io/v1beta2
kind: Cohort
metadata:
  name: team-ab
---
# team-a:保底 4 核,最多借到 6 核
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: team-a-cq
spec:
  cohortName: team-ab
  namespaceSelector: {}
  resourceGroups:
  - coveredResources: ["cpu", "pods"]
    flavors:
    - name: cpu-flavor
      resources:
      - name: cpu
        nominalQuota: 4
        borrowingLimit: 2
      - name: pods
        nominalQuota: 3
---
# team-b:保底 2 核,最多借到 5 核
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: team-b-cq
spec:
  cohortName: team-ab
  namespaceSelector: {}
  resourceGroups:
  - coveredResources: ["cpu", "pods"]
    flavors:
    - name: cpu-flavor
      resources:
      - name: cpu
        nominalQuota: 2
        borrowingLimit: 3
      - name: pods
        nominalQuota: 3
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
  namespace: default
  name: team-a-queue
spec:
  clusterQueue: team-a-cq
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
  namespace: default
  name: team-b-queue
spec:
  clusterQueue: team-b-cq

Step 4:提交 Job A(team-a,3 核,保底内)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: batch/v1
kind: Job
metadata:
  name: job-a
  labels:
    kueue.x-k8s.io/queue-name: team-a-queue
spec:
  template:
    spec:
      containers:
      - name: sleep
        image: busybox
        command: ['sleep', '30']
        resources:
          requests:
            cpu: '3'
      restartPolicy: Never

30 秒后 Job A 跑完,验证 Kueue 自动注入了 toleration 和 nodeSelector:

1
2
3
4
5
$ kubectl get pod job-a-<pod-suffix> -o jsonpath='{.spec.tolerations[0]}'
{"effect":"NoSchedule","key":"dedicated","operator":"Exists"}   # ← 自动注入的

$ kubectl get pod job-a-<pod-suffix> -o jsonpath='{.spec.nodeSelector}'
{"node-type":"cpu"}                                              # ← 自动注入的

Step 5:提交 Job B(team-b,3 核,需要借用)

team-b 保底只有 2 核,要 3 核需要从 team-a 借 1 核空闲配额(此时 Job A 还在跑,用了 3 核,team-a 空闲 1 核)。 此时提交 JobB:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: batch/v1
kind: Job
metadata:
  name: job-b
  labels:
    kueue.x-k8s.io/queue-name: team-b-queue
spec:
  template:
    spec:
      containers:
      - name: sleep
        image: busybox
        command: ['sleep', '30']
        resources:
          requests:
            cpu: '3'
      restartPolicy: Never

借用过程:team-a 保底 4 核用了 3 核,空闲 1 核 → team-b 保底 2 核不够(要 3 核)→ 从 team-a 借了 1 核 → 准入成功。

30 秒后 Job B 跑完,team-b 的 3 核配额归还。等 Job A 也跑完后,整个 Cohort 就空了:team-a 保底 4 核全空闲,team-b 保底 2 核全空闲,总共 6 核可借。

Step 6:提交 Job D(team-b,5 核,测试 borrowingLimit 上限)

Job A、Job B 都已跑完,配额全部归还。现在 team-b 最多能用保底 2 + 借 3 = 5 核(team-a 空闲 4 核,足够借)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: batch/v1
kind: Job
metadata:
  name: job-d
  labels:
    kueue.x-k8s.io/queue-name: team-b-queue
spec:
  template:
    spec:
      containers:
      - name: sleep
        image: busybox
        command: ['sleep', '600']
        resources:
          requests:
            cpu: '5'
      restartPolicy: Never

结果:

1
2
3
$ kubectl get workloads
NAME              QUEUE          ADMITTED
job-job-d-fd3f3   team-b-queue   True          # 保底2+借3=5,刚好到上限,准入

Step 7:提交 Job E(team-b 再多 1 核,超过 borrowingLimit)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: batch/v1
kind: Job
metadata:
  name: job-e
  labels:
    kueue.x-k8s.io/queue-name: team-b-queue
spec:
  template:
    spec:
      containers:
      - name: sleep
        image: busybox
        command: ['sleep', '120']
        resources:
          requests:
            cpu: '1'
      restartPolicy: Never

结果:

1
2
3
4
5
6
7
8
$ kubectl get workloads
NAME              QUEUE          ADMITTED
job-job-d-fd3f3   team-b-queue   True          # 5核已用满
job-job-e-918d8   team-b-queue   False         # 再多 1 核,Pending

$ kubectl get workload job-job-e-918d8 -o jsonpath='{.status.conditions[0].message}'
insufficient unused quota for cpu in flavor cpu-flavor, 1 more needed
# team-b 最多 5 核,Job D 已用 5 核,没有余量了

小结

Job团队请求 CPU保底借用结果
Job Ateam-a3 核4 核0✅ 准入
Job Bteam-b3 核2 核借 1 核✅ 准入
Job Dteam-b5 核2 核借 3 核✅ 准入(到 borrowingLimit 上限)
Job Eteam-b1 核已满无法借❌ Pending

5 个核心对象在这里全部串联起来了。

7. 小结

Kueue 的核心,其实不是“调度算法”,而是围绕 Workload 的准入与资源分配模型。

五个对象分别扮演了不同角色,但可以用一句话快速串起来:

  • ResourceFlavor :定义“资源长什么样”(节点/污点/标签)
  • ClusterQueue :定义“资源怎么分、给谁多少”
  • LocalQueue :定义“哪个命名空间用哪个资源池”
  • Cohort :定义“多个队列之间如何共享资源”
  • Workload :真正被调度和准入的执行单元

第一次接触 Kueue 时,很容易把这些对象看成彼此独立的 CRD,但实际上它们共同组成了一条完整的资源准入链路:

1
Job → LocalQueue → ClusterQueue → ResourceFlavor → Admission → Workload

理解了这条链路,再去看 Borrowing、Fair Sharing、Preemption 等高级能力,就会发现它们都只是围绕这套模型做扩展,而不是新的概念。

Kueue 核心对象总结

0%