编程 Kueue:Kubernetes 原生 Job Queueing——一个 Controller 搞定 Job 排队和资源配额

2026-06-28 12:18:45 +0800 CST views 17

Kueue:Kubernetes 原生 Job Queueing——一个 Controller 搞定 Job 排队和资源配额

如果你运维过 AI 训练集群,一定遇到过这种场景:某团队提交了 100 个 PyTorchJob,Pod 数量瞬间打满整个集群,其他团队的训练任务全部卡住。原生 Kubernetes 的 batch/v1beta1 Job 没有队列管理,提交即调度,FIFO 无公平性可言。Kueue 正是 SIG-Scheduling 为解决这个问题而生的 Job Queueing 框架——它不替换 kube-scheduler,而是在准入层实现资源配额和公平调度,用一个 Controller 把 Job 的排队问题彻底管起来。

作者按: 本文写作时 Kueue 版本为 v0.10+,部分 API 已从 v1beta1 升级至 v1beta2,后文 YAML 示例统一采用 v1beta2 API,建议读者以官方文档为准。


一、背景与痛点:原生 K8s 调度为什么不够用?

1.1 Kubernetes Batch Job 的调度盲区

Kubernetes 的 batch/v1 Job(以及 batch/v1beta1 CronJob)在设计上极度简单:用户提交一个 Job,kube-scheduler 直接将 Pod 调度到节点,没有任何排队机制。这在以下场景会造成严重问题:

场景一:突发性资源争抢

一个 ML 平台同时服务多个算法团队。某天上午 10 点,A 团队需要跑 50 个分布式训练任务做模型调参,B 团队正在做日常模型评估。当 A 团队一口气提交 50 个 PyTorchJob 时,所有 Pod 立即进入调度状态——CPU、GPU、内存瞬间被占满,B 团队的评估任务只能等待,甚至因资源耗尽导致 OOM。

场景二:多租户资源配额失控

假设集群有 64 块 NVIDIA A100,按团队划分应该各用 16 块。但没有任何机制强制执行这个配额——先提交的 Job 先调度,谁手快谁用得多,最终 A 团队可能用掉 48 块,B 团队只剩 16 块甚至更少。

场景三:Fair Sharing 缺失

某段时间 A 团队完全没提交任务,集群 64 块 GPU 空闲;B 团队提交了 20 个任务用了 20 块 GPU。这时 A 团队突然来了紧急任务,必须立即用满 64 块——但 B 团队的 20 个任务还在跑,原生 K8s 没有"抢占"机制,无法回收已分配的资源。

这三个问题的本质是:Kubernetes 的调度粒度在 Pod 级别,而资源管控粒度需要在"作业批次"级别。Kueue 正是填补这一空白的设计。

1.2 AI/ML 训练场景的特殊挑战

AI/ML 训练任务(PyTorchJob、TFJob、JobSet)有以下特殊性,进一步放大了上述问题:

  • 分布式训练多 Pod 协作:一个分布式训练 Job 可能需要 8 个 Pod 同时调度,每个 Pod 都需要 1 块 GPU。如果只有一个 Pod 调度成功,整个训练无法开始。Kueue 支持"All-or-nothing"策略,确保所有 Pod 同时就绪。
  • 长时运行、资源独占:一个 LLM 训练任务可能跑几天甚至几周,一旦占满 GPU 就无法让位。Kueue 的抢占(Preemption)机制可以在高优先级任务到达时主动终止低优先级任务。
  • 多框架并行:PyTorchJob、TFJob、MPIJob、RayJob……每种框架的 CRD 不同,但调度需求相同。Kueue 通过统一的 Workload 抽象层,屏蔽了底层框架差异。
  • GPU 资源稀缺:GPU 是有限资源,调度策略直接影响成本和效率。Kueue 支持按 GPU 型号(nvidia.com/gpu)、节点拓扑进行精细化调度。

1.3 现有方案对比

方案定位优点缺点
Kueue原生 K8s 扩展(SIG-Scheduling)轻量、无缝集成、标准 API、社区活跃功能相对克制,高级调度需配合其他工具
Volcano通用批处理调度器功能丰富、Job 级别调度成熟、GPU 感知好需要替换/扩展 kube-scheduler,侵入性较强
YuniKorn资源调度器(Apache 项目)支持 Spark/Flink 等大数据任务、Fair Queue主要面向大数据场景,K8s 集成相对间接
Karmada多集群管理跨集群调度能力不是为 Job Queueing 设计的

Kueue 的核心哲学是最小侵入:它不替换 kube-scheduler,而是作为一个准入控制器(Admission Controller)运行在调度之前,通过 webhook 将 Job 转换为 Workload,再由 Kueue Controller 管理队列和准入。这种设计使得 Kueue 可以渐进式引入,不需要大规模改造现有集群。


二、核心概念:Kueue 的五块积木

理解 Kueue,首先需要理解它的五个核心 API 对象:ResourceFlavor、ClusterQueue、LocalQueue、Workload、Cohort。它们之间的关系可以用一句话概括:用户通过 LocalQueue 提交 Workload,Workload 在 ClusterQueue 中排队等资源,ClusterQueue 从 Cohort 中借资源,ResourceFlavor 定义节点标签决定 Pod 去哪里

2.1 ResourceFlavor:节点的"身份证"

ResourceFlavor 是 Kueue 中最基础的概念,它本质上是一个带有标签的 CRD 对象,用来描述一组节点的特征。这些标签随后会被 Kube-scheduler 用于节点亲和性调度。

apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: nvidia-a100-80gb  # 节点标签名
spec:
  nodeLabels:
    # 节点必须有这些标签才能匹配这个 Flavor
    node.kubernetes.io/gpu-model: nvidia-A100-SXM4-80GB
    topology.kubernetes.io/rack: rack-a
  taints:
    # 如果节点有污点,Workload 必须有对应 Toleration 才能使用
    - key: nvidia.com/gpu
      value: "true"
      effect: NoSchedule

为什么需要 ResourceFlavor?因为集群中的节点往往异构——有些是 A100,有些是 H100,有些是纯 CPU 节点。Kueue 通过 ResourceFlavor 区分这些节点,在调度时将 Workload 的 Pod 分配到匹配的节点上。

一个常见的实践是:为一个 GPU 集群创建多个 ResourceFlavor,分别对应不同的 GPU 型号和可用区:

# 节点示例:打标签让节点归属对应 Flavor
# kubectl label node gpu-node-1 node.kubernetes.io/gpu-model=nvidia-A100-SXM4-80GB
# kubectl label node gpu-node-2 node.kubernetes.io/gpu-model=nvidia-H100-SXM-80GB

2.2 ClusterQueue:集群级资源池

ClusterQueue(简称 CQ)是 Kueue 中最核心的资源抽象,它定义了集群中一个资源池的配额和调度策略。ClusterQueue 是集群作用域的(cluster-scoped),可以被多个命名空间共享。

apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: team-ai-cluster-queue
spec:
  # 资源组:定义该队列管理的资源及其 Flavor
  resourceGroups:
    - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
      flavors:
        - name: "nvidia-a100-80gb"  # 引用 ResourceFlavor
          resources:
            - name: "cpu"
              nominalQuota: 36        # 名义配额:独占资源量
              borrowingLimit: 18      # 可借用上限(从 Cohort)
            - name: "memory"
              nominalQuota: "144Gi"
              borrowingLimit: "72Gi"
            - name: "nvidia.com/gpu"
              nominalQuota: 8
              borrowingLimit: 4
  # 队列策略
  queueingStrategy: BestEffortFIFO
  # Fair Sharing 权重(优先级)
  nominalUsageRatio: 1.5

关键字段解析:

  • nominalQuota:该 ClusterQueue 的名义配额,即独占资源量。在没有其他队列借用时,该队列最多使用这么多资源。
  • borrowingLimit:可借用配额上限。当本队列的名义配额用完时,可以从同 Cohort 的其他队列借用,但不超过这个上限。
  • queueingStrategy:排队策略。StrictFIFO 严格按优先级和创建时间排队(老任务阻塞新任务);BestEffortFIFO 允许新任务"插队"如果它能 fit(默认策略)。
  • nominalUsageRatio:用于 Fair Sharing 权重,数值越小优先级越高。

2.3 LocalQueue:命名空间级入口

LocalQueue(简称 LQ)是 ClusterQueue 在命名空间级别的"视图",是用户提交 Job 的入口。每个 LocalQueue 绑定到一个 ClusterQueue,Workload 提交到 LocalQueue 后,会自动进入对应 ClusterQueue 的调度队列。

apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
  name: training-queue
  namespace: team-ai  # 命名空间级别
spec:
  clusterQueue: team-ai-cluster-queue  # 绑定到哪个 ClusterQueue

为什么要引入 LocalQueue 这层抽象?

  1. 权限隔离:ClusterQueue 是集群级别的,普通用户无权直接操作。LocalQueue 允许运维预先创建好队列,普通用户只需要提交到自己的 LocalQueue。
  2. 命名空间绑定:一个 LocalQueue 只能属于一个命名空间,避免跨命名空间混乱。
  3. 默认队列kueue.x-k8s.io/default-local-queue: "true" 注解可以让该命名空间的所有 Job 自动路由到该队列,用户无需每次指定队列名。

2.4 Workload:调度的基本单元

Workload 是 Kueue 的调度单元,对应一个待调度的作业请求。当用户提交一个 Kubernetes Job(或 PyTorchJob 等)时,Kueue 的 webhook 会自动创建一个对应的 Workload 对象:

apiVersion: kueue.x-k8s.io/v1beta2
kind: Workload
metadata:
  name: training-job-xyz-uid
  namespace: team-ai
spec:
  # 资源请求:Job 中所有 Pod 的资源需求汇总
  podSets:
    - count: 4                    # Pod 数量
      template:
        spec:
          containers:
            - name: trainer
              resources:
                requests:
                  cpu: "8"
                  memory: "32Gi"
                  nvidia.com/gpu: "1"
  # Workload 优先级(影响调度顺序)
  priorityClassName: high-priority
  priority: 100000

Workload 的生命周期:

  1. Created:Job 被提交,webhook 自动创建 Workload,状态为 Created
  2. Queued:Workload 进入 ClusterQueue 调度队列,等待资源。
  3. AdmittedPendingAdmitted,Kueue 通过 webhook 将 Job 的 .spec.suspendtrue 改为 false,Pod 开始真正调度。
  4. Finished:Job 完成后 Workload 进入 Finished 状态,释放配额。

核心机制——Suspend 方案: Kueue 不使用污点/亲和性来控制调度,而是通过修改 Job 的 .spec.suspend 字段来控制 Pod 是否启动。初始状态下 Job 的 suspend 为 true(webhook 注入),Kueue 准入后将 suspend 改为 false,Pod 才进入调度队列。这种方案的优点是对 kube-scheduler 完全透明,无需修改调度器逻辑。

2.5 Cohort:资源分组的艺术

Cohort 是将多个 ClusterQueue 组合成一个资源池的关键机制。属于同一 Cohort 的 ClusterQueue 可以互相借用对方的空闲配额,实现动态资源 Fair Sharing

# ClusterQueue A:team-a 的队列
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: team-a-cq
spec:
  cohort: team-research  # 同 Cohort 名,所有成员共享资源池
  resourceGroups:
    - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
      flavors:
        - name: "nvidia-a100-80gb"
          resources:
            - name: "cpu"
              nominalQuota: 36
              borrowingLimit: 18  # 最多从 Cohort 借用 18 CPU
            - name: "memory"
              nominalQuota: "144Gi"
              borrowingLimit: "72Gi"
            - name: "nvidia.com/gpu"
              nominalQuota: 8
              borrowingLimit: 4
  queueingStrategy: BestEffortFIFO

---
# ClusterQueue B:team-b 的队列,同一个 Cohort
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: team-b-cq
spec:
  cohort: team-research  # 与 team-a 同一 Cohort
  resourceGroups:
    - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
      flavors:
        - name: "nvidia-a100-80gb"
          resources:
            - name: "cpu"
              nominalQuota: 36
            - name: "memory"
              nominalQuota: "144Gi"
            - name: "nvidia.com/gpu"
              nominalQuota: 8
  queueingStrategy: BestEffortFIFO

借用语义详解:

假设 team-a-cq 和 team-b-cq 各有 8 块 GPU nominalQuota,且 borrowingLimit 均为 4。

  • 场景 1:team-a 提交了 8 个任务用了 8 块 GPU,team-b 提交了 4 个任务。此时 team-b 的任务可以借用 team-a 的 4 块空闲 GPU,共使用 8 块。
  • 场景 2:team-a 需要用满 16 块 GPU(8 自有 + 8 借用),Kueue 检测到 team-b 有 pending 任务时,会触发抢占:将 team-a 借用来的 4 块 GPU 归还给 team-b,确保 team-b 的任务能调度。
  • 场景 3:team-b 的 nominalQuota 为 0(只能借用),team-a 有 8 块空闲 GPU,此时 team-b 可以借用最多 4 块(受 borrowingLimit 限制)。

这种机制完美解决了开头提到的 Fair Sharing 问题——队列不会饿死,借来的资源有借有还。

2.6 Preemption:抢占的艺术

Preemption(抢占)是 Kueue 实现资源弹性分配的关键机制。当一个高优先级 Workload 无法在当前可用配额下调度时,Kueue 可以主动终止(evict)一个或多个低优先级 Workload 来腾出资源。

