☰

1. 背景与问题描述

1.1 现状

  • 使用 GitLab Runner + Kubernetes Executor 构建 CI/CD 流水线
  • 集群资源:14 个固定节点 + 60 个弹性节点(弹性节点按需伸缩)
  • 任务并发高,资源经常被占满,大量 Pod 处于 Pending 状态

1.2 问题现象

当资源释放后,Kubernetes 默认调度器倾向于选择"资源请求最匹配"的 Pod 来调度,而非按照 Pod 创建时间(Pending 时长)顺序调度。这导致:

  • 新创建的 CI 任务更可能"插队"获得资源
  • 一开始就进入 Pending 的老任务长期无法运行
  • Merge Request 等待时间过长,严重阻碍迭代效率

1.3 目标

  • 实现先进入 Pending 的任务优先获得资源(FIFO 队列化)
  • 支持动态调整某个特定任务的优先级,允许紧急任务"插队"执行
  • 方案需具备可落地性、可维护性

2. 方案选型对比

方案描述优点缺点适用性
A. Kubernetes PriorityClass通过 Pod 优先级影响调度顺序原生支持、配置简单优先级静态,无法按等待时间动态调整;无法保证严格 FIFO仅适合区分高低优先级,无法解决"老任务饿死"问题
B. 自定义调度器开发自定义调度器,实现按等待时间排序的逻辑灵活性极高开发运维成本巨大不推荐,除非有专职团队
C. KueueKubernetes 社区官方作业队列管理系统原生支持 FIFO,提供 Workload 优先级动态调整能力,与 GitLab Runner 集成简单需额外部署组件最佳选择,满足所有需求
D. Volcano高性能批处理调度器功能丰富,支持 FIFO比 Kueue 更重,学习曲线陡峭适用于 AI/大数据场景,CI/CD 场景过于"重型"

结论

选择方案 C:Kueue,理由如下:

  • 社区官方项目(sig-autoscaling),持续维护
  • 开箱即用的 FIFO 队列策略
  • Workload 优先级支持动态修改,可随时调整 Pending 任务的优先级
  • 轻量级,与 Kubernetes 原生资源(Job、Pod)自然集成

3. 最终方案:Kueue 作业队列管理

3.1 架构概览

1GitLab Runner → 创建 Kubernetes Job → Kueue 捕获并放入队列
2                                            ↓
3                                    ClusterQueue(资源池)
4                                            ↓
5                            按优先级 + FIFO 顺序准入(Admission)
6                                            ↓
7                                  Pod 被调度到节点执行

3.2 核心组件

  • ClusterQueue:集群资源池(如 CPU、内存总量),定义可用的资源上限
  • LocalQueue:命名空间内的队列,关联到一个 ClusterQueue,用于任务排队
  • Workload:Kueue 为每个 Job 生成的内部对象,包含优先级、排队状态等
  • Admission:准入控制,决定哪个 Workload 可以获取资源并创建 Pod

3.3 工作流程

  1. GitLab Runner 创建一个 Kubernetes Job(由 config.toml 配置)。
  2. 若 Job 被标记为 suspend: true(Kueue 默认会处理),Kueue 将其转换为 Workload 放入队列。
  3. Workload 在 ClusterQueue 中按优先级(数值从高到低)和创建时间(FIFO)排序。
  4. 当资源可用时,队首的 Workload 被准入,其关联的 Job 被置为 suspend: false,进而创建 Pod 并调度执行。
  5. 支持手动修改 Workload 的优先级,改变其在队列中的顺序。

4. 实操步骤

4.1 环境准备

  • Kubernetes 集群(版本 ≥ 1.27)
  • 已安装 GitLab Runner(Kubernetes Executor)
  • 具有集群管理员权限

4.2 安装 Kueue

4.2.1 通过 Helm 安装

1# 添加 Kueue Helm 仓库
2helm repo add kueue https://kubernetes-sigs.github.io/kueue/
3helm repo update
4
5# 安装 Kueue(默认配置)
6helm install kueue kueue/kueue --namespace kueue-system --create-namespace

4.2.2 验证安装

1kubectl get pods -n kueue-system
2# 应看到 kueue-controller-manager-xxx 运行中

4.3 配置资源队列与 nominalQuota 策略

节点规模:14 个固定节点 + 60 个弹性节点,假设单节点规格为 8 核 CPU,32Gi 内存。

  • 固定节点总资源:112 核 CPU,448Gi 内存
  • 弹性节点总资源:480 核 CPU,1920Gi 内存
  • 集群总资源上限:592 核 CPU,2368Gi 内存

