Volcano 批量调度与 Gang Scheduling
一个分布式训练作业需要 8 个 worker 和 1 个 master。默认调度器先把 6 个 worker 放进集群,剩余 Pod 因 GPU 和拓扑约束持续 Pending。已启动进程占着 GPU、挂载和连接,却永远等不到完整通信环;自动伸缩器又把这些已占用 request 当成真实需求继续扩节点。团队看到“已有 6 个 Pod Running”以为任务正在推进,实际上算力只是在等待屏障。
另一个团队把所有批任务都标成高优先级,期待调度器自动保证吞吐。大任务不断驱逐小任务,小任务重启后又参与竞争;Queue 权重、抢占策略和最小成员数没有共同模型,结果是 GPU 利用率很高,完成作业数却下降。Volcano 的价值不在于再造一个提交队列,而在于让批任务以 Job/PodGroup 为调度单元,通过 Gang、DRF、predicates、proportion 等插件在调度会话中决定哪些 Pod 可以一起分配并真正绑定到 Node。
Volcano 接管的是实际调度路径
Pod 的 spec.schedulerName 决定由哪个调度器处理。VolcanoJob 把 spec.schedulerName 设为 volcano 后,其 Pod 会进入 volcano-scheduler 的缓存与调度会话;普通 Pod 也可以显式选择 Volcano 并通过 PodGroup 形成批调度语义。这个边界与 Kueue 不同:Kueue 的 Admitted=True 表示作业获得执行额度,Pod 放在哪仍由后续调度器决定;Volcano 会执行节点过滤、资源分配、抢占判断和绑定。
Gang Scheduling 解决的是“部分成员启动没有业务价值”的工作负载。PodGroup 的 minMember 或 VolcanoJob 的 minAvailable 定义最小可运行成员数,minResources 可进一步表达整组最低资源。调度器在一个 session 中先尝试为成员找到可行节点,达到门槛后才提交分配;达不到时撤销临时决策,让资源留给能够推进的作业。它保证的是调度原子性门槛,不保证应用进程一定建立通信、模型一定收敛或数据一定可读。
VolcanoJob、PodGroup 与 Queue 各保存一层事实
batch.volcano.sh/v1alpha1 的 VolcanoJob 是批任务声明,包含多个 task、每个 task 的副本模板、minAvailable、queue、priorityClassName、重试和生命周期 policy。Job controller 创建 Pod、Service、ConfigMap 等运行对象,并根据 Pod 结果推进 Pending、Running、Restarting、Terminating、Completed 等状态。alpha API 意味着字段与升级兼容必须按发行版本核对,不能把它当作 Kubernetes 稳定 Job API 的等价替换。
scheduling.volcano.sh/v1beta1 的 PodGroup 是调度器看到的整组单位。VolcanoJob 通常自动创建名称形如 <job-name>-<job-UID> 的 PodGroup,而不是与 Job 严格同名;原生控制器或自定义训练算子也可以通过集成生成 PodGroup。status.phase 的 Pending、Inqueue、Running、Unknown 与 conditions 能说明整组是否通过队列校验、是否满足最小成员和为何卡住。官方 PodGroup 概念页给出了 minMember、minResources 和状态含义。
Queue 是集群级资源共享对象,表达权重、capability、reclaimable 等调度治理。它与 Kubernetes ResourceQuota 不在同一层:ResourceQuota 在 API 准入时限制 namespace 对象总 request,Volcano Queue 在调度时参与作业排序与资源份额。两者同时启用时,前者可能先拒绝 Pod 创建,后者则只能对已存在的调度对象决策;排障必须先看 API/Job controller 是否创建出完整成员,再看 PodGroup 和 scheduler。
安装固定发行版并检查三个控制组件
下面以 Volcano v1.15.0 发行版本为实验基线。它包含 scheduler、controllers 和 admission webhook,核心对象仍跨 v1alpha1 与 v1beta1。实验需要可销毁 Kubernetes 集群、kubectl、安装 CRD、ClusterRole、Webhook、Service 与控制组件的集群管理员权限。正式环境优先使用受控 Helm values、镜像 digest 与变更审计;这里使用固定 tag 的官方清单,便于复现和彻底清理。
kubectl apply -f https://raw.githubusercontent.com/volcano-sh/volcano/v1.15.0/installer/volcano-development.yaml
kubectl -n volcano-system get deployments,pods,services
kubectl -n volcano-system rollout status deployment/volcano-scheduler --timeout=180s
kubectl -n volcano-system rollout status deployment/volcano-controllers --timeout=180s
kubectl -n volcano-system rollout status deployment/volcano-admission --timeout=180s
kubectl api-resources | grep -E 'volcano|PodGroup'预期三个 Deployment 可用,API 资源中出现 jobs.batch.volcano.sh、podgroups.scheduling.volcano.sh 和 queues.scheduling.volcano.sh。若 admission Pod Running 但创建 vcjob 超时,应检查 webhook Service endpoints、证书、CABundle 和控制面网络;若 scheduler Running 而 Pod 一直没有调度事件,应先确认 schedulerName 与 scheduler profile。安装和升级命令可对照 Volcano v1.15.0 发行说明。
调度会话如何把插件动作串起来
volcano-scheduler 周期性打开调度 session,把 Node、Queue、Job/PodGroup 和 Task 快照装入缓存。actions 决定一次 session 执行的阶段,例如 enqueue 判断作业能否进入 Queue,allocate 为 task 寻找节点,preempt/reclaim 处理同队列或跨队列资源争用,backfill 用暂时空闲资源运行不会阻塞更高优先级作业的任务。动作顺序会改变结果,不是无关紧要的配置排序。
插件通过 tier 组合职责。priority 处理优先级,gang 检查最小成员,predicates 复用节点可行性约束,drf 按主导资源份额排序,proportion 依据 Queue 权重与 capability 分配份额。DRF 对每个作业计算 CPU、内存、GPU 等资源占集群总量的比例,取最大的主导份额,并优先调度份额更小者;它改善多资源公平性,却不能替业务定义优先级、截止时间或数据局部性。算法与配置入口可参考 DRF 插件文档。
session 内的 allocate 通常先形成可撤销的 statement。gang 插件发现整组达不到 minMember 时,临时分配不会全部提交;满足后再进入 bind。这个“先模拟一组、后整体提交”的转折,是零 Pod Running 与部分 Pod Running 差异的机制来源。若 Job controller 根本没创建出足够 Pod,调度器也无法凭 minAvailable 变出成员,因此 controller 日志、PodGroup 状态与 scheduler Event 必须一起看。
正向实验:两个成员满足后一起运行
先建立隔离 namespace,再提交一个有两个 worker、最小可用成员为 2 的 VolcanoJob。镜像与版本固定,request 很小,以降低实验受集群容量影响;生产作业应根据真实 working set 设置 requests,而不是照抄演示值。
apiVersion: v1
kind: Namespace
metadata:
name: volcano-lab
---
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: gang-ok
namespace: volcano-lab
spec:
schedulerName: volcano
queue: default
minAvailable: 2
maxRetry: 1
tasks:
- replicas: 2
name: worker
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: registry.k8s.io/busybox:1.36.1
command: ["sh", "-c", "echo gang-ready; sleep 30"]
resources:
requests:
cpu: 100m
memory: 64Mi保存为 /tmp/volcano-gang-ok.yaml,执行并连续观察:
kubectl apply -f /tmp/volcano-gang-ok.yaml
kubectl -n volcano-lab get vcjob,podgroup,pods -w
JOB_UID=$(kubectl -n volcano-lab get vcjob gang-ok -o jsonpath='{.metadata.uid}')
kubectl -n volcano-lab get podgroup "gang-ok-$JOB_UID" -o yaml
kubectl -n volcano-lab get pods -l volcano.sh/job-name=gang-ok -o wide
kubectl -n volcano-lab logs -l volcano.sh/job-name=gang-ok --all-containers --prefix预期 VolcanoJob 创建 gang-ok-<job-UID> PodGroup,PodGroup 的 spec.minMember 为 2,短暂经过 Pending/Inqueue 后达到 Running;两个 Pod 的 spec.schedulerName 都是 volcano,并进入 Running,日志各输出 gang-ready。Job 最终 Completed。证据链是 vcjob 声明、PodGroup 门槛、schedulerName、Pod 的 Scheduled Event 和应用日志,不能只用 Job Running 代替。
反向实验:一个不可放置成员阻断整组
为了稳定暴露 Gang 语义,创建一个 minAvailable: 3 的作业:两个普通 worker 本可调度,第三个 blocker 要求不存在的节点标签。若调度器逐 Pod 立即绑定,两个 worker 会先占资源;在 gang 约束下,整组无法达到三成员可行,普通 worker 也不应进入 Running。
apiVersion: batch.volcano.sh/v1alpha1
kind: Job
metadata:
name: gang-blocked
namespace: volcano-lab
spec:
schedulerName: volcano
queue: default
minAvailable: 3
tasks:
- replicas: 2
name: worker
template:
spec:
restartPolicy: Never
containers:
- name: worker
image: registry.k8s.io/busybox:1.36.1
command: ["sh", "-c", "sleep 300"]
resources:
requests: {cpu: 100m, memory: 64Mi}
- replicas: 1
name: blocker
template:
spec:
restartPolicy: Never
nodeSelector:
lab.volcano/blocked: "true"
containers:
- name: blocker
image: registry.k8s.io/busybox:1.36.1
command: ["sh", "-c", "sleep 300"]
resources:
requests: {cpu: 100m, memory: 64Mi}执行 kubectl apply -f /tmp/volcano-gang-blocked.yaml,然后取三层证据:
kubectl -n volcano-lab get vcjob gang-blocked -o yaml
JOB_UID=$(kubectl -n volcano-lab get vcjob gang-blocked -o jsonpath='{.metadata.uid}')
kubectl -n volcano-lab get podgroup "gang-blocked-$JOB_UID" -o yaml
kubectl -n volcano-lab get pods -l volcano.sh/job-name=gang-blocked -o wide
kubectl -n volcano-lab describe pods -l volcano.sh/job-name=gang-blocked
kubectl -n volcano-system logs deployment/volcano-scheduler --since=10m | grep gang-blocked预期 PodGroup 保持 Pending 或 Inqueue,三个 Pod 可以已经由 controller 创建,但都不应出现 status.conditions[type=PodScheduled].status=True,Running 成员数应为 0;blocker 的事件或 scheduler 日志指向节点选择约束不满足。不同日志级别的文字可能变化,因此自动判断应以 PodGroup condition、Running 成员数和每个 Pod 的 Scheduled condition 为主,日志用于补充插件分支。删除 blocker task 并重新提交作业,或只在隔离实验节点添加匹配标签后,下一调度 session 才可能整体通过。不要在共享生产节点上临时加伪标签来“修复”事故,应修正声明或供给模型。
Queue、DRF 与抢占会怎样改变结果
Queue 的 weight 影响 proportion 插件计算的应得份额,capability 可以限制该队列可占的资源上限,reclaimable 影响空闲份额被借出后是否可回收。权重不是固定配额,也不是“权重 2 就永远获得两倍 GPU”;实际结果还受当前集群资源、其他 Queue 需求、Job requests、插件配置和不可分割设备影响。
DRF 关注多维资源的主导份额。例如 CPU 密集作业占 20% CPU、5% 内存,其 dominant share 为 20%;GPU 作业占 10% CPU、30% GPU,dominant share 为 30%。在其他条件相同的 session 中,份额较小的作业优先。错误或虚高的 requests 会直接扭曲公平性,隐藏真实需求也会让作业被过度分配并在节点执行时 OOM 或节流。
抢占与 reclaim 会中断已经获得节点的任务。priorityClassName 只是候选排序的一部分,victim 选择还受 Queue、插件和 action 影响。v1.15.0 增加的 gangPreempt、gangReclaim 仍是 alpha 能力,需要显式配置;官方明确不建议在同一 action 列表中混用新 gang 动作与旧 preempt/reclaim。生产启用前必须验证整组 victim、checkpoint、PVC 写入一致性和恢复时间,而不是只验证高优先级 Pod 最终 Running。
从 Pending 到失败要沿三条链排查
第一条是对象生成链。vcjob 是否通过 admission、controller 是否创建完整 task Pod 和 PodGroup、owner reference 是否正确?缺少 Pod 时看 admission/controller 日志、ResourceQuota、镜像模板和生命周期 policy。此时 scheduler 没有完整输入,调插件参数没有意义。
第二条是队列与整组链。PodGroup 是否进入 Inqueue,Queue 是否 Open,可用/应得资源是否达到 minResources 与 minMember,DRF、priority、proportion 的排序是否让作业持续后移?持续 Pending 但没有单 Pod FailedScheduling 时,PodGroup conditions 和 volcano-scheduler 日志通常比默认 scheduler 日志更接近原因。
第三条是节点可行性链。检查每个 task 的 requests、selector、affinity、toleration、PVC topology、设备资源和端口冲突。某个成员不可行会阻断整个 gang,不能只抽查第一个 worker。已达到 Running 后再失败,则转向容器日志、Job policy、网络通信、存储和应用屏障;Gang 只保证调度门槛,不保证运行时健康。
Webhook、RBAC 与作业数据都需要隔离
Volcano scheduler 需要读取 Node、Pod、PVC、PriorityClass、Queue 与 PodGroup,并执行 Pod bind、更新状态和发起驱逐;controllers 需要创建和删除 Job 派生对象;admission webhook 能校验或修改进入集群的工作负载。这些都是高影响权限。生产应使用发行版最小 RBAC,限制管理 Queue、Scheduler ConfigMap、WebhookConfiguration 与 CRD 的账号,并对 Pod bind、eviction 和 Queue 变更建立审计。
VolcanoJob task 常携带数据集地址、模型参数、SSH 或服务发现配置。官方插件可以帮助生成运行对象,但 Secret 仍应通过受控引用和 workload identity 交付,不应内联到 vcjob、环境变量展示、Event 或 scheduler 日志。调度日志中的 namespace、Queue、GPU request、优先级和节点名也属于容量与业务敏感事实,日志平台要做租户访问控制和保留期治理。
Webhook 证书到期、CABundle 不一致或控制面无法访问 Service,会把调度工具故障扩大成 API 提交故障。升级和卸载前应先确认 failurePolicy、超时与应急旁路经过评审;临时删除 webhook 虽能恢复提交,却可能让不符合 PodGroup/Job 契约的对象进入集群,后续必须补审计和清理。
吞吐、碎片和空转时间共同决定成本
Gang Scheduling 可以避免部分成员长期空转,但较大的 minAvailable 也会增加等待并形成大块资源需求。一个需要 8 张同型号 GPU 的作业可能让零散 GPU 长期空闲,而允许弹性成员的框架可以更早启动。是否降低门槛要由算法正确性、通信拓扑、扩展效率和 checkpoint 语义决定,不能单纯为了提高表面利用率。
观测至少要关联 Queue allocated/deserved、PodGroup Pending 年龄、session 调度延迟、每个 action/plugin 的失败或耗时、Pod 绑定到 Running 的时间,以及 completed job throughput。GPU 利用率高但完成率下降,可能是频繁抢占与重算;Queue 份额公平但节点碎片上升,可能是 requests 粒度、拓扑或设备不可分割造成。阈值应由负载回放与 SLO 建立,不能把实验中的 100m CPU 当生产建议。
scheduler、controller 和 webhook 本身也消耗控制面容量。大量 task 会放大缓存对象、调度 session 计算、API watch、Event 与 bind 请求;日志 debug 级别会显著增加 I/O。容量测试应覆盖作业突发提交、超大 PodGroup、Queue 数量、节点与 GPU 规模、抢占风暴和控制组件重启后的缓存重建。
选型与共存先明确谁拥有哪道门
需要 Gang Scheduling、DRF、多 Queue 公平、批任务生命周期以及可插拔调度动作,并愿意让指定工作负载使用独立 scheduler 时,Volcano 是直接选择。普通在线 Deployment、单 Pod Job 或只需 affinity/taint/topology spread 的服务,继续使用 kube-scheduler 通常更简单,避免额外 CRD、Webhook 与调度器升级责任。
Kueue 更适合在现有 Job 与调度器之前增加配额准入。两者共存时,一条清晰链路是:Kueue 管 suspend、Workload quota 与 AdmissionCheck,准入后 Volcano 根据 schedulerName: volcano、PodGroup 和 Queue 完成实际放置。组织必须指定哪一方拥有名义 quota、借用与抢占;若 Kueue Cohort 和 Volcano Queue 同时独立决定同一批 GPU 的借用/回收,作业可能先在 Kueue 获准,随后又在 Volcano 长期排队,等待 SLO 与中断责任都无法解释。
迁移已有工作负载时先选低风险 Queue 灰度,不要把默认 schedulerName 全局改成 volcano。比较同一回放负载的完成吞吐、Pending 年龄、抢占次数、碎片、scheduler latency 和业务失败,再扩大范围。保留回到默认 scheduler 的模板开关,但已经由 Volcano 管理的 PodGroup/VolcanoJob 不能靠中途改 schedulerName 无损迁移,应让当前批次结束或按 checkpoint 重新提交。
升级、回滚、清理与责任治理
升级前备份 CRD schema、Queue、scheduler ConfigMap、PriorityClass、VolcanoJob 与 PodGroup,核对 Kubernetes 兼容性、API 变化、默认插件和 action 顺序。v1.15.0 的 gang 级抢占、Scheduling Gates Queue Admission 等 alpha 能力不能未经灰度成为基线;DRA 队列配额还依赖 Kubernetes DRA 与可用驱动。先在隔离集群重放正向 Gang、不可满足成员、Queue 争用、抢占与 controller 重启,再升级生产。
回滚要求旧 scheduler/controllers 能理解当前 CRD 与配置。若新版本已经写入旧版不识别的字段或状态,直接换镜像并不安全,应先停用新 feature gate、迁移对象并验证 conversion。退出时先停止新 vcjob 提交,等待或 checkpoint 当前作业,把后续模板切回替代 scheduler/Job API,确认没有 Pod 仍指定 schedulerName: volcano,再删除控制组件和 CRD。先删 CRD 会级联删除对象或让 finalizer、webhook 与运行 Pod 失去控制面。
本地实验先删作业并确认 Pod/PodGroup 释放,再删 namespace,最后按同一固定清单卸载:
kubectl -n volcano-lab delete vcjob gang-ok gang-blocked --ignore-not-found
kubectl -n volcano-lab wait --for=delete pod -l volcano.sh/job-name=gang-ok --timeout=120s
kubectl -n volcano-lab wait --for=delete pod -l volcano.sh/job-name=gang-blocked --timeout=120s
kubectl -n volcano-lab wait --for=delete podgroup --all --timeout=120s
kubectl delete namespace volcano-lab --ignore-not-found
kubectl delete -f https://raw.githubusercontent.com/volcano-sh/volcano/v1.15.0/installer/volcano-development.yaml --ignore-not-found卸载后检查残留的 schedulerName: volcano Pod、ClusterRoleBinding、WebhookConfiguration、Service、证书 Secret、CRD 与监控目标。平台组负责控制组件、配置、兼容和性能,容量团队负责 Queue 权重、capability 与抢占政策,业务团队负责 minAvailable、requests、checkpoint 和重试幂等,安全团队负责 bind/eviction 权限、Webhook 与敏感日志。只有当一次 Job 等待、绑定、中断和完成都能找到状态所有者,Volcano 才真正提升批处理效率,而不是把默认调度器之外再增加一套不可解释的控制面。