抢占的三种策略:

  1. WithinClusterQueue:在同一 ClusterQueue 内部抢占。发生在高优先级 Workload 无法 fit 但低优先级 Workload 持有可被回收的资源时。
  2. WithinCohort:在同 Cohort 的 ClusterQueue 之间抢占。当一个 ClusterQueue 需要资源但当前配额不足,而同 Cohort 的另一个队列有可借用资源(且其所有者当前有 pending Workload)时触发。
  3. BorrowFromOwn:从自己的 borrowingLimit 中抢占。当一个 ClusterQueue 借用了 Cohort 资源,现在资源所有者需要时,被借方需要释放资源。

抢占策略通过 WorkloadPriorityClass 控制:

apiVersion: kueue.x-k8s.io/v1beta2
kind: WorkloadPriorityClass
metadata:
  name: high-priority
value: 100000          # 数值越高优先级越高
preemptionPolicy: Preempt // 还有 Daemon、Never 选项
description: "Production training jobs"
---
apiVersion: kueue.x-k8s.io/v1beta2
kind: WorkloadPriorityClass
metadata:
  name: low-priority
value: 100
preemptionPolicy: Preempt
description: "Experiment jobs"

注意: 抢占只会发生在 Workload 持有的资源超过其名义配额时。如果一个 Workload 只用了自己的 nominalQuota,抢占不会发生。这确保了"名义配额"的资源至少是安全的。


三、架构深度解析:Kueue 的内部工作机制

3.1 整体架构

Kueue 的架构可以用"五层同心圆"来理解:

┌─────────────────────────────────────────┐
│         User Layer (提交 Job)            │
├─────────────────────────────────────────┤
│         Webhook Layer (Mutating)        │  ← Job → Workload 转换
│         (Job/mutating webhook)           │     suspend=true 注入
├─────────────────────────────────────────┤
│         Kueue Controller Layer           │
│  ┌─────────────────────────────────────┐ │
│  │  Job Controller  (watch Job → WL)  │ │
│  │  Workload Controller (调度队列)     │ │
│  │  Admission Controller (准入控制)     │ │
│  │  Quota Reservation (配额预留)        │ │
│  └─────────────────────────────────────┘ │
├─────────────────────────────────────────┤
│         Scheduler Layer                  │  ← Kueue 内置调度器
│         (FIFO / Fair Sharing)            │
├─────────────────────────────────────────┤
│         Kube-scheduler                   │  ← Pod 级别的调度
│         (Pod scheduling)                 │     不感知 Kueue
└─────────────────────────────────────────┘

3.2 Webhook 机制详解

Kueue 使用 Kubernetes 的 Mutating Admission Webhook 机制来拦截 Job 的创建请求。这个过程对用户完全透明:

第一步:用户提交 Job

apiVersion: batch/v1
kind: Job
metadata:
  name: training-job
  namespace: team-ai
  labels:
    kueue.x-k8s.io/queue-name: training-queue  # 可选:指定 LocalQueue
spec:
  parallelism: 4
  completions: 4
  template:
    spec:
      containers:
        - name: trainer
          image: pytorch/pytorch:2.1.0
          resources:
            requests:
              cpu: "8"
              memory: "32Gi"
              nvidia.com/gpu: "1"
      restartPolicy: Never

第二步:Webhook 拦截并修改

Kueue 的 Job/mutating webhook 拦截到这个 Job,执行以下操作:

  1. 检查 kueue.x-k8s.io/queue-name 标签,确定目标 LocalQueue
  2. 将 Job 的 .spec.suspend 设置为 true(Pod 暂停调度)
  3. 添加 annotations 记录原始 suspend 值
  4. 如果未指定队列名且命名空间有默认 LocalQueue,自动注入队列名
# Webhook 修改后的 Job spec
spec:
  suspend: true  # ← 被 webhook 改为 true,Pod 暂停调度
  # 其他字段不变

第三步:Job Controller 创建 Workload

Kueue 的 Job Controller 监听 Job 资源变化。当检测到一个新的非 suspend Job(或一个 Job 的 Workload 尚未创建)时,自动创建对应的 Workload:

Job (suspend=true, queued by webhook)
  ↓ Job Controller watch
Workload (包含资源请求信息)
  ↓ Workload Controller 放入调度队列
调度队列 (ClusterQueue 中排队)
  ↓ 配额满足 + 优先级最高
Admission: suspend=false
  ↓
Pod 开始调度

3.3 Kueue Controller 的四大模块

模块一:Job Controller

Job Controller 负责监听 Kubernetes Job(或 PyTorchJob、TFJob 等受支持的 CRD)资源,并根据 Job 的状态管理对应的 Workload:

  • Job 创建 → 创建 Workload
  • Job 规格变更 → 更新 Workload
  • Job 完成/删除 → 完成/删除 Workload

Kueue 通过聚合 API(Aggregated API) 注册到 kube-apiserver,不需要直接 watch etcd,保证了良好的可扩展性。

模块二:Workload Controller

Workload Controller 维护所有 Workload 的状态,并负责调度决策:

  • 维护 ClusterQueue 的资源使用状态
  • 计算每个 Workload 的配额预留(Quota Reservation)
  • 决定 Workload 是否可以进入 "Admitted" 状态
  • 处理超时和重新入队逻辑

模块三:Quota Reservation(配额预留)

配额预留是 Kueue 调度器的核心算法。当一个 Workload 在队列中等待调度时,Kueue 会尝试为它预留配额:

  1. 在所有满足条件的 ClusterQueue 中选择一个(按 Fair Sharing 权重)
  2. 在该 ClusterQueue 的资源组中选择一个可用的 Flavor
  3. 检查该 Flavor 的 nominalQuota 是否足够,或可以通过 borrowing 满足
  4. 如果满足,预留配额并设置 Workload 状态为 QuotaReserved
  5. 如果不满足,Workload 保持在 Pending 状态

配额预留使用了乐观锁机制:预留时记录时间戳,如果有更高优先级的 Workload 到达,预留可能被撤销(reclaim)。

模块四:Admission Controller

Admission Controller 负责将 Workload 从 "QuotaReserved" 变为 "Admitted":

  1. 检查 Workload 的所有 AdmissionCheck(如果有)是否通过
  2. 通过后,通过 Job/mutating webhook 将 Job 的 .spec.suspend 设置为 false
  3. Pod 开始调度,Workload 状态更新为 Admitted
  4. 定期检查 Pod 是否真正完成调度,如果没有则回退

3.4 与 kube-scheduler 的关系

这是 Kueue 最容易混淆的点:Kueue 和 kube-scheduler 是什么关系?

简单回答:互补,不重叠。

  • kube-scheduler:Pod 级别的调度,决定 Pod 应该调度到哪个具体节点。
  • Kueue:Job/Workload 级别的调度,决定 Job 是否可以启动(Admission),以及在哪个资源池中运行。