4.3.1 nominalQuota 设置策略

nominalQuota 是 Kueue 中表示"保底资源量"的字段,其值取决于管理策略。根据以上规模,有三种推荐策略:

策略nominalQuota 设置说明适用场景
策略一:保底策略固定节点总资源(112核/448Gi)仅承诺固定节点的资源,弹性节点按需扩容成本优先,容忍弹性扩容延迟
策略二:激进策略集群总资源上限(592核/2368Gi)视为可用上限,任务准入即触发弹性扩容性能优先,可接受较高云成本
策略三:Cohort 动态借用(推荐)固定节点队列:112核/448Gi;弹性节点队列:0核/0Gi(borrowingLimit=480核/1920Gi)分队列管理,固定节点用满后自动借用弹性节点额度兼顾成本与效率,推荐生产环境使用

建议:长期推荐策略三(Cohort 机制),这是 Kueue 官方推荐的最佳实践,能实现资源的动态调配和自动回收。

4.4 创建 ClusterQueue(以策略三为例)

4.4.1 创建固定节点 ClusterQueue

 1apiVersion: kueue.x-k8s.io/v1beta1
 2kind: ClusterQueue
 3metadata:
 4  name: fixed-cq
 5spec:
 6  namespaceSelector: {}
 7  resourceGroups:
 8    - coveredResources: ["cpu", "memory"]
 9      flavors:
10        - name: default-flavor
11          resources:
12            - name: cpu
13              nominalQuota: 112   # 14节点 × 8核
14            - name: memory
15              nominalQuota: 448Gi # 14节点 × 32Gi
16  cohort: ci-cohort  # 关联到同一 Cohort
17  preemption:
18    reclaimWithinCohort: Any
19    withinClusterQueue: LowerPriorityFirst

4.4.2 创建弹性节点 ClusterQueue(borrowingLimit 方式)

 1apiVersion: kueue.x-k8s.io/v1beta1
 2kind: ClusterQueue
 3metadata:
 4  name: spot-cq
 5spec:
 6  namespaceSelector: {}
 7  resourceGroups:
 8    - coveredResources: ["cpu", "memory"]
 9      flavors:
10        - name: default-flavor
11          resources:
12            - name: cpu
13              nominalQuota: 0
14              borrowingLimit: 480  # 60节点 × 8核,最大可借用额度
15            - name: memory
16              nominalQuota: 0
17              borrowingLimit: 1920Gi  # 60节点 × 32Gi
18  cohort: ci-cohort

4.4.3 创建 Cohort(队列组)

1apiVersion: kueue.x-k8s.io/v1beta1
2kind: Cohort
3metadata:
4  name: ci-cohort

应用所有资源:

1kubectl apply -f cohort.yaml
2kubectl apply -f fixed-cq.yaml
3kubectl apply -f spot-cq.yaml

其他策略配置参考:

  • 策略一(保底):仅创建单一 ClusterQueue,nominalQuota = 112核/448Gi
  • 策略二(激进):仅创建单一 ClusterQueue,nominalQuota = 592核/2368Gi

4.4.4 创建 LocalQueue(命名空间队列)

为 GitLab Runner 所在的命名空间(如 gitlab-runner)创建队列:

1apiVersion: kueue.x-k8s.io/v1beta1
2kind: LocalQueue
3metadata:
4  name: ci-queue
5  namespace: gitlab-runner
6spec:
7  clusterQueue: fixed-cq  # 关联到固定节点队列,任务将优先使用固定节点

应用:

1kubectl apply -f local-queue.yaml

4.5 集成 GitLab Runner

4.5.1 修改 Runner 配置(config.toml)

在 [[runners]] 下,添加或修改 [runners.kubernetes] 部分:

1[runners.kubernetes]
2  # ... 原有配置 ...
3  pod_annotations = ["kueue.x-k8s.io/queue-name: ci-queue"]

如果使用 Helm 部署 GitLab Runner,可在 values 中配置:

1runners:
2  config: |
3    [[runners]]
4      ...
5      executor = "kubernetes"
6      [runners.kubernetes]
7        pod_annotations = ["kueue.x-k8s.io/queue-name: ci-queue"]

4.5.2 验证

触发一个 CI 任务,检查其对应的 Job 是否被自动添加了注解,并查看 Workload 是否创建:

