KAI Scheduler 深入拆解(二):从 Pod 到 BindRequest 的控制面工作流
上一篇我把 KAI Scheduler 当成一个平台来读:它不是单个 scheduler 进程,而是一串围绕 CRD、controller 和 scheduling session 组织起来的控制面。
这一篇开始看它真正的“主干路径”:一个 workload 进入集群后,到底是怎么一步步变成调度决策,再变成真实节点绑定的?
如果要先给结论,我会这样概括:
在 KAI 里,提交 workload 并不会立刻进入“给 Pod 选 node”的同步流程,而是先被重写成更适合调度的 workload model,再进入周期性调度循环,最后通过
BindRequest异步落地。
这条路径一共可以拆成六步:
- workload 进入 Kubernetes
- pod-grouper 抽取 workload 语义,创建
PodGroup - scheduler 在一个 cycle 里对
PodGroup和Queue做 placement 决策 - scheduler 创建
BindRequest - binder reconcile
BindRequest并执行真实绑定 - PodGroup/Queue controller 回写状态,形成反馈闭环
先看整条链路
sequenceDiagram
participant U as User / Workload Controller
participant A as Admission
participant PG as Pod Grouper
participant PGCRD as PodGroup CRD
participant Q as Queue CRD
participant S as Scheduler
participant BR as BindRequest CRD
participant B as Binder
participant PGC as PodGroup Controller
participant QC as Queue Controller
participant PM as Prometheus / Usage DB
U->>A: 创建 Pod / Job / Ray / Kubeflow workload
A-->>U: 准入校验 / 变更
U->>PG: Pod 被 watch 到
PG->>PGCRD: 基于 top owner 创建或更新 PodGroup
U->>Q: workload 通过 queue 标签归属某个 Queue
S->>PGCRD: 在 snapshot 中读取 PodGroup
S->>Q: 在 snapshot 中读取 Queue
S->>BR: 为选中的 Pod 创建 BindRequest
B->>BR: reconcile BindRequest
B->>B: 执行 bind / 资源准备 / 回滚
B-->>U: Pod 绑定到目标节点
PGC->>PGCRD: 回写 workload 运行态和资源状态
QC->>Q: 聚合队列资源状态
QC->>PM: 暴露历史资源使用数据
PM-->>S: 为 time-based fairshare 提供历史 usage
这个 sequence 图里,真正最关键的转折点有两个:
- Pod -> PodGroup:KAI 把原始 workload 转成更适合调度的抽象。
- Decision -> BindRequest:KAI 把调度决策和执行绑定解耦。
只要把这两个转折点读懂,整个系统就顺了。
第一步:workload 进入集群,但 scheduler 还不会立刻直接处理它
KAI 支持的工作负载并不只有单 Pod。它非常强调 AI / batch / distributed workload 的调度语义,所以常见入口包括:
- 原生 Pod
- Job / CronJob(以及其他 batch 类上层对象)
- Deployment
- MPIJob
- Ray
- Kubeflow 相关 workload
- JobSet
- 以及其他带 owner reference 的上层对象
这些对象刚进入集群时,并不天然具备 KAI 需要的完整调度语义。
例如,scheduler 真正关心的问题往往是:
- 这些 Pod 是否必须一起启动?
- 它们属于哪个 queue?
- 它们默认优先级和 preemptibility 是什么?
- 它们有没有 topology constraint?
- 它们是不是多级 subgroup 结构?
这些信息如果散落在不同 workload 的不同字段里,后续调度逻辑会非常难维护。于是 KAI 先做了一次“语义收敛”。
第二步:pod-grouper 把 workload 重写成 PodGroup
pod-grouper 是整条链路里最容易被低估的组件。
很多人看 scheduler 时会把 pod-grouper 当成辅助服务,但实际上它承担的是“把 Kubernetes 原生 workload 翻译成 KAI workload model”的职责。
它在做什么
pod-grouper 监听 Pod,然后做三件事:
- 找到 Pod 的 top owner
- 根据 owner 类型选择合适的 grouping plugin
- 生成或更新
PodGroup
文档 docs/developer/pod-grouper.md 给出的核心思路非常清楚:它不是简单按 label 聚合 Pod,而是通过 owner chain 去推断真正的 workload 边界。
为什么一定要找 top owner
因为很多 Kubernetes workload 都有多层 owner reference。
例如:
- Pod 的 owner 是 ReplicaSet
- ReplicaSet 的 owner 又是 Deployment
如果只按 Pod 当前直接 owner 聚合,你得到的是“副本控制器层”的分组,而不是“业务 workload 层”的分组。KAI 的做法是继续往上找,找到最适合代表 workload 语义的对象。
这一步带来的收益很大:
- 同一个 workload 的 Pod 可以稳定归到同一个
PodGroup - 不同 workload 类型可以保留各自的分组逻辑
- 调度器不需要理解所有 workload CRD 的细节,只需要理解
PodGroup
PodGroup 才是 KAI 真正的 workload 主语
在 pkg/apis/scheduling/v2alpha2/podgroup_types.go 里,PodGroupSpec 已经不仅仅是一个 minMember 包装器,它承载的是 workload 的调度语义:
MinMemberMinSubGroupQueuePriorityClassNamePreemptibilityTopologyConstraintSubGroupsMarkUnschedulableSchedulingBackoff
这里最重要的不是字段多,而是字段的组合方式。
这意味着 KAI 在表达的是:
- 这是一个 gang 还是普通任务?
- 这是平面 workload 还是层级 workload?
- 它属于哪个资源队列?
- 它可以被抢占吗?
- 它有机架、zone、拓扑域要求吗?
也就是说,从这一步开始,系统不再是“对一堆 Pod 做 placement”,而是“对带有完整调度语义的 workload group 做 placement”。
第三步:Queue 把 workload 放进资源与公平性的上下文里
PodGroup 解决的是 workload 建模问题,Queue 解决的是资源分配问题。
在 KAI 里,queue 不是顺手加的一个标签,而是公平性和资源边界的核心对象。很多关键特性都建立在它上面:
- hierarchical queue
- quota / deserved quota
- over-quota share
- reclaim
- fair-share
- time-based fairshare
也就是说,scheduler 在做决策时,看到的不是“某个 Pod 能不能放到某个节点”,而是:
某个
PodGroup在它所属Queue的资源语境下,是否应该得到这次调度机会。
这是 KAI 跟只做 node fitting 的调度器非常不同的一点。
第四步:scheduler 在一个 cycle 里对 snapshot 做决策
等到 PodGroup 和 Queue 都就位以后,scheduler 才开始它真正擅长的事情。
这里的关键点不是某个单独算法,而是 周期性调度模型。
KAI scheduler 不是收到一个 Pod 事件就立刻同步做完全部决策,而是每个 cycle:
- 同步 cache
- 获取 snapshot
- 打开 session
- 执行 actions
- 关闭 session
这带来两个直接好处:
1. 所有决策基于同一份一致视图
在同一个 cycle 里,所有 action 和 plugin 都基于同一个 cluster snapshot 工作。
这对复杂特性尤其重要:
- gang scheduling
- reclaim
- preempt
- topology-aware placement
- dynamic resources
- queue fairshare
如果没有 snapshot 这一层,很多逻辑会变成“边读边改边抢锁”,复杂度会急剧上升。
2. scheduler 可以处理的是 workload 级别的组合决策
因为它不是只看一个 Pod,而是看 snapshot 里的:
- nodes
- queues
- podgroups
- bindrequests
- 资源状态
- 历史 usage
所以它做出来的是更接近“全局最优”或“局部一致最优”的决策,而不只是一个 request/response 式的即时选择。
第五步:scheduler 不直接 bind,而是创建 BindRequest
这是 KAI 整条工作流里最精彩的一步。
在 pkg/scheduler/cache/cache.go 里,SchedulerCache.Bind(...) 的逻辑不是直接调用 Pod binding API,而是创建 BindRequest。
BindRequestSpec 里会写入:
PodNameSelectedNodeReceivedResourceTypeReceivedGPUSelectedGPUGroupsResourceClaimAllocationsBackoffLimit
这说明什么?说明 scheduler 输出的不是一个轻量的“node choice”,而是一份 完整的绑定执行意图。
为什么这一步非常重要
如果 scheduler 直接 bind Pod,那它就必须自己同步承担这些职责:
- 资源声明与 DRA 处理
- 共享 GPU 相关准备
- 失败重试
- 失败回滚
- 状态更新
- 绑定副作用控制
这会让调度循环变得又慢又重。
KAI 的选择是把它们拆开:
- scheduler 专注于 决策
- binder 专注于 执行
中间用 BindRequest 这个 CRD 做状态中介。
这类设计最大的价值不是“看起来解耦”,而是真正在错误模型和并发模型上得到收益:
- scheduler cycle 可以更短
- binder 可以独立重试
- 执行错误不会把调度内核拖进复杂副作用里
- 中间态对象便于排障和观测
第六步:binder reconcile BindRequest,把决策变成真实集群状态
binder 读取到 BindRequest 后,会进入 controller-runtime 的 reconcile 流程。
它的大致步骤是:
- 取回
BindRequest - 找到对应 Pod
- 找到目标 Node
- 如果 Pod 已经绑定则退出
- 调用 binder 实现执行真实 bind
- 如果失败则 rollback
- 更新
BindRequest状态和 Pod condition
这里最值得注意的是两点。
1. binder 处理的是“执行型失败”
比如:
- 节点在短时间内变化了
- 资源声明失败了
- 共享 GPU 相关准备失败了
- 某些 bind-time side effect 没完成
这些都属于执行面的问题,而不是 placement 逻辑本身的问题。
KAI 通过 binder 把这些风险从主调度循环里隔离了出来。
2. binder 能做 rollback
这很关键。
当 bind 失败时,binder 会尝试 rollback。对于一个支持 DRA、GPU sharing、复杂资源声明的系统来说,这是必须的。
因为 scheduler 的决策不只是“选 node”,它往往还隐含了若干资源分配语义。如果失败后没有清理干净,后面很容易出现资源状态污染。
最后一环:状态回写与历史反馈
如果工作流只到 binder 为止,那 KAI 还只是一个“高级 scheduler”。
它之所以更像平台,是因为后面还有完整的反馈闭环。
PodGroupController
负责把 workload 运行态、资源状态、调度条件回写到 PodGroup。
这让 PodGroup 不只是“调度前的输入”,也变成“调度后的观测对象”。
QueueController
负责聚合 queue 层的资源状态,为公平性和资源分配提供基础。
Prometheus / usage DB
当 time-based fairshare 打开以后,这条反馈链会继续延伸:
- pod-group-controller 更新 podgroup 资源状态
- queue-controller 聚合 queue 级 usage
- Prometheus 存储历史 usage
- scheduler 下一轮从 usage DB 读取历史数据
这就让 KAI 不只是“看当前资源”的 scheduler,而是一个能把 历史使用行为 纳入决策的系统。
KAI 的 workflow 为什么适合 AI / batch workload
把整条链路看完以后,会很容易理解它为什么适合 AI 场景。
1. 它先建模,再调度
AI workload 往往不是单 Pod,而是:
- 多副本训练
- 分布式推理
- 分层通信结构
- gang / subgroup / topology 约束
KAI 通过 PodGroup 先把这些语义固定下来,再做 scheduling,避免把 workload 复杂性泄漏进每个 action/plugin 的实现里。
2. 它先决策,再执行
AI workload 的 bind-time 复杂度通常更高:
- GPU sharing
- DRA / ResourceClaim
- volume / storage 约束
- 失败回滚
用 BindRequest 把 decision plane 和 execution plane 分开,能让调度器更专注,也让系统更稳定。
3. 它有反馈闭环
公平性不只是“这一刻谁先跑”,而是“长期来看谁占用了多少资源”。
KAI 的 queue controller + Prometheus + usage DB 让它可以逐步走向时间维度的公平性,而不是只做瞬时队列排序。
我对这条工作流的一个总结
如果把 KAI 的控制面 workflow 用一句话概括,我会写成:
先把 Kubernetes workload 翻译成 KAI 的 workload language,再在一致性 snapshot 上做调度决策,最后把决策通过异步 binding pipeline 落地,并把结果反馈回 queue 与 workload 状态。
这条链路的好处是系统边界非常清楚:
- pod-grouper 负责“翻译 workload”
- scheduler 负责“决定谁该上、上到哪”
- binder 负责“把决定真的执行掉”
- controller 们负责“把执行结果重新喂回系统”
下一篇看什么
理解 workflow 以后,下一步最自然的问题就是:
- snapshot 到底是什么?
- session 里都存了什么?
- action 和 plugin 究竟谁负责流程,谁负责策略?
- KAI 为什么要引入 statement / scenario 这种“事务化模拟”模型?
下一篇就进入调度内核本身,拆解 KAI 最核心的设计:
cycle、snapshot、session、action、plugin、statement。