时间线:
[Job 提交]
    ↓
[Kueue Webhook: suspend=true]
    ↓
[Workload 创建,进入调度队列]
    ↓
[Kueue 调度: QuotaReserved]
    ↓
[Kueue 调度: Admission (suspend=false)]
    ↓
[Kube-scheduler: Pod → Node 调度]
    ↓
[Pod Running]

可以理解为:Kueue 是 Job 的"入场检票员",kube-scheduler 是 Pod 的"座位分配员"。入场检票员决定谁可以进剧场,座位分配员决定谁坐哪个座位。

3.5 AdmissionCheck:准入检查的扩展点

AdmissionCheck 是 Kueue 为外部系统提供的一个扩展点,允许外部组件影响 Workload 的准入时机。典型应用场景包括:

  • ProvisioningRequest:调用云服务商按需创建 GPU 节点(Autopilot),节点创建完成后再准入 Workload
  • GenericManaged Resource:检查外部资源管理器(如 HAMi vGPU 调度器)的资源状态
  • 自定义检查:例如检查 Spot 实例价格是否在阈值内,数据是否准备就绪等
apiVersion: kueue.x-k8s.io/v1beta2
kind: AdmissionCheck
metadata:
  name: check-gpu-available
spec:
  controllerName: kueue.x-k8s.io/admission-check-controller
  retryDelayMinutes: 10  # 失败后重试间隔
  strategy: Queueing  # 或 "Immediate":失败后立即重试
status:
  conditions:
    - type: Ready
      status: "True"

四、完整代码实战:从零搭建 Kueue 环境

4.1 环境准备

本文使用 Kind(Kubernetes in Docker)在本地搭建演示环境。如果已有真实集群,可跳过安装步骤。

# 安装 Kind
brew install kind  # macOS
# 或: wget https://github.com/kubernetes-sigs/kind/releases/download/v0.20.0/kind-linux-amd64

# 创建 Kind 集群(需要开启 API Server 的 AdmissionWebhookConfig 特性)
kind create cluster --name kueue-demo --config - <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  kubeadmConfigPatches:
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "node.kubernetes.io/gpu-model=nvidia-A100-SXM4-80GB"
EOF

# 确认集群可用
kubectl cluster-info

4.2 安装 Kueue

推荐使用 Helm 安装 Kueue(稳定版):

# 添加 Kueue Helm 仓库
helm repo add kueue https://kueue.sigs.k8s.io/charts
helm repo update

# 安装 Kueue(包含 CRD、webhook、controller)
helm install kueue kueue/kueue \
  --namespace kueue-system \
  --create-namespace \
  --version 0.10.0

# 确认 Pod 运行正常
kubectl get pods -n kueue-system
# NAME                      READY   STATUS    RESTARTS   AGE
# kueue-7f4f9c8d7b-abcde     1/1     Running   0          30s

如果你是开发者,也可以使用源代码安装:

# 克隆源码
git clone https://github.com/kubernetes-sigs/kueue.git
cd kueue

# 使用 make 安装(包含 CRD + 部署)
make deploy

4.3 安装 kueuectl(Kueue CLI)

# macOS/Linux 一键安装
kubectl krew install deque
kubectl krew install add deque https://kueue.sigs.k8s.io/krew-index/deque
# 实际版本中安装命令如下:
# 使用 krew 安装
kubectl krew install add kueue https://kueue.sigs.k8s.io/krew-index/kueue

# 或者直接下载二进制
curl -LO "https://github.com/kubernetes-sigs/kueue/releases/download/v0.10.0/kueue-v0.10.0-linux-amd64.tar.gz"
tar -xzf kueue-*.tar.gz
sudo mv ./kubectl-kueue /usr/local/bin/

安装后可以用 kueuectl 快速创建 Kueue 资源:

# 查看 Kueue 版本
kueuectl version

# 查看所有 ClusterQueue 状态
kueuectl get clusterqueue

# 查看所有 Workload
kueuectl get workload -A

4.4 示例一:基础资源配置(CPU 节点)

步骤 1:创建 ResourceFlavor

# resource-flavor-cpu.yaml
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: cpu-node  # 标签匹配名
spec:
  nodeLabels:
    node.kubernetes.io/node-type: cpu-only
kubectl apply -f resource-flavor-cpu.yaml

步骤 2:创建 ClusterQueue(含 CPU/Memory 配额)

# cluster-queue-cpu.yaml
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: cpu-pool-cq
spec:
  # 匹配所有命名空间(可用 namespaceSelector 限制)
  namespaceSelector: {}
  # 资源组:一个 CPU 节点的资源定义
  resourceGroups:
    - coveredResources: ["cpu", "memory"]
      flavors:
        - name: "cpu-node"
          resources:
            # 最多同时运行 18 个 CPU
            - name: "cpu"
              nominalQuota: 18
              # borrowingLimit: 9  # 可选:允许借用同 Cohort 其他队列 9 CPU
            # 最多同时使用 72 Gi 内存
            - name: "memory"
              nominalQuota: "72Gi"
  # 默认队列策略:BestEffortFIFO
  queueingStrategy: BestEffortFIFO
kubectl apply -f cluster-queue-cpu.yaml

# 查看队列状态(ResourceFlavor 列显示已分配/总配额)
kueuectl describe clusterqueue cpu-pool-cq

步骤 3:创建 LocalQueue(team-a 命名空间)

# 创建演示命名空间
kubectl create namespace team-a
# local-queue-team-a.yaml
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
  name: team-a-queue
  namespace: team-a
spec:
  clusterQueue: cpu-pool-cq  # 绑定到上面的 ClusterQueue
kubectl apply -f local-queue-team-a.yaml

# 确认 LocalQueue 已就绪
kubectl get localqueue -n team-a
# NAME           CLUSTERQUEUE    PENDING   ACTIVE   SUCCEEDED   FAILED
# team-a-queue   cpu-pool-cq    0         0        0           0

步骤 4:提交第一个 Job 并观察行为

# job-batch.yaml
apiVersion: batch/v1
kind: Job
metadata:
  name: batch-training-1
  namespace: team-a
  labels:
    # 关键:指定目标 LocalQueue
    kueue.x-k8s.io/queue-name: team-a-queue
spec:
  parallelism: 3            # 同时运行 3 个 Pod
  completions: 3            # 完成后总共 3 个 Pod
  backoffLimit: 0           # 不重试
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: trainer
          image: busybox
          command: ["sh", "-c", "echo 'Training...'; sleep 300"]
          resources:
            requests:
              cpu: "2"
              memory: "4Gi"
            limits:
              cpu: "2"
              memory: "4Gi"
kubectl apply -f job-batch.yaml

# 立即观察:Job 的 suspend 字段已被 webhook 改为 true
kubectl get job -n team-a batch-training-1 -o jsonpath='{.spec.suspend}'  # 应输出 true