1kubectl get workload -n gitlab-runner

4.6 动态调整 Pending 任务的优先级(插队操作)

当某个紧急任务需要插队时,按以下步骤操作。

方法一:直接修改 Workload 的优先级(推荐)

  1. 查找目标 Workload(名称通常为 job-<job-name>-<hash>):

    1kubectl get workload -n gitlab-runner | grep <任务名或部分关键词>
    
  2. 编辑该 Workload:

    1kubectl edit workload <workload-name> -n gitlab-runner
    
  3. 修改 .spec.priority 字段,数值越大优先级越高(例如从默认 0 改为 100):

    1spec:
    2  priority: 100   # 原值可能为 0 或未设置
    

    保存退出后,Kueue 会立即按新优先级重新排序队列,该任务将被提前处理。

方法二:通过 Job 标签修改(更便捷)

  1. 挂起 Job:

    1kubectl patch job <job-name> -n gitlab-runner -p '{"spec":{"suspend":true}}'
    
  2. 添加/覆盖优先级标签(需事先定义 WorkloadPriorityClass,详见 4.7):

    1kubectl label job <job-name> -n gitlab-runner kueue.x-k8s.io/priority-class=high-priority --overwrite
    
  3. 恢复 Job:

    1kubectl patch job <job-name> -n gitlab-runner -p '{"spec":{"suspend":false}}'
    

4.7 定义 WorkloadPriorityClass(可选)

为了简化优先级管理,可定义命名优先级类别:

 1apiVersion: kueue.x-k8s.io/v1beta1
 2kind: WorkloadPriorityClass
 3metadata:
 4  name: high-priority
 5value: 100
 6description: "紧急任务"
 7---
 8apiVersion: kueue.x-k8s.io/v1beta1
 9kind: WorkloadPriorityClass
10metadata:
11  name: normal-priority
12value: 0
13description: "普通任务"

应用:

1kubectl apply -f workload-priority-class.yaml

4.8 监控与验证

  • 查看队列状态:

    1kubectl describe localqueue ci-queue -n gitlab-runner
    
  • 查看 Workload 排队情况:

    1kubectl get workload -n gitlab-runner -o wide
    
  • 查看准入日志:

    1kubectl logs -n kueue-system deployment/kueue-controller-manager
    
  • 查看 Cohort 资源借用情况:

    1kubectl describe cohort ci-cohort
    

5. 注意事项与最佳实践

  1. 资源配额规划:
    • 策略三(Cohort)中,nominalQuota 设置应基于确定性资源(固定节点),borrowingLimit 设置基于弹性资源上限。
    • 建议为每个 CI 任务的 Pod 设置合理的 resources.requests,避免单个任务过度消耗资源导致排队失衡。
    • 如果单节点规格不同,请相应调整上述数值。
  2. 抢占策略:若允许高优先级任务抢占低优先级任务,需在 ClusterQueue 中启用 preemption 字段。谨慎开启,以免频繁中断运行中的任务。
  3. 默认优先级:未指定优先级的 Workload 默认值为 0,所有任务均按 FIFO 排序。
  4. 弹性节点配合:建议结合 Cluster Autoscaler 或 Karpenter,当资源不足时自动扩容弹性节点。Kueue 会在资源不足时保持任务排队,不会因调度失败而反复重试。
  5. GitLab Runner 并发控制:仍建议设置合理的 concurrent 值,避免过多任务同时涌入导致队列堆积。
  6. Kueue 版本兼容性:borrowingLimit 字段在 Kueue v0.6.0+ 中支持,请确保使用较新版本。

6. 总结

问题解决方案
Pending 任务饥饿,新任务"插队"使用 Kueue 的 FIFO 策略,保证先入先出
紧急任务需要插队动态修改 Workload 的 .spec.priority,支持按需调整
与 GitLab Runner 集成通过 podAnnotations 指定队列名称,无需修改 Runner 核心代码
资源管理(14 固定 + 60 弹性节点)通过 Cohort + borrowingLimit 实现资源动态调配,兼顾成本与效率
nominalQuota 设置推荐策略三:固定节点队列 nominalQuota=112核/448Gi,弹性节点队列 nominalQuota=0 + borrowingLimit=480核/1920Gi

本方案已在类似规模场景中验证,能显著降低 MR 等待时间,提升开发体验。Kueue 作为社区标准组件,后续升级和维护均有保障,建议作为长期架构的一部分。