饮墨

子安饮墨馀三斗,留与卿儿作赋来

Kueue 实战:Kubernetes 原生批处理调度器,4 步搞定 AI/ML 工作负载队列管理

0 views

痛点:AI 训练任务抢 GPU,集群资源乱成一锅粥

跑过 AI/ML 训练任务的运维都知道这个痛:团队 A 提交了一个 8 卡 GPU 的大模型微调任务,团队 B 同时提交了 20 个数据预处理 Job,结果所有任务一起抢资源,谁都跑不动。更头疼的是:

  • 原生 Job 没有排队机制kubectl apply 之后 Pod 直接创建,不管资源够不够,Pending 一堆
  • 多团队资源配额靠 ResourceQuota 太粗糙:只能限制上限,不能做公平调度和借用
  • GPU 等昂贵资源没有优先级抢占:低优先级任务占着 GPU 不释放,高优先级任务干等
  • 批处理和在线服务混部时互相干扰:没有统一的准入控制

Kubernetes 原生的 kube-scheduler 擅长 Pod 到 Node 的调度,但对"这个 Job 该不该现在运行"这个问题,它管不了。这正是 Kueue 要解决的。

方案:Kueue — Kubernetes SIG 官方的作业队列管理器

Kueue 是 Kubernetes SIG Scheduling 下的官方子项目,不替换任何 K8s 原生组件,而是在 Job 创建和 Pod 调度之间加了一层"准入门控":

  • Job 挂起 → 提交到 Kueue 队列 → Kueue 判断配额和优先级 → 放行(unsuspend)→ Pod 创建 → kube-scheduler 正常调度

核心概念只有三个:

