4. 调度器框架与插件机制
Volcano 的调度器并不是一个“大函数”,而是一套由 Scheduler 主循环、Session 会话快照、Cache 集群缓存、Action 调度动作 与 Plugin 扩展插件 共同组成的可插拔框架。理解这五者的职责边界与调用顺序,是后续编写自定义插件、排查调度异常、调整调度策略的基础。本节先建立整体架构认知,再沿着一次真实调度周期的数据流,逐层拆解每个组件的输入、输出与扩展点。
一、五层职责:谁负责什么
从静态结构看,Volcano 调度器可以划分为五个层次,它们各自承担明确且不重叠的职责:
- Scheduler:调度器进程的入口与主循环。它负责启动 Cache、加载配置、注册 Action 与 Plugin,并以固定周期(默认 1 秒)触发一次调度会话。它本身不决定“哪个 Pod 该去哪”,只负责“什么时候开始调度、按什么顺序执行动作”。
- Cache:集群状态的本地缓存。它通过 Informer 监听 Pod、Node、Queue、PodGroup 等资源,维护一份与 API Server 最终一致的内存快照。Cache 是调度决策的唯一数据来源,避免每次调度都直接访问 API Server。
- Session:一次调度周期的“工作台”。它从 Cache 复制出本次调度所需的节点、队列、作业快照,并持有本次会话的 Job 列表、Node 列表、PodGroup 状态以及各插件写入的中间结果。Session 是 Action 与 Plugin 之间共享数据的载体。
- Action:调度的“阶段”。Volcano 内置了 enqueue、allocate、preempt、backfill、reclaim 等 Action,每个 Action 负责调度流程中的一个明确阶段,按固定顺序依次执行。
- Plugin:调度的“策略”。插件通过实现特定接口,在 Action 执行过程中被回调,参与节点过滤、打分、作业排序、抢占判断等决策。插件是 Volcano 调度策略的主要扩展点。
这五者的关系可以概括为:Scheduler 驱动 Session,Session 承载 Cache 快照,Action 定义阶段,Plugin 注入策略。下面这张图展示了它们在一次调度周期中的协作关系与数据流向。
二、一次调度周期的完整数据流
把上图展开成时间线,一次调度周期大致经历以下阶段。假设集群中有 3 个待调度 PodGroup,分别属于 queue-a、queue-b、queue-c,其中 queue-a 已接近其 deserved 配额。
- Cache 同步:Scheduler 启动时先等待 Cache 完成首次同步(
cache.WaitForCacheSync),确保本地快照包含当前所有 Node、Pod、Queue、PodGroup。若同步未完成就调度,会出现“节点明明空闲却调度失败”的假象。 - 打开 Session:主循环调用
OpenSession,Session 从 Cache 复制出本次调度所需的 Job、Node、Queue 快照。注意这里是复制而非引用,Action 与 Plugin 对 Session 的修改不会立即写回 Cache,只有会话成功提交后才统一更新。 - 执行 Action 链:按配置顺序执行 enqueue → allocate → preempt → backfill → reclaim。每个 Action 内部会调用已注册插件的对应回调。例如 allocate 阶段会依次调用插件的
Predicate(过滤节点)和NodeOrder(节点排序)。 - Plugin 参与决策:以 gang 插件为例,它在 allocate 前会检查同一 PodGroup 内的 Pod 是否都能满足,若不能则整个 PodGroup 不分配,避免“部分启动、部分等待”的死锁。proportion 插件则在 Job 排序阶段按队列 deserved 计算优先级。
- 关闭 Session 并提交:所有 Action 执行完毕后,Scheduler 调用
CloseSession,将本次会话中产生的绑定决策(Pod → Node)批量提交给 API Server,并更新 Cache。
这里有一个容易混淆的点:Action 决定“做什么阶段”,Plugin 决定“这个阶段怎么判断”。例如 allocate 这个 Action 本身不包含任何打分逻辑,它只是遍历 Session 中的 Job,然后调用插件链完成过滤与排序。因此,替换或新增插件不会改变调度阶段,只会改变每个阶段内部的决策结果。
三、扩展点:自定义插件应该挂在哪里
Volcano 的插件接口按调度阶段划分,常见的扩展点包括:
| 扩展点 | 触发时机 | 典型用途 |
|---|---|---|
OnSessionOpen | Session 创建后 | 注册 Job/Node 排序函数、初始化插件内部状态 |
JobOrderFn | allocate 前对 Job 排序 | 按队列优先级、提交时间、DRF 份额排序 |
PredicateFn | 为 Pod 选择节点时 | 过滤不满足资源、亲和性、污点的节点 |
NodeOrderFn | 候选节点打分 | binpack 紧凑放置、spread 分散放置 |
PreemptableFn | preempt 阶段 | 判断某 Job 是否可被抢占 |
ReclaimableFn | reclaim 阶段 | 判断队列资源是否可回收 |
编写自定义插件时,通常只需实现 Plugin 接口并注册到 Scheduler 的插件列表中。例如一个“只允许在带 gpu=true 标签的节点上调度”的插件,核心逻辑就是在 PredicateFn 中检查节点标签,不满足则返回拒绝。插件注册后,Scheduler 会在对应阶段自动回调,无需修改 Action 代码。
--v=4,观察每个 Action 执行前后 Session 中 Job 状态的变化。常见现象是:插件 Predicate 返回拒绝后,Pod 会进入 Pending 并记录事件,但不会立即重试,需要等下一个调度周期。
四、一个可观察的调度示例
假设集群有 2 个节点,各 4 CPU、8Gi 内存。queue-a 的 deserved 为 4 CPU,当前已使用 3 CPU。此时提交一个 gang 作业,包含 2 个 Pod,每个请求 1 CPU。调度过程如下:
周期 T:
Cache 快照:node-1 剩余 1 CPU,node-2 剩余 4 CPU
Session 打开:加载 PodGroup pg-a(minMember=2)
Action enqueue:pg-a 进入待调度队列
Action allocate:
JobOrderFn:pg-a 排在 queue-b 的作业之前
PredicateFn:node-1 满足 1 CPU,node-2 满足 1 CPU
gang 插件检查:两个 Pod 都能找到节点 → 允许分配
绑定:pod-1 → node-1,pod-2 → node-2
CloseSession:提交绑定,Cache 更新 node-1 剩余 0 CPU
如果 gang 插件发现只有一个 Pod 能找到节点,它会拒绝整个 PodGroup 的分配,两个 Pod 都保持 Pending。这正是 gang 插件存在的意义:避免分布式训练任务因部分 Pod 启动而互相等待。观察结果时,可以查看 PodGroup 的 status.phase 是否为 Inqueue 或 Running,以及 Pod 事件中是否出现 not enough resources 或 gang scheduling rejected 等提示。