# 查看 Workload 是否自动创建
kubectl get workload -n team-a
# NAME                              CLUSTERQUEUE   WORKLOAD PRIORITY   QUEUE NAME      ADMITTED   AGE
# batch-training-1-user           cpu-pool-cq    100000             team-a-queue   false      5s

# 等待 Kueue 调度(几秒内应该发生)
kubectl get workload -n team-a -w
# 观察 PENDING → ADMITTED 的状态变化

# 确认 Job 的 suspend 被改为 false,Pod 开始运行
kubectl get pods -n team-a
# NAME                     READY   STATUS    RESTARTS   AGE
# batch-training-1-abcde   0/1     Running   0          10s
# batch-training-1-fghij   0/1     Running   0          10s
# batch-training-1-klmno   0/1     Running   0          10s

4.5 示例二:多租户 GPU 集群与资源竞争

场景设置: 两个团队(team-a 和 team-b)共享 8 块 A100 GPU,各有 4 块 nominalQuota,最多可借用对方 2 块。

步骤 1:创建 GPU ResourceFlavor

# resource-flavor-gpu.yaml
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: nvidia-a100-80gb
spec:
  nodeLabels:
    # 实际节点需要有此标签:
    # kubectl label node <node-name> node.kubernetes.io/gpu-model=nvidia-A100-SXM4-80GB
    node.kubernetes.io/gpu-model: nvidia-A100-SXM4-80GB

步骤 2:创建两个 ClusterQueue 并加入同一 Cohort

# team-a-cluster-queue.yaml
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: team-a-gpu-cq
spec:
  cohort: gpu-research  # ← 关键:同 Cohort,共享 GPU 池
  namespaceSelector: {}  # 允许所有命名空间(实际生产应限制)
  resourceGroups:
    - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
      flavors:
        - name: "nvidia-a100-80gb"
          resources:
            - name: "cpu"
              nominalQuota: 32
              borrowingLimit: 16  # 可从 Cohort 借用最多 16 CPU
            - name: "memory"
              nominalQuota: "256Gi"
              borrowingLimit: "128Gi"
            - name: "nvidia.com/gpu"
              nominalQuota: 4     # team-a 独占 4 块 A100
              borrowingLimit: 2    # 最多再借用 2 块(共 6 块)
  queueingStrategy: BestEffortFIFO

---
# team-b-cluster-queue.yaml
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: team-b-gpu-cq
spec:
  cohort: gpu-research  # ← 与 team-a 同一 Cohort
  namespaceSelector: {}
  resourceGroups:
    - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
      flavors:
        - name: "nvidia-a100-80gb"
          resources:
            - name: "cpu"
              nominalQuota: 32
              borrowingLimit: 16
            - name: "memory"
              nominalQuota: "256Gi"
              borrowingLimit: "128Gi"
            - name: "nvidia.com/gpu"
              nominalQuota: 4     # team-b 也独占 4 块 A100
              borrowingLimit: 2
  queueingStrategy: BestEffortFIFO
kubectl apply -f team-a-cluster-queue.yaml
kubectl apply -f team-b-cluster-queue.yaml

# 查看队列状态,确认 Cohort 配置
kueuectl get clusterqueue
# NAME             COHORT       WORKLOADS*   PENDING   ACTIVE   Admitted    AGE
# team-a-gpu-cq   gpu-research    0           0         0       0/4         5s
# team-b-gpu-cq   gpu-research    0           0         0       0/4         5s

步骤 3:创建 LocalQueue

kubectl create namespace team-a
kubectl create namespace team-b

# team-a 的队列
kubectl apply -f - <<'EOF'
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
  name: team-a-gpu-queue
  namespace: team-a
spec:
  clusterQueue: team-a-gpu-cq
EOF

# team-b 的队列
kubectl apply -f - <<'EOF'
apiVersion: kueue.x-k8s.io/v1beta2
kind: LocalQueue
metadata:
  name: team-b-gpu-queue
  namespace: team-b
spec:
  clusterQueue: team-b-gpu-cq
EOF

步骤 4:提交竞争任务,观察 Fair Sharing

# team-a 提交 4 个训练任务(各需 1 块 GPU)
for i in $(seq 1 4); do
  kubectl apply -f - <<EOF
apiVersion: batch/v1
kind: Job
metadata:
  name: team-a-training-$i
  namespace: team-a
  labels:
    kueue.x-k8s.io/queue-name: team-a-gpu-queue
spec:
  parallelism: 1
  completions: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: trainer
          image: nvidia/cuda:12.0-runtime-ubuntu22.04
          command: ["nvidia-smi", "||", "echo", "No GPU available"]
          resources:
            requests:
              nvidia.com/gpu: "1"
              cpu: "4"
              memory: "8Gi"
            limits:
              nvidia.com/gpu: "1"
EOF
done

# team-b 提交 4 个训练任务
for i in $(seq 1 4); do
  kubectl apply -f - <<EOF
apiVersion: batch/v1
kind: Job
metadata:
  name: team-b-training-$i
  namespace: team-b
  labels:
    kueue.x-k8s.io/queue-name: team-b-gpu-queue
spec:
  parallelism: 1
  completions: 1
  template:
    spec:
      restartPolicy: Never
      containers:
        - name: trainer
          image: nvidia/cuda:12.0-runtime-ubuntu22.04
          command: ["nvidia-smi", "||", "echo", "No GPU available"]
          resources:
            requests:
              nvidia.com/gpu: "1"
              cpu: "4"
              memory: "8Gi"
            limits:
              nvidia.com/gpu: "1"
EOF
done
# 观察资源分配情况
kueuectl get clusterqueue
# 应该看到:
# team-a-gpu-cq: Admitted 4/4(使用了全部 4 块 nominalQuota GPU)
# team-b-gpu-cq: Admitted 0/4(nominalQuota 已满,等待资源)

# team-b 的任务因资源不足处于 PENDING 状态
kubectl get workload -A
# NAMESPACE   NAME                  CLUSTERQUEUE      ADMITTED   AGE
# team-a      team-a-training-1     team-a-gpu-cq    true       30s
# team-a      team-a-training-2     team-a-gpu-cq    true       30s
# team-a      team-a-training-3     team-a-gpu-cq    true       30s
# team-a      team-a-training-4     team-a-gpu-cq    true       30s
# team-b      team-b-training-1     team-b-gpu-cq    false      25s
# team-b      team-b-training-2     team-b-gpu-cq    false      25s
# team-b      team-a-training-3     team-b-gpu-cq    false      25s
# team-b      team-b-training-4     team-b-gpu-cq    false      25s

# 当 team-a 完成部分任务后,team-b 可以借用空闲资源
# Kueue 会自动将 team-b 的 PENDING Workload 调度为 ADMITTED

