学习库/ AI Infra 转型/ 07 · 07 Volcano 实战
🧠 AI Infra 转型 · 第 8 / 17 篇

07 Volcano 实战

2802 字· 阅读约 7 分钟· 2026-08-23 更新

07 Volcano 实战

这一章解决上一章留下的「Gang 问题」。Volcano 是 CNCF 首个基于 K8s 的批处理调度系统(华为开源),AI/大数据训练的标配调度器。你之前对话里反复问的「Volcano 是什么、怎么用」,这里一次讲透。

💡 给你的话:Volcano 对你来说不陌生——它就是个装在 K8s 上的「批处理调度器」,用 CRD 描述作业,和你用过的任何 Operator 体系同构。


7.1 为什么需要 Volcano

默认调度器痛点(见 06):

  • 分布式训练「整组卡同时到位」做不到 → 死锁。
  • 没有队列/配额/公平份额。
  • 没有任务依赖、优先级、抢占的批处理语义。

Volcano 补上这些,专为 AI 训练、HPC(高性能计算)、大数据 批量负载设计。

名词解释HPC(High-Performance Computing,高性能计算) 指传统科学/工程超算,也是批处理负载,和 AI 训练调度需求类似。批处理(Batch) 指一次性提交、跑完即退的工作负载(区别于常驻服务)。


7.2 核心概念

概念 含义 K8s 类比
Queue 资源池/租户,带配额(如 team-a 最多用 64 卡) 类似 ResourceQuota,但更强
Job(vcjob) 一次完整训练/批处理作业 类似 Job/Deployment,但支持多任务
Task Job 内的任务单元(对应一组 Pod) Job 里的 template
PodGroup 把多个 Pod 绑成「一组」统一调度(Gang 的载体) Volcano 核心 CRD

关键心智:Volcano 把「一组 Pod」当一个整体调度,而不是一个个孤 Pod 各自抢资源。

重点PodGroup 是 Gang 的载体——它声明「这一组 Pod 必须 minAvailable 个同时就绪才能启动」。这是 Volcano 区别于原生调度的灵魂。


7.3 Gang Scheduling(⭐最重要)

问题:8 卡训练任务,默认调度器可能先给 4 卡就启动 4 个 Pod,剩 4 卡一直等 → 已启动的 4 卡空转、整任务卡死。

Gang 语义整组 Pod 必须同时拿到全部资源才启动;否则全部等待,不占任何资源。

8 卡 Job
  ├─ 集群只剩 6 卡空闲 → Gang 不满足 → 全部等待(不启动任何 Pod)
  └─ 集群有 8 卡空闲   → 同时满足 → 8 个 Pod 同时启动

Volcano 的 gang 插件实现此语义,避免资源碎片死锁。

💡 类比:Gang Scheduling 像餐厅「8 人桌必须人到齐才入座」——避免来 4 个先占一桌、剩下 4 个永远等不到的尴尬。默认调度器是「来一个坐一个」,所以会卡死。


7.4 公平调度与优先级

  • Queue 公平:多个 Queue 按 weight 分配集群算力(如 team-a:team-b = 2:1)。
    • 公平份额(Fair Share):按权重瓜分资源,避免一个团队独占。
  • 优先级:Job 可设 priorityClassName,高优抢占低优。
  • binpack / spread
    • binpack(装箱):集中填满节点,减少碎片。
    • spread(打散):分散到多节点,容错/拓扑。
  • 拓扑感知numa-topology 等插件考虑 NVLink/NUMA 亲和。
    • 名词解释NUMA(Non-Uniform Memory Access,非一致内存访问) 是多 CPU 节点的内存架构,跨 NUMA 访问慢。调度考虑 NUMA 亲和能让任务用本地内存更快。

7.5 部署与实战

# 安装(Helm)
helm repo add volcano https://volcano-sh.github.io/helm-charts
helm repo update
helm install volcano volcano/volcano -n volcano-tns --create-namespace

# 创建队列
cat <<EOF | kubectl apply -f -
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata: {name: team-a}
spec: {capability: {nvidia.com/gpu: 8}, weight: 1}
EOF

提交一个 8 卡 Gang 训练 Job:

apiVersion: batch.volcano.sh/v1beta1
kind: Job
metadata: {name: train-8gpu}
spec:
  minAvailable: 8          # Gang:必须 8 个 Pod 同时就绪
  queue: team-a
  tasks:
  - name: worker
    replicas: 8
    template:
      spec:
        containers:
        - name: train
          image: pytorch/pytorch:2.2.0-cuda12.1
          command: ["python","train.py"]
          resources:
            limits: {nvidia.com/gpu: 1}
        restartPolicy: Never
EOF
kubectl apply -f job.yaml

观察:kubectl get pods——要么 8 个同时 Running,要么都在 Pending(Gang 未满足),不会出现「启动 4 个卡死」

⭐ 注意 API 版本已统一为 v1beta1(早期资料可能写 v1alpha1,已过时)。


