按:一个任务的一生来梳理 Volcano,重点包括:

  • vcjob 提交、controller 创建 PodGroup/Podscheduler 执行 enqueue/allocate/bind,一直到 kubelet 起容器、Job Policy 处理异常、TTL 自动清理
  • QueuePodGroupgangactionplugin 这些核心概念挂到同一条主线上,方便理解调度过程
  • 补充网络拓扑感知、层级队列、在线离线混部、反调度、TDM、GPU/NPU、扩展方式、生态和运维排查
  • 最后记录一次 CRD 版本不匹配导致 vcjob 不创建 PodGroup 的真实排查过程

Volcano 笔记:以"一个任务的一生"为主线

基于看完官方文档 + 一次真实排查整理。 Volcano 版本迭代较快(尤其 CRD 集合),照着做前先确认自己集群的版本。


一句话定位

Volcano = 给 K8s 装一个批处理 / AI 友好的调度大脑

K8s 原生 kube-scheduler 是「一个 Pod 一个 Pod 地看」,但 AI / 大数据任务需要:

  • 一组 Pod 要么一起上、要么都别上(gang,避免分布式任务部分启动死锁)
  • 队列级的公平与配额(多团队共享集群)
  • 抢占 / 回收 / 防饿死(资源紧张时的腾挪)

这些原生调度器都不擅长,正是 Volcano 的存在意义。

核心扩展点:Volcano Job(vcjob) 这种新工作负载 + 自研 volcano-scheduler(action + plugin 可插拔)+ volcano-controller(把 vcjob 翻译成 PodGroup/Pod)+ 一套 webhook。


1. 主线:一个任务从提交到结束,经历了什么

这是整篇笔记的骨架。其余所有概念都挂在这条线上。

flowchart TD
    A[提交 vcjob] --> B[controller 创建 PodGroup + Pod]
    B --> C{enqueue<br/>基本资源够吗?}
    C -- 不够 --> C1[PodGroup: Pending<br/>继续排队]
    C -- 够 --> D[PodGroup: Inqueue]
    D --> E{allocate<br/>串行遍历 job<br/>plugin 过滤+打分}
    E -- gang 凑不齐/无合适节点 --> E1[保持 Pending<br/>等下一轮]
    E -- 资源不足且需腾挪 --> F[preempt 队内抢占<br/>reclaim 队间回收]
    E -- 选到节点 --> G[bind 到节点]
    F --> G
    E2[backfill: 边角资源喂 BestEffort] -.-> G
    G --> H[【脱离 Volcano】<br/>kubelet: 拉镜像/起容器]
    H --> I[Pod Running<br/>job plugin 已注入组网信息]
    I --> J[运行中: Job Policy 处理 Pod 失败/驱逐事件]
    J --> K[结束: TTL 到点自动清理]

关键分界线:Pod 被 bind 到节点之前,一切都在 Volcano 手里(Pending 的 vcjob pod 等的是 volcano-scheduler,不是 kube-scheduler);bind 之后进入 ContainerCreating,就脱离 Volcano,问题归 kubelet / 镜像 / CNI / 存储。


2. 基本概念(挂在主线上)

四个概念是层层包含关系,不是并列:

概念 是什么 在主线哪一步
Volcano Job (vcjob) 批处理任务定义,内含多个 task(每个 task 多副本) 第①步:你提交的东西
Queue 资源池 / 配额边界,多团队共享集群的隔离单位 vcjob 归属某 queue;proportion/reclaim 以它为单位
PodGroup (pg) 一组 Pod 的调度单位,gang 调度的最小单位 controller 据 vcjob 创建;调度以它为单位
Cron VolcanoJob 定时触发的 vcjob(类似 CronJob) 定时产生 vcjob,后续流程相同

关系:vcjob 提交 → controller 建 PodGroup(归属某 Queue)→ gang 以 PodGroup 为单位「全有或全无」。

PodGroup 的关键状态机(排查时最常看):

Pending ──enqueue放行──> Inqueue ──allocate成功──> Running

3. 调度器内核:action(流程)+ plugin(算法)

