KAI Scheduler 深入拆解(二):从 Pod 到 BindRequest 的控制面工作流

上一篇我把 KAI Scheduler 当成一个平台来读:它不是单个 scheduler 进程,而是一串围绕 CRD、controller 和 scheduling session 组织起来的控制面。

这一篇开始看它真正的“主干路径”:一个 workload 进入集群后,到底是怎么一步步变成调度决策,再变成真实节点绑定的?

如果要先给结论,我会这样概括:

在 KAI 里,提交 workload 并不会立刻进入“给 Pod 选 node”的同步流程,而是先被重写成更适合调度的 workload model,再进入周期性调度循环,最后通过 BindRequest 异步落地。

这条路径一共可以拆成六步:

  1. workload 进入 Kubernetes
  2. pod-grouper 抽取 workload 语义,创建 PodGroup
  3. scheduler 在一个 cycle 里对 PodGroupQueue 做 placement 决策
  4. scheduler 创建 BindRequest
  5. binder reconcile BindRequest 并执行真实绑定
  6. 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,然后做三件事:

  1. 找到 Pod 的 top owner
  2. 根据 owner 类型选择合适的 grouping plugin
  3. 生成或更新 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 的调度语义:

  • MinMember
  • MinSubGroup
  • Queue
  • PriorityClassName
  • Preemptibility
  • TopologyConstraint
  • SubGroups
  • MarkUnschedulable
  • SchedulingBackoff

这里最重要的不是字段多,而是字段的组合方式。

这意味着 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 做决策

等到 PodGroupQueue 都就位以后,scheduler 才开始它真正擅长的事情。

这里的关键点不是某个单独算法,而是 周期性调度模型

KAI scheduler 不是收到一个 Pod 事件就立刻同步做完全部决策,而是每个 cycle:

  1. 同步 cache
  2. 获取 snapshot
  3. 打开 session
  4. 执行 actions
  5. 关闭 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 里会写入:

  • PodName
  • SelectedNode
  • ReceivedResourceType
  • ReceivedGPU
  • SelectedGPUGroups
  • ResourceClaimAllocations
  • BackoffLimit

这说明什么?说明 scheduler 输出的不是一个轻量的“node choice”,而是一份 完整的绑定执行意图

为什么这一步非常重要

如果 scheduler 直接 bind Pod,那它就必须自己同步承担这些职责:

  • 资源声明与 DRA 处理
  • 共享 GPU 相关准备
  • 失败重试
  • 失败回滚
  • 状态更新
  • 绑定副作用控制

这会让调度循环变得又慢又重。

KAI 的选择是把它们拆开:

  • scheduler 专注于 决策
  • binder 专注于 执行

中间用 BindRequest 这个 CRD 做状态中介。

这类设计最大的价值不是“看起来解耦”,而是真正在错误模型和并发模型上得到收益:

  • scheduler cycle 可以更短
  • binder 可以独立重试
  • 执行错误不会把调度内核拖进复杂副作用里
  • 中间态对象便于排障和观测

第六步:binder reconcile BindRequest,把决策变成真实集群状态

binder 读取到 BindRequest 后,会进入 controller-runtime 的 reconcile 流程。

它的大致步骤是:

  1. 取回 BindRequest
  2. 找到对应 Pod
  3. 找到目标 Node
  4. 如果 Pod 已经绑定则退出
  5. 调用 binder 实现执行真实 bind
  6. 如果失败则 rollback
  7. 更新 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。