概念 作用 类比
ResourceFlavor 定义资源类型(如 gpu-a100gpu-t4spot-cpu 不同规格的服务器池
ClusterQueue 集群级队列,绑定资源配额和抢占策略 资源池 + 调度规则
LocalQueue 命名空间级队列,用户提交 Job 的入口 团队的提交窗口

工作流:用户在自己命名空间的 LocalQueue 提交 Job → LocalQueue 关联到 ClusterQueue → Kueue 按配额、优先级、公平分享策略决定准入顺序。

实操:4 步部署 Kueue 并管理 GPU 训练队列

第 1 步:安装 Kueue

# 推荐使用官方 release manifest,当前稳定版 v0.10.x
KUEUE_VERSION=v0.10.0
kubectl apply --server-side -f \
  https://github.com/kubernetes-sigs/kueue/releases/download/${KUEUE_VERSION}/manifests.yaml

# 验证安装
kubectl -n kueue-system get pods
# 应看到 kueue-controller-manager Running

第 2 步:定义 ResourceFlavor 和 ClusterQueue

# resource-flavors.yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: gpu-a100
spec:
  nodeLabels:
    cloud.google.com/gke-accelerator: nvidia-tesla-a100
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: default-cpu
spec: {}  # 匹配所有 CPU 节点
# cluster-queue.yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: ai-training-queue
spec:
  queueingStrategy: BestEffortFIFO  # 允许小任务插队
  preemption:
    reclaimWithinCohort: Any
    withinClusterQueue: LowerPriority
  resourceGroups:
  - coveredResources: ["cpu", "memory", "nvidia.com/gpu"]
    flavors:
    - name: gpu-a100
      resources:
      - name: "nvidia.com/gpu"
        nominalQuota: 16        # 常规配额 16 卡
        borrowingLimit: 8       # 最多再借 8 卡
      - name: "cpu"
        nominalQuota: 64
      - name: "memory"
        nominalQuota: 256Gi
    - name: default-cpu         # GPU 不够时退化到 CPU
      resources:
      - name: "cpu"
        nominalQuota: 128
      - name: "memory"
        nominalQuota: 512Gi

关键配置说明: - BestEffortFIFO:严格 FIFO 可能导致大任务阻塞整个队列,BestEffort 允许资源足够时小任务先跑 - borrowingLimit:配合 Cohort(队列组)实现跨团队资源借用 - Flavor 顺序有优先级:先尝试 gpu-a100,配额满了自动尝试 default-cpu(Flavor Fungibility)

第 3 步:创建 LocalQueue 并提交任务

# local-queue.yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  namespace: team-ml
  name: ml-jobs
spec:
  clusterQueue: ai-training-queue
# training-job.yaml
apiVersion: batch/v1
kind: Job
metadata:
  namespace: team-ml
  name: llm-finetune-0919
  labels:
    kueue.x-k8s.io/queue-name: ml-jobs  # 指定队列
spec:
  suspend: true  # 关键:必须以挂起状态提交,交给 Kueue 管理
  parallelism: 4
  completions: 4
  template:
    spec:
      containers:
      - name: trainer
        image: nvcr.io/nvidia/pytorch:24.05-py3
        command: ["torchrun", "--nproc_per_node=1", "train.py"]
        resources:
          requests:
            nvidia.com/gpu: 1
            cpu: "8"
            memory: "32Gi"
          limits:
            nvidia.com/gpu: 1
      restartPolicy: Never
kubectl apply -f resource-flavors.yaml
kubectl apply -f cluster-queue.yaml
kubectl apply -f local-queue.yaml
kubectl apply -f training-job.yaml

# 查看队列状态
kubectl get clusterqueue ai-training-queue -o wide
# 查看工作负载准入情况
kubectl get workloads -n team-ml

第 4 步:配置优先级抢占

# workload-priority.yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: WorkloadPriorityClass
metadata:
  name: production-training
value: 1000
description: "生产级模型训练,可抢占低优先级任务"
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: WorkloadPriorityClass
metadata:
  name: experiment
value: 100
description: "实验性训练,可被抢占"

在 Job 中通过 label 指定优先级:

metadata:
  labels:
    kueue.x-k8s.io/queue-name: ml-jobs
    kueue.x-k8s.io/priority-class: production-training

当高优先级任务到达但 GPU 配额已满时,Kueue 会自动挂起低优先级任务(delete pods),腾出资源给高优先级任务。被抢占的任务回到队列等待重新准入。

3 个常见坑

坑 1:Job 忘记设置 suspend: true

这是最常见的错误。如果 Job 提交时没有 suspend: true,Pod 会直接创建,完全绕过 Kueue 的准入控制,配额形同虚设。

解决方案:用 Kyverno 或 ValidatingAdmissionPolicy 强制检查:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-kueue-suspend
spec:
  validationFailureAction: Enforce
  rules:
  - name: check-suspend
    match:
      resources:
        kinds: ["Job"]
        namespaceSelector:
          matchLabels:
            kueue-managed: "true"
    validate:
      message: "带 kueue queue-name label  Job 必须设置 suspend: true"
      pattern:
        spec:
          suspend: true

坑 2:Cohort 借用导致资源超卖

多个 ClusterQueue 放在同一个 Cohort 中可以互相借用空闲配额,但如果 borrowingLimit 没设上限,一个团队的突发任务可能吃光整个 Cohort 的资源。

解决方案:每个 ClusterQueue 必须设置 borrowingLimit,同时配合 lendingLimit 控制出借上限:

# 最多借入 8 卡 GPU,最多出借 4 卡
- name: gpu-a100
  resources:
  - name: "nvidia.com/gpu"
    nominalQuota: 16
    borrowingLimit: 8
    lendingLimit: 4

坑 3:Cluster Autoscaler 扩容慢导致 Workload 超时

Kueue 准入放行后,如果节点不够,Pod 还是 Pending 等 Cluster Autoscaler 扩容。对于 GPU 节点,扩容可能需要 5-10 分钟。

解决方案:启用 ProvisioningRequest AdmissionCheck,让 Kueue 在准入前就触发预扩容:

apiVersion: kueue.x-k8s.io/v1beta1
kind: AdmissionCheck
metadata:
  name: provision-gpu-nodes
spec:
  controllerName: kueue.x-k8s.io/provisioning-request
  parameters:
    apiGroup: kueue.x-k8s.io
    kind: ProvisioningRequestConfig
    name: gpu-provision-config

配合 Karpenter 的 NodePool,可以将 GPU 节点预热时间从分钟级缩短到秒级。

总结

Kueue 解决了 Kubernetes 原生 Job 管理的核心缺失——作业级准入控制和队列调度。适用场景:

  • AI/ML 团队共享 GPU 集群:配额管理 + 优先级抢占 + 公平分享
  • 批处理密集型工作负载:数据 ETL、CI/CD 批量构建、科学计算
  • 混合工作负载集群:训练任务和推理服务在同一集群,Kueue 管批处理,原生 HPA 管在线服务

选型建议:如果你的 K8s 集群跑的是简单 CronJob,不需要 Kueue;但只要涉及 多团队共享 + GPU/昂贵资源 + 批量并行任务,Kueue 是目前 Kubernetes 生态里最正统的方案——SIG 官方维护,兼容所有主流 AI 训练框架(PyTorch、Kubeflow、Ray),生产可用。

分享:

相关文章