4.6 示例三:集成 Kubeflow PyTorchJob

Kueue 原生支持 Kubeflow 的 PyTorchJob、TFJob、MPIJob 等 CRD。以下是与 PyTorchJob 的集成示例:

前置条件: 集群已安装 Kubeflow Training Operator

# 检查 Training Operator 是否安装
kubectl get pods -n kubeflow
# 如果未安装,可以快速安装
# kubectl apply -k "github.com/kubeflow/training-operator/manifests/overlays/standalone?ref=v1.8.0"

步骤 1:创建 PyTorchJob 的 LocalQueue 配置

PyTorchJob 需要使用 Kueue 的 batch/v1 Job 风格的集成——通过 labels 注解队列名:

# pytorch-job-distributed.yaml
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: pytorch-distributed-training
  namespace: team-a
  labels:
    kueue.x-k8s.io/queue-name: team-a-gpu-queue  # ← 关键:指定 Kueue 队列
spec:
  # PyTorchJob 配置:1 个 Master + 2 个 Worker
  # 每个角色 1 块 GPU,共 3 块
  torchReplicaSpecs:
    Master:
      replicas: 1
      template:
        spec:
          containers:
            - name: pytorch
              image: pytorch/pytorch:2.1.0
              args:
                - "-m"
                - "torch.distributed.run"
                - "--master_addr"
                - "$(PYTORCH_MASTER_ADDR)"
                - "--nnodes=3"
                - "--node_rank=0"
                - "--nproc_per_node=1"
                # 实际训练请替换为真实训练脚本
                - "echo 'Master started'; sleep 3600"
              env:
                - name: PYTORCH_MASTER_ADDR
                  value: "pytorch-distributed-training-master.0"
              resources:
                requests:
                  nvidia.com/gpu: "1"
                  cpu: "4"
                  memory: "16Gi"
                limits:
                  nvidia.com/gpu: "1"
          restartPolicy: Never
    Worker:
      replicas: 2
      template:
        spec:
          containers:
            - name: pytorch
              image: pytorch/pytorch:2.1.0
              args:
                - "-m"
                - "torch.distributed.run"
                - "--master_addr"
                - "$(PYTORCH_MASTER_ADDR)"
                - "--nnodes=3"
                - "--node_rank=$(MY_NODE_RANK)"
                - "--nproc_per_node=1"
                - "echo 'Worker started'; sleep 3600"
              env:
                - name: PYTORCH_MASTER_ADDR
                  value: "pytorch-distributed-training-master.0"
                - name: MY_NODE_RANK
                  valueFrom:
                    fieldRef:
                      fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
              resources:
                requests:
                  nvidia.com/gpu: "1"
                  cpu: "4"
                  memory: "16Gi"
                limits:
                  nvidia.com/gpu: "1"
          restartPolicy: Never
kubectl apply -f pytorch-job-distributed.yaml

# 观察 PyTorchJob 对应的 Workload
kubectl get workload -n team-a
# NAME                                   CLUSTERQUEUE      ADMITTED   AGE
# pytorch-distributed-training-master    team-a-gpu-cq    false      10s
# pytorch-distributed-training-worker-0  team-a-gpu-cq    false      10s
# pytorch-distributed-training-worker-1  team-a-gpu-cq    false      10s

重要提示:PyTorchJob 的 Workload 数量取决于角色数量。 Kueue 会为每个 Pod replica(Master + 每个 Worker)各创建一个 Workload。这些 Workload 全部被 admitted 后,分布式训练才能正式开始。


五、生产级最佳实践

5.1 多租户资源隔离设计

原则:Namespace + ClusterQueue + namespaceSelector 三层隔离

Tenant A Namespace (team-a)
  └── LocalQueue (team-a-cq)  →  ClusterQueue A (nominalQuota=16 GPU)
        └── Workload 1, 2, 3...

Tenant B Namespace (team-b)
  └── LocalQueue (team-b-cq)  →  ClusterQueue B (nominalQuota=16 GPU)
        └── Workload 4, 5, 6...

Cohort: team-research (A + B 可以互相借用,borrowingLimit 控制上限)

命名空间级配额限制(推荐生产使用):

# 限制 team-a 命名空间只能使用 team-a-gpu-cq
apiVersion: kueue.x-k8s.io/v1beta2
kind: ClusterQueue
metadata:
  name: team-a-gpu-cq
spec:
  # 精确限制:只有 team-a 命名空间可以使用这个队列
  namespaceSelector:
    matchLabels:
      kubernetes.io/metadata.name: team-a
  # ... resourceGroups ...

5.2 GPU 集群配额管理细节

GPU 配额管理有几个容易踩坑的点:

点一:GPU 资源类型

NVIDIA GPU 使用 nvidia.com/gpu 资源名;AMD GPU 使用 amd.com/gpu;HAMi 虚拟 GPU 使用厂商自定义资源(如 huawei.com/gpu)。确保 ClusterQueue 中的 coveredResources 包含正确的资源名。

点二:GPU 节点的污点和容忍

GPU 节点通常有污点防止普通 Pod 调度:

# 检查 GPU 节点的污点
kubectl get nodes <gpu-node> -o jsonpath='{.spec.taints}'
# 通常是:nvidia.com/gpu:NoSchedule

# ResourceFlavor 需要对应声明容忍
apiVersion: kueue.x-k8s.io/v1beta2
kind: ResourceFlavor
metadata:
  name: nvidia-a100-80gb
spec:
  nodeLabels:
    node.kubernetes.io/gpu-model: nvidia-A100-SXM4-80GB
  tolerations:
    # Workload 使用这个 Flavor 时,自动继承此容忍
    - key: "nvidia.com/gpu"
      operator: "Exists"
      effect: "NoSchedule"

点三:节点亲和性自动注入

Kueue 的 webhook 还可以自动为 Pod 添加节点亲和性,确保 Pod 调度到对应 ResourceFlavor 的节点上。启用方式是设置 Feature Gate(或在 Kueue Config 中配置)。

5.3 队列优先级设置

通过 WorkloadPriorityClass 设置作业优先级:

# workload-priority-classes.yaml
apiVersion: kueue.x-k8s.io/v1beta2
kind: WorkloadPriorityClass
metadata:
  name: production-training
value: 1000000
preemptionPolicy: Preempt
description: "Production model training - highest priority"

---
apiVersion: kueue.x-k8s.io/v1beta2
kind: WorkloadPriorityClass
metadata:
  name: experiment-training
value: 100
preemptionPolicy: Preempt
description: "Experiment training - can be preempted"

在 Job 中使用:

spec:
  template:
    spec:
      # 方式 1:直接指定 WorkloadPriorityClass
      priorityClassName: production-training
      # 注意:priorityClassName 对应的是 Kubernetes Pod PriorityClass
      # Kueue 的 WorkloadPriorityClass 是独立于 Pod Priority 的另一套优先级系统