核心区分:

  • action = 调度流水线的「步骤」,定义"走哪几步”。
  • plugin = 挂在 action 上的「算法」,定义"每步用什么逻辑、谁说了算”。
  • configmap(volcano-scheduler-configmap)里的 actionstiers 只能组合已编译进二进制的 action/plugin,不能凭空造新逻辑。

调度循环:每隔 ~1 秒 OpenSession(快照资源+排序 job)→ 按序执行 actions → CloseSession(真正 bind)。 单 session 串行处理所有 job,每分配一个就立即在内存快照里扣减资源 → 天然不会 over-commit(不会出现两个 gang 任务都判断通过的"写倾斜”)。

3.1 Action 清单

action 作用 维度 默认开?
enqueue 判断基本资源够不够进队,Pending→Inqueue 进场
allocate 给有明确资源请求的 task 精确选节点 分配
backfill 把剩余边角资源喂给 BestEffort / 说不清需求的小任务 填空
preempt 队内按优先级抢占低优任务 队内 ❌ 需手动加
reclaim 队间按配额回收被借走的资源 队间 ❌ 需手动加

preempt vs reclaim 是最容易混的一对:

  • preempt:同一 queue 内,依据 task 优先级(PriorityClass),驱逐队内低优任务。
  • reclaim:queue 之间,依据 queue 权重/配额,把别队超额借用的资源要回来。

开启抢占+回收的典型配置:

1
actions: "enqueue, allocate, reclaim, preempt, backfill"

3.2 Plugin 清单(按职责分四组记,比平铺好用)

A. 能不能一起上 | plugin | 作用 | |—|—| | gang | 全有或全无:满足 minAvailable 才整组调度,否则一个都不下发。Volcano 的灵魂。 |

B. 给谁(公平 / 优先级 / 防饿死) | plugin | 作用 | |—|—| | priority | 按显式 PriorityClassName 排座次(人为指定)。排在 DRF 前,是更强的决定因素。 | | drf | Dominant Resource Fairness:多资源下取每个 job 占比最高的「主导资源」,让各 job 的「主导份额」尽量拉平,占得少的优先。管 job 之间 公平。 | | proportion | 按 queue 权重分配集群配额。管 queue 之间。也是 reclaim 的依据。 | | capacity | 比 proportion 更新的队列资源管理插件;SchedulingGatesQueueAdmission 功能实现在这里面。 | | sla | Service Level Agreement:给 job 设最长等待时间 JobWaitingTime,等够久就提优先级,防饿死。 | | cdp | Cooldown Protection:任务起来后给一段冷却保护期,防止刚调度上就被抢占/驱逐反复横跳。与 sla 是一对(一个防等不到,一个防刚上就被踢)。 |

C. 能放哪(过滤 / 约束) | plugin | 作用 | |—|—| | predicates | 节点是否满足 task 的硬性条件(资源、亲和、污点等),GPU 场景常用。 | | numa-aware | 节点内 NUMA 感知:把 pod 的 CPU/内存收拢到单个 NUMA node,减少跨 NUMA 损耗。需节点开 cpuManager=static + 装 resource exporter。 | | task-topology | 同一 job 内不同 task 的亲和/反亲和(如 TF 的 ps 与 worker 同机)。 |

D. 放哪最好(打分 / 排序) | plugin | 作用 | |—|—| | nodeorder | 多维度综合打分,选最合适节点。 | | binpack | 装箱:尽量塞满已用节点、减少碎片,利于节点缩容。 | | resource-strategy-fit | 按资源类型分别设策略:CPU/内存 LeastAllocated(摊开)、GPU MostAllocated(塞满腾整机)。比 binpack 更细。 |

其它/混部相关(见第 6 节单列)

  • TDM(Time Division Multiplexing):和 YARN 等错峰分时共用节点。

3.3 tiers 分层与 tie-break

tiers 把 plugin 分层,同名函数按 tier 顺序逐层调用,前层可「一票否决」(返回 <0 即停)。 两个 job 谁先调度的 tie-break 链:priority → drf → … → 创建时间(先到先得) 兜底,保证排序确定性。 → 想在平手时指定赢家,用 PriorityClass(priority 在 DRF 前),而不是动 DRF。

默认配置参考:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
actions: "enqueue, allocate, backfill"
tiers:
- plugins:
  - name: priority
  - name: gang
  - name: conformance      # 保护 kube-system 等关键 pod 不被抢占
- plugins:
  - name: drf
  - name: predicates
  - name: proportion
  - name: nodeorder
  - name: binpack

4. 任务跑起来:Job Plugin(组网与运行)

gang 保证「一起被调度」,但「调度上了怎么组成一个分布式任务」靠 job plugin(写在 vcjob 的 spec.plugins,和 scheduler plugin 是两回事):

job plugin 作用 给身份/互联/发现
env 注入环境变量(如 task 索引 VK_TASK_INDEX) 给身份
svc 建 headless service + 主机信息,pod 间 DNS 互相发现 给发现
ssh pod 间免密 SSH(MPI 必备) 给互联
pytorch 自动注入 MASTER_ADDR/PORT/WORLD_SIZE/RANK、建 svc、让 worker 等 master 框架级编排(底层强制依赖 svc)
ray 配 head/worker 启动命令、开三端口、建 head svc(需同时开 svc) 框架级编排
mpi mpirun launcher + worker 编排(依赖 ssh) 框架级编排

pytorch/ray/mpi 本质 = 把 env/svc/ssh 组合 + 再注入框架专属配置。


5. 任务的容错与清理(生命周期收尾)

机制 作用 例子
Job Policy event → action 的容错规则 PodEvicted → RestartJob(任一 pod 被驱逐整组重启)
Job TTL 结束后定时自动清理 ttlSecondsAfterFinished: 3600

Policy 可选 action:RestartJob / RestartTask / AbortJob / TerminateJob / CompleteJob。


6. 主线之外的重要模块

这些不一定出现在「单个任务提交」的主线里,但都是 Volcano 的重要能力。

6.1 网络拓扑感知(多机训练用)

  • 节点之间的网络层级(node → 交换机/tor → spine),让多卡 worker 尽量同交换机,减少跨交换机通信。
  • HyperNode CR 描述拓扑;支持自动发现(v1.12+,源:UFM = NVIDIA InfiniBand 管理平台 / RoCE(暂不支持)/ label)。
  • vcjob 里用 networkTopology(mode: hard/soft + highestTierAllowed)+ partitionPolicy(子组)表达约束。
  • ⚠️ 和 NUMA 区分:NUMA 是「节点内」CPU/内存局部性,HyperNode 是「节点间」网络局部性,两套数据源。

层级示意(让一个 job 的多卡 worker 收拢在同一 tier1 交换机下):

flowchart TD
    S[Spine tier2<br/>跨交换机骨干]
    S --> A[交换机 A tier1<br/>同交换机=低延迟高吞吐]
    S --> B[交换机 B tier1<br/>另一个性能域]
    A --> N1[node-1<br/>8 GPU]
    A --> N2[node-2<br/>8 GPU]
    B --> N3[node-3<br/>8 GPU]
    B --> N4[node-4<br/>8 GPU]
    C[调度约束 networkTopology<br/>hard=必须同域 / highestTierAllowed] -.约束.-> A
    H[拓扑来源 HyperNode CR<br/>UFM/label 自动发现] -.提供层级.-> S

读法:hard 模式下,一个训练 job 的 4 个 worker 会被强制收拢在同一个 tier1(交换机 A 或 B)内,避免跨 spine 通信。拓扑数据由 HyperNode CR 提供,可经 UFM 或节点 label 自动发现生成。

YAML ①:vcjob 侧——声明拓扑约束

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
  name: qwen-train
spec:
  schedulerName: volcano
  minAvailable: 4
  networkTopology:
    mode: hard               # hard=必须同域;soft=尽量同域
    highestTierAllowed: 1    # 配合 hard:最高只允许到 tier1(同交换机)
  tasks:
    - name: worker
      replicas: 4
      template:
        spec:
          containers:
            - name: worker
              image: <训练镜像>
              resources:
                limits:
                  nvidia.com/gpu: 8

超大规模(一个交换机放不下)用 partitionPolicy 做"多级”:整体允许跨 tier2,每个子组内部仍锁 tier1。

1
2
3
4
5
6
7
8
9
  tasks:
    - name: worker
      replicas: 16
      partitionPolicy:
        totalPartitions: 2     # 切 2 个子组
        partitionSize: 8
        networkTopology:
          mode: hard
          highestTierAllowed: 1   # 每个子组内部锁同一交换机

YAML ②:HyperNode 自动发现(UFM 源,NVIDIA InfiniBand)——controller configmap

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 先建 UFM 凭据 Secret:
# kubectl create secret generic ufm-credentials \
#   --from-literal=username='xxx' --from-literal=password='xxx' -n volcano-system
data:
  volcano-controller.conf: |
    networkTopologyDiscovery:
      - source: ufm
        enabled: true
        interval: 10m
        credentials:
          secretRef: { name: ufm-credentials, namespace: volcano-system }
        config:
          endpoint: https://ufm-server:8080
          insecureSkipVerify: true
1
2
3
4
# 验证:发现源是否启动 + 自动生成的 HyperNode
kubectl -n volcano-system logs -l app=volcano-controllers \
  | grep "Successfully started all network topology discoverers"
kubectl get hypernodes -l volcano.sh/network-topology-source=ufm

没有 UFM 时可用 source: label,靠给节点打 volcano.sh/hypernodevolcano.sh/hypercluster 等标签从下往上构建层级。

6.2 Hierarchical Queue(层级队列)

  • 队列按部门/团队分层,配合 hierarchical DRF 做多层公平与配额。

6.3 Cloud Native Colocation(在线离线混部)

  • 在线(高优、延迟敏感)和离线(低优、可被压制)业务混跑同一批节点,提升利用率。
  • 需要额外的 volcano-agent(节点侧组件):做资源超卖、QoS 保障、干扰检测、必要时驱逐/压制离线任务。
  • 相关 CRD:colocationconfigurations.config.volcano.sh(⚠️ 排查就是因为这个 CRD 缺失,导致新版 job-controller 卡在 cache sync)。

结构示意(同一节点分层共享,agent 兜底 QoS):

flowchart TD
    subgraph NODE[一个物理节点 · 资源池]
        ON[在线业务 高优<br/>推理/Web,占住 request,优先扩张]
        OFF[离线业务 低优<br/>批处理/训练,吃超卖资源,一抢就让路]
    end
    AGENT[volcano-agent 节点侧<br/>超卖 · QoS · 干扰检测 · 压制/驱逐离线]
    AGENT -->|监控并保障| ON
    AGENT -->|压制/驱逐| OFF

读法:在线占住自己的 request 且优先;离线吃节点空闲/超卖资源跑。volcano-agent 实时盯着,一旦干扰检测发现在线受影响,就压制甚至驱逐离线任务,优先保障在线 QoS。 ⚠️ 注意和 6.1 的区别:拓扑感知管「任务放哪台机器」(节点间),混部管「同一台机器上两类业务怎么共处」(节点内)。

YAML ①:工作负载侧——用 QoS 等级标记在线/离线

1
2
3
4
5
6
7
8
9
# 在线业务(高优,保障):
metadata:
  annotations:
    volcano.sh/qos-level: "HLS"      # 高优先级延迟敏感
---
# 离线业务(低优,可被压制/驱逐):
metadata:
  annotations:
    volcano.sh/qos-level: "BE"       # BestEffort,吃超卖资源

YAML ②:ColocationConfiguration——超卖与压制策略(CRD)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: config.volcano.sh/v1alpha1
kind: ColocationConfiguration
metadata:
  name: colocation-config
spec:
  cpuBurstConfig: { enable: true }
  overSubscriptionConfig:
    enable: true                     # 开启资源超卖
    overSubscriptionTypes: cpu,memory
  evictingConfig:                    # 在线受压时,对离线的水位线与驱逐策略
    evictingCPUHighWatermark: 80
    evictingMemoryHighWatermark: 60

部署与排查

1
2
3
4
5
6
# 混部需要额外部署 volcano-agent(DaemonSet,节点侧)
kubectl -n volcano-system get ds | grep agent
kubectl -n volcano-system get pods -l app=volcano-agent

# ⚠️ 用到混部的新版 controller 依赖这个 CRD,缺了会卡 cache sync(本笔记 9.3 踩坑)
kubectl get crd colocationconfigurations.config.volcano.sh

6.4 Load-aware Descheduling(反调度)

  • 调度是「放上去的那一刻」最优,但运行久了节点负载会失衡(热点)。
  • 反调度器周期性检查,驱逐部分 pod 让它们重新被调度,把负载从过热节点摊匀。
  • 和调度互补:一个管「进来时放哪」,一个管「跑起来后再平衡」。

6.5 TDM(时分复用)

  • K8s 与 YARN 混部同一批机器,按时间窗错峰共用。把共享节点标记为 revocable node,在 revocable time 内放 preemptable task、窗外驱逐。
  • 仅在和 Hadoop/YARN 混部时才用得上。

6.6 GPU / NPU 能力(vGPU)

能力 说明
GPU Number / Sharing 按整卡数 / 多 pod 共享一张卡
vGPU / GPU Virtualization 虚拟化切分(显存+算力),主要 NVIDIA
Ascend vNPU 华为昇腾 NPU 的虚拟化(注意是 NPU 不是 GPU)
DRA 动态资源分配(K8s 新机制)在 Volcano 的支持

6.7 其它特性

  • Multi-Cluster AI Job Scheduling:跨多个 K8s 集群分发 AI 任务。
  • Unified Scheduling:统一调度原生工作负载 + vcjob。
  • SchedulingGatesQueueAdmission(v1.15+,在 capacity 插件里):用 K8s schedulingGates 把「排队中」的 pod 对 autoscaler 隐藏,避免把"排队"误判成"缺机器"而触发 Cluster Autoscaler / Karpenter 乱扩节点。代价:目前无配额预留超时。

7. 自定义扩展(三条路)

方式 改源码/编译? 适合
in-tree plugin 复杂、性能敏感、要深度访问调度上下文(抢占/入队/gang)
自定义 action 加一整个新流水线步骤(很少需要)
extender(HTTP 回调) 不要 大多数「加点自定义过滤/打分」需求,迭代独立

extender 是挂在已有 allocate action 上的已有 extender plugin,只覆盖**过滤(predicate)+ 打分(prioritize)**两个点,管不了 enqueue/preempt/gang。

  • 协议沿用 K8s kube-scheduler/extender/v1(ExtenderArgs / ExtenderFilterResult / HostPriorityList)。
  • 用任意语言(如 Spring Boot)起 HTTP 服务实现 /predicate/prioritize 即可。
  • ⚠️ 性能敏感(每轮 session、每 pod、每节点都可能回调):务必 extender.ignorable: true + 紧 httpTimeout
1
2
3
4
5
6
7
8
- name: extender
  arguments:
    extender.urlPrefix: http://my-extender-svc.default:8080/scheduler
    extender.predicateVerb: predicate
    extender.prioritizeVerb: prioritize
    extender.weight: 10
    extender.httpTimeout: 1s
    extender.ignorable: true

8. 生态(各计算框架 on Volcano)

让这些框架的任务用 Volcano 做 gang 调度,而不是各自的默认调度:

Flink / Kubeflow / MindSpore / MPI / PaddlePaddle / Spark / TensorFlow / Ray on Volcano。

共同价值:把「一组相关 pod」交给 Volcano 统一 gang 调度 + 队列公平,避免框架各自为战导致的资源死锁与不公平。


9. 监控 & 运维

9.1 可观测

  • 自带 Dashboard
  • Prometheus 指标(调度延迟、队列状态、pending 任务数等)。

9.2 排查 SOP(实战核心)

flowchart TD
    Q0[现象:任务起不来] --> Q1{PodGroup 创建了吗?}
    Q1 -- 否 --> C[controller 层问题<br/>查 vc-controllers 日志 / CRD 是否齐 / 版本匹配]
    Q1 -- 是 --> Q2{PodGroup 什么状态?}
    Q2 -- Pending --> P[enqueue 没过<br/>queue 配额 / queue Closed / 集群资源不足]
    Q2 -- Inqueue --> S[allocate 没过,看 pod]
    Q2 -- Running --> OK[PG 层 OK,看 pod]
    S --> Q3
    OK --> Q3{Pod 还 Pending?}
    Q3 -- 是,describe pod 看 scheduler Events --> R{报错类型}
    R -- gang --> R1[minAvailable 没凑齐]
    R -- Insufficient xxx --> R2[节点资源不够 GPU/CPU]
    R -- predicate/topology --> R3[节点过滤/拓扑约束没过]
    R -- queue --> R4[队列配额/状态问题]
    Q3 -- 否,已 ContainerCreating --> OUT[已脱离 Volcano<br/>查 kubelet/镜像/CNI/存储]

常用命令:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# controller 层
kubectl -n volcano-system get pods
kubectl -n volcano-system logs deploy/volcano-controllers | grep -iE "caches synced|starting .*controller|panic"
kubectl get crd | grep volcano                      # CRD 是否齐(版本错配常见坑)

# PodGroup 层
kubectl -n <ns> get pg
kubectl -n <ns> get pg <pg> -o jsonpath='{.status.phase}'
kubectl -n <ns> get pg <pg> -o yaml                 # 看 conditions 里的原因

# Pod 层
kubectl -n <ns> describe pod <pod>                  # Events 里 scheduler 报的失败原因

# 配置
kubectl -n volcano-system get cm volcano-scheduler-configmap -o yaml

9.3 真实踩坑记录(这次排查的复盘)

  • 现象:vcjob 提交成功,但无 Pod、无 PodGroup;但 volcano-system 的 pod 都 Running;同集群 Deployment 能正常建 PG。
  • 关键日志:Failed to watch *v1alpha1.ColocationConfiguration: could not find the requested resource
  • 错误猜测:以为 controller-manager 整体挂了(被 pod Running 否定)。
  • 真因:异常集群 controller 是更新版本(多了 serving 类 CRD、少了 nodeshards),其 job-controller 依赖的 CRD(colocation/nodeshards 等)没装齐 → informer 永远 cache sync 不上 → job-controller 卡死,不为 vcjob 建 PG;而 podgroup-controller 不依赖这些 → Deployment 照常建 PG → 这就解释了「为什么只有 vcjob 不行」。
  • 修复:用 controller 镜像对应版本的清单完整重装 CRD,重启 controller。
  • 教训:① pod Running ≠ controller 健康(可能卡在 WaitForCacheSync);② CRD 集合必须和 controller 版本严格一致;③「只有某类工作负载不行」是定位到具体 controller 的关键线索。

10. 贯穿全局的几条规律(最该记住的)

  1. 三层分工:controller(建 PG/Pod)→ scheduler(决定放哪)→ kubelet(真正跑)。排查先定位在哪一层。
  2. PodGroup 是 gang 的灵魂:以 PodGroup 为单位「全有或全无」,是 Volcano 区别于原生调度器的根本。
  3. 串行调度避免 over-commit:单 session 顺序处理 + 内存即时扣减 → 不会写倾斜。
  4. action 是流程,plugin 是算法:configmap 只能组合已编译进二进制的;新逻辑要么改源码(in-tree),要么 extender 外置。
  5. 公平有三个维度别混:priority(人为优先级)/ drf(job 间多资源公平)/ proportion(queue 间配额)。
  6. 抢占/回收/防饿死/防驱逐是一组对立统一:preempt+reclaim 让"抢得到”,sla 防"被饿死”,cdp 防"刚上就被踢”。
  7. 边界意识:Pod 一旦 bind 进 ContainerCreating,就不再是 Volcano 的问题。