- Published on
Kubernetes v1.36 的 Workload/PodGroup API — Gang Scheduling 正走进 kube-scheduler
- Authors

- Name
- Youngju Kim
- @fjvbn20031
- 引言 — 逐个调度 Pod 会发生什么
- Kubernetes 为什么要亲自出手
- v1.35 — 第一块拼图:Workload API 与 Permit 门
- v1.36 — Workload 是模板,PodGroup 是运行时
- PodGroup 调度周期 — 原子性地评估整个组
- 算法诚实交代的局限
- 拓扑感知调度 — 第一版迭代
- 工作负载感知抢占
- 与 DRA 的结合 — 256 个的墙被打开了
- Job 控制器集成 — 条件相当苛刻
- 怎么打开它
- 那要不要把 Volcano 和 Kueue 拆掉
- 为什么现在不该用这个
- 尽管如此,为什么还值得关注
- 结语
- 参考资料
引言 — 逐个调度 Pod 会发生什么
跑过分布式训练作业的人都知道这种失败模式。要 4 个 worker 全部起来训练才能开始,但默认调度器是一个一个、彼此独立地放置 Pod。如果集群只剩下够 3 个 Pod 用的 GPU,那 3 个 Pod 就会占住节点坐在那儿,永远等着第 4 个。3 张 GPU 就这么被占着,什么活也不干。要是两个作业同时这么干,还会走到彼此咬住对方资源不放的死锁。
KEP-4671 的动机说明把这情况写得很直白 — 并行应用要开始执行,需要所有 Pod 之间的通信;要继续推进,需要屏障(barrier)或 all-reduce 这类持续通信。所以所有 Pod 必须尽可能在同一时刻启动,否则昂贵的算力就会闲置,或者应用会因通信超时而崩溃。
解法早就有了:gang scheduling,也就是「全有或全无」的放置方式。问题在于,这个解法一直活在集群外面 — 装 Volcano,接 Kueue,或者叠一个 coscheduling 插件。这一侧生态的故事,我已经在Kubernetes AI 训练流水线 — Volcano、Training Operator、Kueue一文里写过了。
本文讲的是另一个故事。在 2026 年 4 月 22 日发布的 Kubernetes v1.36(代号 ハル / Haru,共 70 项改进 — 稳定版 18 项、Beta 25 项、Alpha 25 项)中,把 gang scheduling 搬进 kube-scheduler 本身的这项工作,进入了第二阶段。而在这个过程中,六个月前才发布的 API 被整个换掉了。
Kubernetes 为什么要亲自出手
外面已经有能跑得好好的实现了,为什么核心还要出手?KEP-4671 直接给出了答案 — gang scheduling 已经在 kube-scheduler 之外被实现了至少四次,而且部分控制器为了能移植到使用不同调度器的各个集群,已经开始同时支持多个 gang scheduler。
问题就在这儿冒出来了。想要 gang scheduling 的是工作负载控制器(Job、JobSet、LeaderWorkerSet、MPIJob、TrainJob),而它们每一个都得分别拖着一套给 Volcano 用的代码和一套给 Kueue 用的代码,因为没有标准接口。KEP 瞄准了三件事 — 让 gang scheduling 在所有 Kubernetes 发行版上都能用,让控制器可以用同一种方式向标准调度器和自定义调度器发起请求,以及让集群自动扩缩器这类其他组件也能理解工作负载的需求。
最后一条不显眼,却很重要。现在的自动扩缩器只看得到「有 3 个 Pod 处于 Pending」,它不知道「这 3 个是一个 4 人组(gang)的一部分,要是第 4 个起不来,那这 3 个也没意义」。一旦工作负载被表达成 API 对象,这件事就能被读取到了。
接着是拓扑的问题。需要 gang 的工作负载,通常只有当 gang 的成员在拓扑上彼此靠近时才能发挥性能。现有的 Pod 亲和性确实会影响放置,但正如 KEP 指出的那样,它并不把 gang 当作调度单元,也不会为一组 Pod 高效地尝试多个互斥的放置候选方案。亲和性是一个从单个 Pod 视角出发设计的工具。
v1.35 — 第一块拼图:Workload API 与 Permit 门
v1.35(2025 年 12 月 17 日)发布了第一批内容。scheduling.k8s.io/v1alpha1 中出现了 Workload 资源。
apiVersion: scheduling.k8s.io/v1alpha1
kind: Workload
metadata:
name: training-job-workload
namespace: some-ns
spec:
podGroups:
- name: workers
policy:
gang:
# The gang is schedulable only if 4 pods can run at once
minCount: 4
Pod 通过 workloadRef 与之关联。运作方式是这样的。
- Pod 创建时,调度器会在
PreEnqueue阶段把它扣住 — 只要存在对应的 Workload 对象、其中包含相应的 Pod 组,并且等待中的 Pod 数量还没到达minCount。 - 一旦凑够数量,就尝试寻找放置方案,但不会立即绑定,而是让它们在
Permit门处等待。 - 如果为整个组(至少
minCount个)找到了有效的放置方案,门就会打开,一次性绑定;如果在超时时间(5 分钟)内只调度成功了一部分,就会把该组全部的 Pod 拒绝回队列,并释放已经占用的资源。
同时加入的还有 opportunistic batching。这项功能既不需要 Workload API,也不需要用户显式开启,在 v1.35 中是默认开启的 Beta 特性。它能识别出调度需求相同的 Pod(容器镜像、资源请求、亲和性等),把针对一个 Pod 做的可行性计算,复用到随后那些完全相同的 Pod 上,从而提升处理速度。组成 gang 的 Pod 通常彼此相同,正好合拍。不过,调度器用来寻找放置方案的每一个字段都必须在 Pod 之间保持一致,而用到某些功能时,为了保证正确性,批处理会被关闭。文档特意提醒你,去检查一下你的 kube-scheduler 配置是不是在隐式关闭批处理。
v1.35 的博客明确说这只是第一版实现,并预告了下一步:把整个 gang 放在一个周期内处理的调度阶段,以及工作负载级别的抢占。而这正是 v1.36 带来的东西。
v1.36 — Workload 是模板,PodGroup 是运行时
v1.36 发布了 scheduling.k8s.io/v1alpha2,把之前的 v1alpha1 完全替换掉了。API 在六个月之内就被换掉了。原因在 KEP 里写得很详细,也很有说服力。
原本的设计是把 PodGroup 塞进 Workload 的 spec 里。但 Workload 是长期存在的「配置意图」(configuration-intent)对象,而 PodGroup 是一个短暂的调度单元。把运行时的执行单元绑到一个持久化的定义对象上,会破坏关注点分离。除此之外,还会正面撞上可扩展性问题 — PodGroup 很多的大型 Workload,很容易碰到 etcd 1.5MB 的对象大小上限,而每次更新单个 PodGroup 的状态,都得对巨大的中心 Workload 对象做一次 read-modify-write,从而产生竞争。
所以就把它们拆开了。现在 Workload 是一个静态模板。
apiVersion: scheduling.k8s.io/v1alpha2
kind: Workload
metadata:
name: training-job-workload
namespace: some-ns
spec:
# 从 v1alpha1 的 podGroups 更名为 podGroupTemplates
podGroupTemplates:
- name: workers
# 从 v1alpha1 的 policy 更名为 schedulingPolicy
schedulingPolicy:
gang:
minCount: 4
然后由控制器从这个模板中生成运行时的 PodGroup。
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
name: training-job-workers-pg
namespace: some-ns
spec:
podGroupTemplateRef:
workload:
workloadName: training-job-workload
podGroupTemplateName: workers
schedulingPolicy:
gang:
minCount: 4
status:
conditions:
- type: PodGroupScheduled
status: "True"
lastTransitionTime: 2026-04-03T00:00:00Z
Pod 端的关联字段也变了。workloadRef 消失了,换成了 schedulingGroup。
apiVersion: v1
kind: Pod
metadata:
name: worker-0
namespace: some-ns
spec:
schedulingGroup:
podGroupName: training-job-workers-pg
值得留意的是,.metadata.ownerReferences 仍然指向「真正的」工作负载对象(比如一个 Job)。podGroupTemplateRef 只是说明这是从哪个模板生成的。所有权和来源被分开了。
这样一来,调度器的逻辑也变简单了。因为一个 PodGroup 里就包含了调度器所需的全部信息,也就没有理由再去 watch 或解析 Workload 对象了。
PodGroup 调度周期 — 原子性地评估整个组
v1.36 真正的核心就在这里。如果按 Pod 逐个顺序地评估、预留资源,会有死锁风险,所以调度器现在有了一个专门的周期,把整个组当作一个单元来评估。
调度器从队列里取出一个 PodGroup 成员时,不管策略如何,都会把该组剩余的等待中 Pod 收集起来,做确定性排序,然后走一个原子性周期。
- 对集群状态只拍一次快照 — 目的是在评估整个组的过程中防止竞态、保证一致性。
- 用 PodGroup 调度算法,为组里的每一个 Pod 寻找有效的节点放置方案。这一步的过滤(filtering)和打分(scoring)阶段,沿用的是现有基于 Pod 的逻辑。
- 把结果原子性地应用到整个组。
- 如果成功,能被调度的成员 Pod 会一起进入绑定阶段,其余 Pod 回到队列,等待资源释放。
- 如果失败,整个组会被判定为不可调度,没有任何 Pod 会被绑定,退避(backoff)之后重试。
这里有一个不起眼、但在运维上很重要的细节。如果往一个已经有部分 Pod 被调度的 PodGroup 里添加新 Pod,这个周期会在考虑已有 Pod 的前提下评估新 Pod。而且 已经分配到节点上的 Pod 会继续运行下去 — 即便这个组在之后的周期里无法满足要求,调度器也不会取消这些 Pod 的分配或把它们驱逐。也就是说,「全有或全无」是绑定那一刻的原子性,而不是整个组生命周期内始终维持的不变量。
minCount 的门控本身还在。调度器依然会在 PreEnqueue 阶段扣住 Pod,只是实际的调度阶段现在完全依赖新的 PodGroup 周期。算法运行过程中,会检查可调度的 Pod 数量是否满足 minCount,如果集群撑不住这个最小值,就什么都不绑定。
算法诚实交代的局限
这一部分是本文最重要的内容。而且值得称赞的是,这不是我自己的观察,而是 Kubernetes 项目在自己的博客里,用标题为「Limitations」的一节亲自写出来的。
第一版 PodGroup 调度周期的局限如下。
- 对于 同构(homogeneous)的 Pod 组 — 所有 Pod 的调度需求完全一致,且不存在亲和性、反亲和性、拓扑分布约束这类 Pod 间依赖的情况 — 只要放置方案存在,算法预期是能找到的。
- 对于 异构(heterogeneous)的 Pod 组,即便放置方案存在,也不保证能找到。哪怕答案看起来很显然也一样。
- 对于 存在 Pod 间依赖的组,同样没有保证。
除此之外,在组内部存在依赖的情况下(比如 Pod 间亲和性让某个 Pod 的可调度性取决于另一个成员),由于确定性的处理顺序,无论集群状态如何,都可能找不到放置方案。KEP 用另一种说法表达了同一件事 — 默认算法可能找不到某个原本可以找到的有效放置方案,只因为组里的 Pod 是按照当前这个顺序处理的,而不是别的顺序。
读完之后,适用范围就清晰了。目前这项功能真正瞄准的是 worker 完全相同的 PyTorch DDP 风格训练作业。像 leader 和 worker 规格不同的 LeaderWorkerSet、异构 GPU 资源池,或是掺杂了拓扑分布约束的工作负载,都还在保证范围之外。这不是 bug,而是 alpha 阶段的范围。
拓扑感知调度 — 第一版迭代
v1.36 让你可以直接在 PodGroup 上加拓扑约束。
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
name: topology-aware-workers-pg
spec:
schedulingPolicy:
gang:
minCount: 4
schedulingConstraints:
topology:
- key: topology.kubernetes.io/rack
调度器会尝试满足 rack 约束的各种节点组合,并根据 PodGroup 使用资源的效率、以及该拓扑域内能容纳多少个 Pod,来挑选最优的放置方案。为此,PodGroup 周期新增了基于放置(placement)的三段式算法 — 生成候选放置方案(PlacementGenerate 扩展点)、验证整个组是否真的能塞进每个候选方案、给可行的放置方案打分并选出最优(PlacementScore 扩展点)。
局限也写得很明确。目前的拓扑感知调度不会为了满足约束而触发 Pod 抢占;据称计划在未来的版本中把它和工作负载感知抢占整合起来。而多层拓扑级别、软约束(偏好)、与 DRA 的深度整合、以及和 basic 策略搭配使用时的稳健性,都还是留待未来的课题。也就是说,现在能用的只有 单层硬约束 这一种。
工作负载感知抢占
当 PodGroup 无法被调度时使用的一套新抢占机制也加入了进来。它和现有的按 Pod 抢占不同之处在于,它把整个 PodGroup 当作一个抢占者单元来看待。它不是逐个节点分别寻找牺牲者,而是在整个集群范围内搜索,同时在多个节点上驱逐 Pod,从而为整个组腾出空间。没有这个机制,gang 的抢占会变得很别扭 — 因为每个节点上只驱逐一点点,凑不出足够容纳整个 gang 的空间。
PodGroup API 新增了两个概念。
- priority — 覆盖组成 PodGroup 的各个 Pod 的优先级。
- disruptionMode — 指定组内的 Pod 是可以被单独抢占,还是必须全有或全无地一起被抢占。
apiVersion: scheduling.k8s.io/v1alpha2
kind: PodGroup
metadata:
name: victim-pg
spec:
priorityClassName: high-priority
priority: 1000
disruptionMode: PodGroup
这里有个陷阱。在 v1.36 中,这两个字段 只在工作负载感知抢占机制里 会被尊重。其他的中断(disruption)路径 — 包括现有按 Pod 周期里的默认抢占 — 都会忽略这两个字段。把它们扩展到其他中断来源,还只是未来的期望。所以不要把设置了 disruptionMode: PodGroup 理解成你的 gang 会被整体保护起来,免受一切驱逐。
与 DRA 的结合 — 256 个的墙被打开了
DRA 在 v1.34 中 GA(DRA 本身可参考Kubernetes DRA 与 GPU 调度一文),现在 PodGroup 可以充当 ResourceClaimTemplate 的复制单元了。
以前,引用一个 ResourceClaimTemplate,会为每个 Pod 各生成一个 ResourceClaim。多个 Pod 要共享同一个设备,就得直接按名字引用一个 ResourceClaim,而这就意味着那个 claim 得由用户自己创建和管理。现在,对于 PodGroup 的 spec.resourceClaims 所引用的 ResourceClaimTemplate 而言,无论组里有多少个 Pod,整个组只会生成一个 ResourceClaim。如果某个 Pod 的 spec.resourceClaims 条目和它所属 PodGroup 上的条目相符,那个 Pod 的 claim 就会解析到为该组生成的 ResourceClaim 上,不会再为它单独创建 claim。
更实际的变化在这儿。以前,kube-scheduler 只能在 ResourceClaim 的 status.reservedFor 里逐个列出 Pod,而这个字段有 256 个的上限。现在,status.reservedFor 里只要放一个 PodGroup 引用,就能代表远超 256 个的 Pod。这就为大规模工作负载以高基数(high-cardinality)共享设备打开了一条路。对于成百上千个 Pod 规模的训练作业来说,这不是理论上的改进,而是一堵实实在在挡在路上的墙被打开了。
Job 控制器集成 — 条件相当苛刻
如果你读到这儿,心里在想「那我是不是得自己创建 Workload 和 PodGroup」,v1.36 给出的答案是:如果是 Job,就不用。打开 WorkloadWithJob 特性门控之后,Job 控制器会自动创建 Workload 和运行时 PodGroup,给 Job 创建的每一个 Pod 都设置 .spec.schedulingGroup,并把生成对象的所有者指定为该 Job,这样 Job 被删除时它们也会被垃圾回收。
不过,为了让第一版迭代保持可预测,条件设得很窄。只有当 Job 同时 满足以下所有条件时,这个集成才会生效。
.spec.parallelism大于 1.spec.completionMode是Indexed.spec.completions等于.spec.parallelism- Pod 模板里还没有设置
schedulingGroup
逻辑很清楚 — 每个 Pod 得有稳定的身份(Indexed),gang 的大小得在 admission 阶段就固定下来(parallelism == completions),而且不能有别的控制器已经接管了调度职责。不满足条件的 Job,会像以前一样按 Pod 逐个调度。
最后一条尤其考虑周到。如果你已经自己在 Pod 模板上设置了 schedulingGroup(比如某个上层控制器正在管理这个工作负载),Job 控制器就不会碰这个模板,也不会创建它自己的 Workload/PodGroup。也就是说,即便在已经使用外部放置系统的集群上打开这个门控,也是安全的。
缺失的部分同样清楚。目前的约束把这个集成限定在静态的、Indexed 的、完全并行的 Job 上。对弹性(elastic)Job 和其他内建控制器的支持,正在 KEP-5547 中跟踪。
怎么打开它
在 v1.36 中这一切都是 alpha。先说前提条件。
- Workload/PodGroup API — 在
kube-apiserver和kube-scheduler上都打开GenericWorkload门控,并启用scheduling.k8s.io/v1alpha2API 组。
然后按你想要的功能逐一开启。
- gang scheduling — 在
kube-scheduler上开GangScheduling - 拓扑感知调度 — 在
kube-scheduler上开TopologyAwareWorkloadScheduling - 工作负载感知抢占 — 在
kube-scheduler上开WorkloadAwarePreemption(GangScheduling也必须同时打开) - DRA ResourceClaim 支持 — 在
kube-apiserver、kube-controller-manager、kube-scheduler、kubelet全部打开DRAWorkloadResourceClaims - Job 控制器集成 — 在
kube-apiserver和kube-controller-manager上开WorkloadWithJob
请留意,门控在不同组件上是分开打开的。光看这份清单就能得出结论:在托管集群(EKS、GKE、AKS)上,你短期内是碰不到这些的。你得能在控制平面上打开 alpha 门控,所以用 kubeadm 或 kind 搭的测试集群,才是现实中能实验的地方。
那要不要把 Volcano 和 Kueue 拆掉
不需要。而且这不是我的看法,而是 KEP-4671 的 Non-Goals 里写的内容 — 把公平性或多工作负载队列引入 kube-scheduler 并不是目标,Kueue 和 Volcano.sh 会继续提供这些能力。
这就把边界划得很清楚了。核心接过来的是一种 机制:「把这一批 Pod 一起、原子性地、考虑拓扑地放置好」。而「给哪个团队多少配额、队列里先取谁出来、借出去的资源什么时候回收」这类 策略,仍然是 Kueue 和 Volcano 的地盘。实际上,大多数组织当初引入 gang scheduler,正是为了后者。真正只需要 gang scheduling 本身的团队,比想象中要少。
同一份 Non-Goals 里还有这些 — 不从控制器手里拿走创建 Pod 的职责,集群自动扩缩容与 gang scheduling 的整合(暂时)不在范围内,工作负载级别的抢占不是(这份 KEP 的)目标,不同调度器之间的资源竞争(包括可能出现的死锁)不做处理。最后一条很说明问题 — kube-scheduler 和 Volcano 在同一个集群里一起跑时产生的问题,依然是你自己的问题。
为什么现在不该用这个
比起技术本身,这一段大概对实践者更有价值。
一切都是 alpha。 只有 opportunistic batching 是 Beta(默认开启),Workload API、gang scheduling、拓扑、抢占、DRA 集成、Job 集成全都是 alpha。
API 已经破坏性变更过一次,而且还计划再变一次。 v1.35 的 v1alpha1 在 v1.36 中被 v1alpha2 完全替换掉了。podGroups 变成了 podGroupTemplates,policy 变成了 schedulingPolicy,Pod 的 workloadRef 变成了 schedulingGroup,运行时状态也被拆分到了单独的对象里。就在六个月之内。
而 KEP 里写的 v1.37 计划,还要再走一步。计划是把 API 升级到 v1beta1,同时为了给计划中的 alpha 特性的 DisruptionMode 相关内容做不兼容的破坏性变更,新建一个 v1alpha3 来替换 v1alpha2。升级时需要做的事情,KEP 里写得很明确 — 必须先把全部 v1alpha2 资源删除掉再升级(v1.37 不支持它们),而从 v1alpha3 转换回 v1alpha2,在降级时是不支持的。此外,由于 GangScheduling 门控要被合并进 GenericWorkload,升级时得从配置里去掉 GangScheduling,而如果降级回 v1.36,又得把它重新打开。(v1.37 计划于 2026 年 8 月 26 日发布,这里写的一切都是 KEP 中所述的 计划,在正式发布前都可能变化。)
算法的保证范围很窄。 正如前面所说,在同构组之外,可能找不到放置方案。
KEP 自己也列出了风险 — 绑定阶段的竞争窗口比标准调度更长,对象数量增多导致 API 调用量和 etcd 对象数上升,Workload 和 PodGroup 这两个对象之间还存在一致性问题。虽说靠 GC 和 admission 控制器来缓解,但这不是免费的。
尽管如此,为什么还值得关注
因为方向很清楚。Kubernetes 迄今为止一直站在「Pod 是调度的最小单位」这个前提上,而这个前提在 AI 工作负载面前崩塌了。过去这几年的应对方式,全都是从外部绕开这个问题。现在,核心正在把工作负载当作一等公民来对待。
看 v1.37 的路线图,就能读出它要去哪儿 — 在把 Workload/PodGroup API 升级到 Beta 的同时,为弹性作业引入可变的 minCount,为 JobSet 或 LWS 这类基于分布式推理的复杂 AI 工作负载引入多层级层次结构,把拓扑感知调度和工作负载感知抢占升级到 Beta,以及面向真正的工作负载控制器的统一控制器集成 API。项目方补充说,这个优先级和实现顺序都可能改变。
现在你该做的不是迁移。而是在测试集群上用 kind 起一个 v1.36,打开门控,确认一下你的训练作业的形态是否符合 Job 控制器集成的条件(Indexed、parallelism == completions),以及你的 Pod 组是否算得上「同构」。如果两者都符合,那你正是这个功能瞄准的目标,等 Beta 到来时,你有可能相当早就能减轻对 Volcano 的依赖。如果这两条里有任何一条不符合 — 而且你又需要配额、排队 — 那接下来一段时间里,Kueue 和 Volcano 依然是答案。这不是失败,而正是 KEP 设计出来的样子。
结语
归纳一下:gang scheduling 已经在 kube-scheduler 之外被实现了至少四次,Kubernetes 从 v1.35 的 Workload API 开始把这项能力搬进核心,又在 v1.36 里把结构理顺。Workload 是静态模板,PodGroup 是运行时对象,还有一个在快照之上原子性评估整个组的 PodGroup 调度周期。在此之上,又加上了拓扑约束、工作负载级别的抢占、PodGroup 级别的 ResourceClaim 共享(打开了 256 的墙)、以及 Job 控制器自动集成的第一版迭代。
作为代价,这一切都还是 alpha,API 在六个月内破坏性变更过一次,还计划再变一次,算法的保证范围窄,仅限于同构 Pod 组,而公平性和排队从一开始就不是它的目标。所以这不是「Volcano 的替代品」,而是「Volcano 将来要站上去的那块标准地基」。看着这块地基一点点建起来,但先别急着把生产环境放上去。
参考资料
- Kubernetes v1.36: Advancing Workload-Aware Scheduling(官方博客,2026-05-13)
- Kubernetes v1.35: Introducing Workload Aware Scheduling(官方博客,2025-12-29)
- Kubernetes v1.36: ハル (Haru) — 发布公告 (2026-04-22)
- KEP-4671 — Workload API and gang scheduling
- KEP-5732 — Topology-aware scheduling
- KEP-5710 — Workload-aware preemption
- KEP-5729 — DRA ResourceClaim support for workloads
- KEP-5547 — Workload API support in Job controller
- Kubernetes v1.34: DRA has graduated to GA
- Kubernetes AI 训练流水线 — Volcano、Training Operator、Kueue(相关文章)
- Kubernetes DRA 与 GPU 调度(相关文章)
- 分布式训练平台对比(相关文章)