两种优先级系统的区别:

  • Pod PriorityClassspec.priorityClassName,影响 Pod 调度优先级(调度器层面)
  • WorkloadPriorityClass:Kueue 专用,影响 Workload 在队列中的排队顺序和抢占能力

两者可以独立使用,也可以配合使用。

5.4 监控与可观测性

Kueue 内置了丰富的 Prometheus metrics,零配置开箱即用:

# 安装 Prometheus(如果尚未安装)
helm install prometheus prometheus-community/prometheus \
  -n prometheus --create-namespace

# Kueue 的 metrics 通过 Service 暴露
kubectl get svc -n kueue-system
# NAME          TYPE        CLUSTER-IP    PORT(S)
# kueue-metrics-service  ClusterIP  10.96.x.x   8080/TCP

# 直接查看 metrics
kubectl port-forward -n kueue-system svc/kueue-metrics-service 8080 &
curl http://localhost:8080/metrics | grep "kueue_"

# 关键指标解读:
# kueue_cluster_queue_pending_workloads{cluster_queue="team-a-gpu-cq"}  # 队列中等待的任务数
# kueue_workloads_admitted_total{cluster_queue="team-a-gpu-cq"}         # 累计准入的任务数
# kueue_workloads_evicted_total{cluster_queue="team-a-gpu-cq"}          # 累计抢占的任务数
# kueue_quota_reclaimings_total{flavor="nvidia-a100-80gb"}              # 配额回收次数

Grafana 仪表板推荐查询:

# 各队列 Pending Workload 数量
sum by (cluster_queue) (kueue_cluster_queue_pending_workloads)

# GPU 利用率(实际使用 / nominalQuota)
sum by (cluster_queue, flavor) (kueue_admitted_workload_quota_usage{resource="nvidia.com/gpu"})
  /
sum by (cluster_queue, flavor) (kueue_admitted_workload_quota{resource="nvidia.com/gpu"})

# 平均等待时间
rate(kueue_workloads_admitted_total[5m]) / rate(kueue_workloads_created_total[5m])

5.5 与 Volcano / YuniKorn 的选择指南

维度KueueVolcanoYuniKorn
定位Job Queueing(准入控制)批处理调度器(替换 scheduler)资源调度器
安装复杂度低(Helm 一键)高(需修改 scheduler 配置)
调度深度Job 级别准入Pod 级别调度(更精细)Pod 级别调度
Fair Sharing✅ Cohort 机制✅ 多队列 Fair Share✅ FIFO + Fair
Preemption✅ Workload 级别✅ Task 级别✅ 基于优先级
GPU 拓扑感知✅(Topology Aware)
Multi-Cluster✅(MultiKueue)⚠️ 需配合联邦框架
与现有系统冲突无(webhook 方案)需接管 scheduler需接管 scheduler
CNCF 状态沙箱项目毕业项目活跃项目

什么时候选 Kueue:

  • 需要最小侵入地引入 Job 队列管理
  • 已有完整的 kube-scheduler 配置,不想改动
  • 主要场景是 AI/ML 训练任务的准入控制
  • 需要与 Kubeflow、JobSet 等生态无缝集成

什么时候选 Volcano:

  • 需要 Job 内部的 Task 级别调度(如 Spark Executor 的动态扩缩容)
  • 需要 Gang Scheduling(所有 Pod 必须同时调度)
  • 需要与 ARGO Workflow、Tekton 等工作流引擎深度集成

什么时候选 YuniKorn:

  • 大数据场景为主(Spark、Flink、Presto)
  • 需要 Apache Hadoop YARN 风格的调度

5.6 常见坑与排错

坑一:Job 创建后 Workload 没有自动生成

原因:可能未启用 Kueue 的 Job 集成。

# 检查 kueue-config 中是否启用了 Job 集成
kubectl get configmap kueue-manager-config -n kueue-system -o yaml

# 或者手动创建 JobSet 配置启用
# kueue.x-k8s.io/integration.jobTemplateAnnotation: batch.kubernetes.io/job-template

坑二:Workload 一直 PENDING,不进入 ADMITTED

排查步骤:

# 1. 查看 Workload 详细信息
kubectl describe workload <workload-name> -n <namespace>

# 2. 检查 ClusterQueue 配额状态
kueuectl describe clusterqueue <clusterqueue-name>

# 3. 检查是否有可用的 ResourceFlavor 节点
kubectl get nodes -l node.kubernetes.io/gpu-model=nvidia-A100-SXM4-80GB

# 4. 检查是否有节点污点未处理
kubectl get nodes -o json | jq '.items[].spec.taints'

# 5. 检查 AdmissionCheck 状态(如果有)
kubectl get admissionchecks

坑三:Pod 已经 Running 但 Workload 状态还是 PENDING

Kueue 的 Admission Controller 会等待所有 Pod 真正调度到节点。如果部分 Pod 调度失败(资源不足、亲和性不满足等),Workload 不会变为 ADMITTED。解决方案:

# 查看 Pod 调度失败原因
kubectl describe pod <pod-name> -n <namespace>

# 常见原因:
# 1. 节点资源碎片化(Pod 申请 8 CPU,但每个节点只有 4 CPU 剩余)
#    → 拆分 Job parallelism,或增加节点
# 2. 节点亲和性不满足
#    → 检查 ResourceFlavor 标签是否与节点匹配
# 3. GPU 节点无污点容忍
#    → 在 ResourceFlavor 中添加 tolerations

坑四:Borrowing 不生效

确认 ClusterQueue 的 cohort 字段一致,且 borrowingLimit 不为 0。如果 Cohorted ClusterQueue 超过 100 个,Kueue 会有性能影响,请合理拆分 Cohort。


六、源码导读:Kueue 的核心实现

6.1 Job → Workload 转换逻辑

Kueue 的 Job Controller 位于 pkg/controller/jobs/ 目录。对于标准 Kubernetes Job,核心逻辑在 pkg/controller/jobs/job/job_controller.go

// 简化版核心逻辑
func (r *JobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
    // 1. 获取 Job
    job, err := r.GetJob(ctx, req.NamespacedName)
    
    // 2. 检查 Job 是否需要被 Kueue 管理
    if !isJobManaged(job) {
        return ctrl.Result{}, nil
    }
    
    // 3. 获取/创建 Workload
    wl, err := r.getOrCreateWorkload(ctx, job)
    
    // 4. 根据 Job 状态更新 Workload
    if job.IsSuspended() {
        // Job 暂停,Workload 应该是 PENDING 或 QuotaReserved
        r.handleSuspendedJob(ctx, wl)
    } else if job.IsFinished() {
        // Job 完成,标记 Workload 完成
        r.markWorkloadFinished(ctx, wl)
    }
    
    return ctrl.Result{}, nil
}