7.6 源码导读(面试加分:能读源码)

Volcano scheduler 核心目录:pkg/scheduler/

  • cache:集群资源/状态的本地缓存
  • session:一次调度会话(在一个调度周期内构建)
  • action:调度动作(如 enqueueallocatepreempt
  • plugin:各调度策略(gang 插件就在 pkg/scheduler/plugins/gang

读源码路径建议

  1. pkg/scheduler/scheduler.goSchedule() 入手
  2. action/allocate.go:怎么选 Job → 选节点
  3. plugins/gang/gang.go:怎么判断 minAvailable 是否满足
  4. 理解 session 怎么在一次调度里复用 cache,避免反复访问 API Server

重点:面试能说「我读过 Volcano gang 插件源码,它怎么用 PodGroup 的 minAvailable 判断整组是否满足」——这比「我会用 Volcano」强一个量级,正好呼应你云原生 + 读源码的优势。


7.7 Volcano 的局限(引出 08

Volcano 解决了 Gang 和队列,但到千卡/万卡、在离线混部、细粒度隔离时仍不够:

  • 调度吞吐串行,大集群慢。
  • 偏离线批处理,在线+离线混部弱。
  • 细粒度算力/显存切分要靠 qGPU 等外挂。

这就是 08 要讲的:大厂在 Volcano/kube-scheduler 之上做了什么。


7.8 实战:在 kind 上安装 Volcano 并体验 Gang 调度

这一节在 MacBook/Linux 上用 kind 集群安装 Volcano,亲手提交一个 Gang 训练任务,观察「要么全起、要么全等」的效果。

7.8.1 环境准备(复用 04 章的 kind 环境)

# 如果还没有 kind 集群,创建一个
cat <<EOF | kind create cluster --name volcano-lab --config=-
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker       # 两个 worker,模拟多节点
EOF

kubectl get nodes
# 应有 1 个 control-plane + 2 个 worker

7.8.2 安装 Volcano

# ① 添加 Volcano Helm 仓库
helm repo add volcano-sh https://volcano-sh.github.io/helm-charts
helm repo update

# ② 安装 Volcano(含 scheduler + controller + admission)
helm install volcano volcano-sh/volcano \
  -n volcano-system --create-namespace

# ③ 等待组件就绪
kubectl get pods -n volcano-system -w
# 等到所有 Pod 为 Running:
#   volcano-admission-xxx     Running
#   volcano-controllers-xxx   Running
#   volcano-scheduler-xxx     Running

# ④ 验证 CRD 已安装
kubectl get crd | grep volcano
# 应看到:
#   commands.bus.volcano.sh
#   jobs.batch.volcano.sh
#   podgroups.scheduling.volcano.sh
#   queues.scheduling.volcano.sh

目的:Volcano 安装后会注册 4 个 CRD——Job(训练任务)、PodGroup(Gang 组)、Queue(队列)、Command。这三个 CRD 是你后面提交任务的接口。

7.8.3 创建队列

# 创建一个队列(模拟「训练团队」的资源池)
cat <<EOF | kubectl apply -f -
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: training-queue
spec:
  weight: 1                    # 队列权重(多队列时按权重分资源)
  reclaimable: true             # 空闲时资源可被其他队列 reclaim
  capability:
    cpu: "4"                    # 队列最多用 4 核 CPU
    memory: "8Gi"
EOF

# 验证队列
kubectl get queue training-queue
# NAME             WEIGHT
# training-queue   1

目的:Queue 是 Volcano 的资源配额单元。多团队共用集群时,每团队一个 Queue,按 weight 分配公平份额。

7.8.4 提交一个 Gang 训练任务

# 提交一个 4 worker 的「训练任务」
# minAvailable: 4 表示「4 个 worker 必须全部拿到资源才启动」
cat <<EOF | kubectl apply -f -
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: gang-training-demo
spec:
  minAvailable: 4               # ★ Gang 调度的核心:必须 4 个全到位
  schedulerName: volcano         # 用 Volcano 调度器(不是默认调度器)
  queue: training-queue
  policies:
    - event: PodEvicted
      action: RestartJob         # 有 Pod 被驱逐时,整个 Job 重启
  tasks:
  - replicas: 4                  # 4 个 worker
    name: worker
    template:
      spec:
        containers:
        - name: worker
          image: busybox:1.36
          command: ["sh", "-c", "echo 'Worker $HOSTNAME started!'; sleep 300"]
          resources:
            limits:
              cpu: "1"           # 每个 worker 要 1 核 CPU
              memory: "1Gi"
        restartPolicy: Never
EOF

# 观察 Pod 状态
kubectl get pods | grep gang-training
# 应看到 4 个 Pod 都快速进入 Running(因为资源够)

# 看 PodGroup 状态
kubectl get podgroup
# NAME                   STATUS
# gang-training-demo     Running

目的:这是 Gang 调度的正例——4 个 worker 同时拿到资源、同时启动。如果用默认调度器,可能会出现「先起 2 个、再等 2 个」的情况,Gang 调度保证「要么 4 个一起起、要么都不起」。

7.8.5 观察 Gang 死锁(负例)

# 提交一个需要 10 个 worker 的任务(但集群只有 ~4 核 CPU 可用)
cat <<EOF | kubectl apply -f -
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: gang-deadlock-demo
spec:
  minAvailable: 10              # ★ 要 10 个,但资源不够
  schedulerName: volcano
  queue: training-queue
  tasks:
  - replicas: 10
    name: worker
    template:
      spec:
        containers:
        - name: worker
          image: busybox:1.36
          command: ["sleep", "300"]
          resources:
            limits:
              cpu: "1"
        restartPolicy: Never
EOF

# 观察——所有 Pod 都 Pending,不会启动任何 1 个
kubectl get pods | grep gang-deadlock
# STATUS 应全部为 Pending

# 看 PodGroup 状态
kubectl get podgroup gang-deadlock-demo -o yaml | grep -A3 status
# status.phase 应为 Inqueue(在队列里,但不够资源调度)

# ★ 关键:这些 Pending 的 Pod 不占资源!
# 默认调度器会让部分 Pod 启动占着资源等其余的(死锁)
# Volcano 让它们全等,不占资源——其他任务可以用空闲资源

目的:这是 Gang 调度的核心价值——资源不够时全部等待,不占资源。如果用默认调度器,可能先起 5 个占着 5 核 CPU,剩下 5 个永远等不到 → 死锁,且 5 核 CPU 被白白占用。

7.8.6 验证不占资源的好处

# 此时 gang-deadlock-demo 的 10 个 Pod 都 Pending(不占资源)
# 提交一个普通任务,应该能正常调度
kubectl run normal-job --image=busybox --command -- sleep 60
kubectl get pod normal-job
# 应为 Running——因为 gang-deadlock 没占资源

# 如果用默认调度器(非 Gang):
# gang-deadlock 会先起 5 个 Pod 占 5 核 → normal-job 可能 Pending(资源被占)
# 这就是「死锁」的危害

7.8.7 安装 Kueue 管配额(进阶)

# Kueue 是 K8s sig-scheduling 的队列管理工具
# 添加仓库
helm repo add kueue https://kubernetes-sigs.github.io/kueue
helm repo update

# 安装 Kueue
helm install kueue kueue/kueue \
  -n kueue-system --create-namespace

# 创建 ResourceFlavor(抽象资源形态)
cat <<EOF | kubectl apply -f -
apiVersion: kueue.x-k8s.io/v1beta1
kind: ResourceFlavor
metadata:
  name: default-flavor
EOF

# 创建 ClusterQueue(集群级队列,管配额)
cat <<EOF | kubectl apply -f -
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: team-a-queue
spec:
  namespaceSelector: {}
  queueingStrategy: BestEffortFIFO
  resourceGroups:
  - coveredResources: ["cpu", "memory"]
    flavors:
    - name: default-flavor
      resources:
      - name: cpu
        nominalQuota: "4"
      - name: memory
        nominalQuota: "8Gi"
EOF

# 创建 LocalQueue(命名空间级,用户用它提交任务)
cat <<EOF | kubectl apply -f -
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  name: user-queue
  namespace: default
spec:
  clusterQueue: team-a-queue
EOF

# 验证
kubectl get clusterqueue,localqueue

目的:Kueue 管的是「配额」——每个团队最多能用多少资源。Volcano 管「Gang 调度」——整组同时起。两者配合:Kueue 管配额,Volcano 管调度策略。

7.8.8 清理

kubectl delete job.batch.volcano.sh gang-training-demo gang-deadlock-demo --ignore-not-found
helm uninstall volcano -n volcano-system
kubectl delete namespace volcano-system --ignore-not-found
kind delete cluster --name volcano-lab

7.8.9 实战总结

你做了什么 对应概念
安装 Volcano 7.1-7.3 架构与 CRD
创建 Queue 7.4 队列与公平
提交 Gang Job(4 worker) Gang 正例:全起
提交 Gang Job(10 worker) Gang 负例:全等不占资源
验证不占资源 Gang 避免死锁的价值
安装 Kueue 配额管理

7.9 自测题

  1. Volcano 是什么、谁开源、在 CNCF 什么地位?
  2. Queue/Job/Task/PodGroup 分别是什么?哪个是 Gang 的载体?
  3. 什么是 Gang Scheduling?解决了什么死锁?用餐厅类比解释。
  4. minAvailable: 8 的含义?API 版本是 v1alpha1 还是 v1beta1?
  5. Queue 的 weight 控制什么?binpack 和 spread 有何区别?
  6. Volcano 源码里 gang 插件大概在哪个目录?它依据什么判断整组可调度?
  7. ⭐ Volcano 在大规模/混部场景有哪些局限?

答上即可进 08——看字节/腾讯在 Volcano 之上还做了什么。

← 返回专栏