第 7 篇|Volcano Gang 调度:成组分配与绑定
本文最后更新于:2 天前
前言
第六篇中的 PodGroup 已经进入调度器,下面继续用 ray-demo-job-pg 作为示意名称。三个成员 Pod 都在等待节点。假设前两个成员能找到位置,第三个成员放不下,Volcano 会不会先启动前两个?
这篇从 Scheduler 的一次调度循环回答这个问题。示例使用一个调度主实例、一个 Queue ray-batch,不启用拓扑、分片或子组策略。三个成员 Pod 都显式声明非零资源请求,最低成员数为三。
调度器手里的对象与执行结构
第六篇看到的是 Kubernetes API 中的 Volcano Job、PodGroup、Queue 和 Pod。Scheduler 收到这些对象后,会在内存中建立便于反复比较的调度模型。后面的源码主要操作这些内存对象。
以 ray-demo-job-pg 为例,PodGroup 和三个成员 Pod 被组织成一个 JobInfo,每个 Pod 对应一个 TaskInfo。Queue 和 Node 也有相应的内存信息。调度器在这些对象上比较先后、计算资源和尝试放置,最终才把选中的 Node 写回 Kubernetes。
为了判断三个成员能否都找到可用节点,调度器先取一份本轮工作视图,在其中试放第一个、第二个成员,并记下各自占用的资源。第三个也能放下,才提交结果;第三个放不下,就需要撤销前面的尝试。Session 就是这份工作视图,Statement 则保存可提交或撤销的操作记录。
| 在调度循环中的角色 | 源码名称与作用 | 在本例中对应什么 |
|---|---|---|
| 长期缓存 | SchedulerCache 持续接收 informer 更新,保存调度器长期观察到的 Node、Queue、PodGroup 和 Pod 状态 |
调度循环开始前的最新集群视图 |
| 本轮视图 | Session 从缓存取得一轮快照,并保存本轮排序、资源账目和插件状态 |
一次 runOnce 使用的工作空间 |
| 作业 | JobInfo 表示一个 PodGroup 及其成员,包括最低成员数和成员状态 |
ray-demo-job-pg 与三个成员的集合 |
| 成员 | TaskInfo 表示一个成员 Pod,包括资源请求、当前状态和目标节点 |
三个成员中的任意一个 |
| 队列 | QueueInfo 保存一个 Queue 的需求、已分配量和本轮份额 |
ray-batch 的资源账目 |
| 节点 | NodeInfo 保存一个 Node 的空闲、已用、正在释放的资源和成员列表 |
每个候选节点能否再容纳一个成员 |
| 执行阶段 | Action 推进一轮调度中的一个阶段 |
enqueue、allocate、backfill |
| 判断规则 | Plugin 在判断点注册排序、过滤和就绪函数 |
gang、predicates、proportion 等 |
| 尝试记录 | Statement 记录本次尝试产生的分配或驱逐操作,以便整批提交或撤销 |
三个成员的试分配记录 |
JobInfo 和其中的 TaskInfo 描述要调度的作业与成员。Action 推进流程,在需要判断时调用 Plugin 注册的函数。Session 保存它们使用的本轮数据,Statement 记录尚未正式绑定的尝试。
先走完一次成组调度
把这些术语放进同一轮执行,ray-demo-job-pg 会经历下面几步:
| 阶段 | 主要对象 | 调度器做什么 | 结果保存在哪里 |
|---|---|---|---|
| 建立本轮视图 | SchedulerCache、Session | 复制节点、队列、作业和成员状态 | Session |
| enqueue 准入 | JobInfo、Queue | 判断这组需求能否进入后续分配 | JobInfo 的本轮状态与 PodGroup phase |
| 选择候选 | Queue、JobInfo、TaskInfo、NodeInfo | 依次选择队列、作业、成员和候选节点 | Session |
| 试分配 | TaskInfo、Statement | 暂时扣减节点资源,记录成员准备放到哪里 | Session 与 Statement |
| Gang 判断 | JobInfo | 统计已有和本轮试分配的成员,检查最低要求 | JobReady 判断结果 |
| 提交或撤销 | Statement | 达到门槛就保留这批操作;未达到就恢复本轮账目 | SchedulerCache,或恢复后的 Session |
| 异步绑定 | TaskInfo、SchedulerCache | 逐个向 Kubernetes 提交 Pod 与 Node 的绑定 | Kubernetes API |
“试分配、提交、绑定、运行”是四个连续阶段。试分配只修改 Session;Statement 提交后,调度结果进入长期缓存和绑定通道;绑定成功后,节点上的 kubelet 才开始准备并运行 Pod。
从缓存快照到执行策略
SchedulerCache 与 Session
Scheduler 通过 informer 观察资源变化,并把结果写入长期存在的 SchedulerCache。informer 是 Kubernetes 客户端中的观察机制:它持续接收 API 对象变化,同时维护一份本地缓存。周期性调用的 runOnce 再从 SchedulerCache 打开一轮 Session。
Session 是这一轮调度的工作视图。framework/session.go 从 cache.Snapshot() 取得数据,后续 Action 都在这份视图中工作。缓存快照在锁保护下构造,节点等对象被复制,因此试分配可以先修改本轮账目,不会直接改动 informer 正在维护的长期状态。
scheduler.go 中打开 Session 的连续节选如下:
1 | |
随后,Scheduler 按配置顺序调用 action.Execute(ssn)。同一轮里的后一个 Action 可以看到前一个 Action 已经写入 Session 的变化。关闭 Session 时,框架执行插件的收尾函数,并把需要持久化的状态更新交给后续流程。
快照只固定本轮计算的起点。计算过程中,真实节点可能故障,Pod 也可能被删除。绑定阶段重新面对实际集群状态,因此试分配成功以后仍然要处理单个成员的绑定失败。
Action、Plugin 与回调
pkg/scheduler/util.go 中的 DefaultSchedulerConf 定义了默认调度配置:
1 | |
Action 是主流程阶段:
- enqueue 处理作业准入,使符合条件的
JobInfo进入可调度状态。 - allocate 为声明了资源请求的
TaskInfo选择 Node。 - backfill 处理启动资源请求为空的 BestEffort 成员;这里的 BestEffort 指
TaskInfo.InitResreq为空。
主案例的三个成员都有资源请求,因此下面主要跟踪 enqueue 和 allocate。第八篇使用的 preempt、reclaim 是另外两个 Action,需要显式加入配置。
Plugin 不负责推进整段流程。Session 打开时,Plugin 把函数注册到不同判断点;Action 运行到相应位置时再调用它们。tiers 规定插件层级和调用顺序,同一个插件可以参与多个判断点,同一个判断点也可以组合多个插件的结果。
| 插件 | 在本篇调用链中的作用 |
|---|---|
priority |
比较 JobInfo 或 TaskInfo 的优先级 |
gang |
根据最低成员要求判断作业能否提交本轮分配 |
conformance |
在驱逐路径中保护系统关键 Pod;本篇配置未启用驱逐 Action |
overcommit |
根据集群总量、系数和已准入需求计算还能否接纳作业 |
drf |
比较作业的主导资源份额,第八篇用数字展开 |
predicates |
检查某个 TaskInfo 能否放到候选 Node |
proportion |
计算 Queue 的资源份额,并检查队列还能否继续分配 |
nodeorder |
为通过过滤的候选 Node 排序 |
例如,Session.JobOrderFn 是比较 JobInfo 先后的判断点,JobEnqueueable 是资源准入判断点,JobReady 是 Gang 门槛判断点。这些名字表示 Action 向 Session 发起的问题,具体答案由已经注册的 Plugin 函数组合得出。
从资源准入到成员试分配
enqueue:让作业进入可调度状态
先确认作业怎样取得后续分配的资格。actions/enqueue/enqueue.go 根据组的最低资源需求进行下面的判断:
1 | |
enqueue 处理整个 JobInfo,不为单个 TaskInfo 选择节点。有 minResources 时,JobEnqueueable 调用资源准入回调:overcommit 计算集群还能接纳多少需求,proportion 检查 Queue 状态、份额和上限。通过后,当前 Session 中的 JobInfo 进入后续分配,PodGroup phase 更新为 Inqueue。
未设置 minResources 时,这个条件直接通过,enqueue 阶段不做这组资源总量检查。allocate 仍然会检查 Queue 额度、节点条件和 Gang 门槛。
allocate:逐个选择成员和节点
allocate 依次选择 Queue、JobInfo、TaskInfo 和 NodeInfo。前两层决定当前轮到哪个队列和作业,TaskInfo 是这次准备放置的具体成员,NodeInfo 则是它的候选位置。
选择 Node 时会发生两类检查。proportion 等插件先判断 Queue 是否还能分配这份资源;predicates 再判断节点的剩余资源、亲和性、污点等条件是否允许这个 Pod 放入。集群总共有四张空闲 GPU,如果分散在四台机器上,一个需要同机两张 GPU 的 Pod 仍然找不到合法节点。
Gang 判断与 Statement
JobReady 统计什么
成员逐个找到候选节点以后,还要判断本轮结果是否达到整组要求。Gang 插件在 OnSessionOpen 注册的 JobReady 回调负责参与这个判断,以下为连续源码节选:
1 | |
PodGroup 的 minMember 进入内存模型后对应 JobInfo.MinAvailable。JobReady 接收的是整个 JobInfo;它检查作业级最低成员数,也预留了 task 和子组层面的门槛。本例没有额外配置 task 或子组策略。
这里的 Ready 是 Volcano 调度器内部的“成员数量已经满足提交条件”。ReadyTaskNum() 统计的状态包括:
| 内部状态 | 在这条路径中表示什么 |
|---|---|
Allocated |
已在 Session 中试分配资源,尚未提交 API 绑定 |
Binding |
已进入后台绑定流程 |
Bound |
已分配 Node,等待节点侧继续处理 |
Running、Succeeded |
Scheduler 已观察到 Pod 运行或成功结束 |
本例三个成员都声明了资源请求。只要三者在本轮试分配后进入 Allocated,JobReady 就可以达到 MinAvailable=3。Kubernetes Pod 的 Ready 条件由 kubelet 在容器运行后更新,发生在更后面的阶段。
Statement 怎样提交或撤销一次尝试
allocate 使用 Statement 记录本次尝试。Statement.Allocate 会把 TaskInfo 改为 Allocated,扣减 Session 中对应 NodeInfo 的空闲资源,并通知 Plugin 更新本轮账目。这些变化暂时只存在于调度器内存中。
假定三份资源总量足够,但第三个成员要求的节点没有余量。第一个成员找到节点,Session 扣除一份资源;第二个成员再扣一份;第三个成员找不到合法位置。此时 JobInfo 只有两个可计入的成员,没有达到 MinAvailable=3。stmt.Discard() 按相反顺序撤销前两次操作,恢复 TaskInfo 状态、节点空闲量和插件账目。
如果三个成员都试分配成功,普通 allocate 路径进入下面的提交分支:
1 | |
源码中还会出现 Pipelined。NodeInfo 除了当前空闲资源 Idle,还记录已经进入释放过程的 Releasing;两者合起来形成 FutureIdle()。当前空闲量放不下 TaskInfo、但未来空闲量可以放下时,allocate 可以先记录目标 Node,把成员标为 Pipelined,等待释放完成。这个状态没有执行 Pod 绑定。第八篇进入 preempt 和 reclaim 后,再解释哪些成员会进入 Releasing。
若 minMember 小于总副本数,达到最低成员数后就可以提交当前结果,剩余成员继续参与后续调度。
从 Commit 到 Pod 绑定
Statement 的 Commit 是调度器内部操作的提交点。它确认本轮记录的 Allocate 操作,并把每个待绑定成员交给 SchedulerCache。多个 Pod 的绑定仍然分别执行,没有合并成一笔 Kubernetes 事务。
SchedulerCache 的 AddBindTask 把成员改为 Binding,计入节点占用,再送入 BindFlowChannel。这个 channel 是前台调度循环和后台绑定协程之间的传递通道。后续 Session 因而能看到正在绑定的资源已经被占用。
后台的 BindTask 依次执行两类动作:PreBind 运行绑定前的插件准备,Binder 向 Kubernetes 提交 Pod 与 Node 的绑定。每个 TaskInfo 单独得到成功或失败结果。
绑定失败时,SchedulerCache.Bind 记录调度错误,执行相应的 PreBind 回滚,再通过 resyncTask 重新读取这个 Pod 的实际状态。假设前两个成员已经绑定成功,第三个成员因节点状态变化而失败,前两个结果继续保留;失败成员回到后续调度循环重新处理。
小结:Gang 在调度链中的位置
回到开头的问题:三个成员都在等待、最低成员数为三时,只找到两个位置还不能提交这次分配。前两个成员的试分配保存在 Session 和 Statement 中,第三个放不下就撤销这次尝试,等待后续调度。
Gang 的判断发生在试分配之后、Commit 之前。它保证提交时已经达到最低成员要求;Commit 之后,每个成员仍要分别完成绑定、容器启动和应用初始化。
下一篇加入第二个作业,沿 Queue、proportion 和 DRF 分析多个 JobInfo 怎样竞争资源,以及 preempt、reclaim 怎样把未来释放的资源交给等待者。