6.2 调度队列的实现

Kueue 的调度器位于 pkg/scheduler/ 目录。调度器的核心是一个优先级队列,按以下顺序排序:

  1. WorkloadPriorityClass value(数值越大越优先)
  2. 创建时间(相同优先级时,早创建先调度)
  3. PriorityClass(Pod 级别优先级,用于打破平局)
// 调度队列排序(pkg/scheduler/scheduler.go)
type workloadOrder struct{}

func (s workloadOrder) Less(a, b *kueue.Workload) bool {
    // 1. 优先级高的先调度
    if a.Spec.Priority != b.Spec.Priority {
        return a.Spec.Priority > b.Spec.Priority
    }
    // 2. 同优先级,早创建的先调度
    if !a.CreationTimestamp.Equal(&b.CreationTimestamp) {
        return a.CreationTimestamp.Before(&b.CreationTimestamp)
    }
    // 3. 名字字典序(最后手段)
    return a.Name < b.Name
}

6.3 Quota Reservation 的实现

配额预留是调度器中最复杂的部分,位于 pkg/scheduler/flavorassigner/pkg/scheduler/scheduler.go。核心算法如下:

// 伪代码:尝试为 Workload 预留配额
func attemptReservation(ctx context.Context, wl *kueue.Workload, cq *kueue.ClusterQueue) (bool, error) {
    for _, podSet := range wl.Spec.PodSets {
        // 为每个 PodSet 找一个匹配的 Flavor
        flavor, err := assignFlavor(podSet, cq)
        if err != nil {
            return false, err  // 无法满足,跳过
        }
        
        // 检查 nominalQuota 是否足够
        if !hasNominalQuota(cq, flavor, podSet) {
            // nominalQuota 不足,尝试 borrowing
            if !hasBorrowableQuota(cq, flavor, podSet) {
                return false, nil  // 借用也不行,无法预留
            }
            // 可以借用,标记为 borrowing
            markAsBorrowing(wl, flavor)
        }
    }
    
    // 所有 PodSet 都满足条件,预留成功
    reserveQuota(wl, cq)
    updateWorkloadStatus(wl, "QuotaReserved")
    return true, nil
}

七、总结与展望

7.1 Kueue 在 2026 年 AI 工作负载场景的价值

2025-2026 年,大模型训练、推理和多租户 AI 平台成为 Kubernetes 最重要的应用场景。Kueue 作为 SIG-Scheduling 官方出品的 Job Queueing 框架,正好切中了这些场景的核心需求:

多租户 AI 平台:Kueue 的 Cohort 机制让多个团队共享 GPU 集群成为可能——名义配额保证每个团队至少有多少资源,Fair Sharing 让闲置资源得到充分利用。这对于 GPU 资源紧张但使用率低下的企业来说,每年可节省数十万甚至数百万元的 GPU 成本。

AI 训练管道:Kueue 与 Kubeflow、JobSet 的原生集成,让训练任务的队列管理成为基础设施的一部分。团队无需手动管理作业提交顺序,Kueue 自动处理优先级、抢占和资源分配。

Hybrid Cloud:MultiKueue 功能支持跨集群调度,允许多个集群共享一个全局控制平面。配合集群联邦工具,可以实现真正的"GPU 资源按需调度"。

7.2 未来路线图

根据 Kueue 官方 KEP 和 GitHub Roadmap,值得关注的演进方向包括:

  • Dynamic Resource Allocation(DRA)集成:与 K8s 原生 DRA 深度集成,支持更灵活的 GPU 资源划分(单卡切分、多卡聚合)
  • Topology-Aware Scheduling 增强:更好地支持 LLMs 的 tensor parallelism 和 pipeline parallelism 场景
  • Multi-Cluster Federation:完善 MultiKueue,支持更复杂的多集群调度策略
  • Elastic Workloads:支持训练任务的动态伸缩,在保证 SLA 的前提下提高资源利用率

7.3 选择决策树

你的场景是什么?
├── 仅需要 Job 准入控制(排队 + 配额)
│   └── 选择 Kueue ✓
├── 需要 Gang Scheduling(Job 内所有 Pod 同时调度/失败)
│   └── 选择 Volcano
├── 主要是大数据工作负载(Spark/Flink/Presto)
│   └── 选择 YuniKorn
├── 需要替换/增强 kube-scheduler 调度策略
│   ├── 批处理为主 → Volcano
│   └── AI 训练为主 → Kueue + Volcano
└── 多集群 AI 训练
    └── Kueue MultiKueue

记住一点:Kueue 和 Volcano 并不互斥——你可以在一个集群中同时运行 Kueue(Job 准入层)和 Volcano(Gang Scheduling 层)。Kueue 负责"什么时候允许 Job 启动",Volcano 负责"Job 的 Pod 怎么调度到节点"。两者配合,可以构建一套完整的 AI 训练调度体系。


附:完整的 Kueue 核心 YAML 模板库

为方便读者快速复用,这里汇总了文中所有 YAML 模板的核心要素:

# 一、最小化 Kueue 安装清单(kind 快速验证)
# 1. helm install kueue ...
# 2. ResourceFlavor
# 3. ClusterQueue
# 4. LocalQueue
# 5. Job with queue-label

# 二、生产推荐配置要素
# - ClusterQueue: 限制 namespaceSelector
# - Cohort: 设置 borrowingLimit
# - ResourceFlavor: 对应真实节点标签 + tolerations
# - WorkloadPriorityClass: 设置优先级和 preemptionPolicy
# - Prometheus metrics: 监控队列状态和资源使用率

# 三、排错清单
# 1. kubectl get events --field-selector reason=QuotaReservation
# 2. kubectl get workload -A -o wide | grep PENDING
# 3. kueuectl describe clusterqueue <name>
# 4. kubectl get nodes -l <flavor-label>
# 5. 检查 Webhook 是否注册:kubectl get mutatingwebhookconfigurations

本文代码示例已在 Kind v0.20 + Kueue v0.10 环境中验证。如有问题,欢迎在评论区交流。

推荐文章

Roop是一款免费开源的AI换脸工具
2024-11-19 08:31:01 +0800 CST
html流光登陆页面
2024-11-18 15:36:18 +0800 CST
npm速度过慢的解决办法
2024-11-19 10:10:39 +0800 CST
记录一次服务器的优化对比
2024-11-19 09:18:23 +0800 CST
Golang 几种使用 Channel 的错误姿势
2024-11-19 01:42:18 +0800 CST
Python设计模式之工厂模式详解
2024-11-19 09:36:23 +0800 CST
阿里云发送短信php
2025-06-16 20:36:07 +0800 CST
在JavaScript中实现队列
2024-11-19 01:38:36 +0800 CST
Rust 与 sqlx:数据库迁移实战指南
2024-11-19 02:38:49 +0800 CST
程序员茄子在线接单