Scheduler Framework
Scheduler Framework
状态:Accepted;v0.x 实现完成(Filter、Score、Reserve/Assume/Bind、Run 间亲和性和有界可观测性)
本文定义 kruntimes 的目标调度架构。它将当前“每个 Pending Run 独立 reconcile”的模型替换为 scheduler queue 与单 Run scheduling cycle。每个 cycle 针对一个 Run,读取 Runtime Pods、active assignments 和 scheduler-local assumed assignments 的一致快照。
问题
当前 scheduler 每次只处理一条 Pending Run:列出 Runtime Pods 和 Runs,过滤 candidate,选择一个 Pod, 然后 patch 这一条 Run 的 status。下一次 reconcile 从新的 cache snapshot 开始。
该模型有两个限制:
- filter、scoring、retry 和 capacity accounting 不断堆积在同一个 reconciler 中,未来很难加入或推理 priority 等 feature;
- candidate selection 与
Scheduledstatus patch 之间,没有 scheduler-local 的 tentative assignment representation; - required Run affinity 目前只看到已 assignment 的 active Runs。需要 co-locate 的一组 Pending Runs 无法看到彼此的预期 placement,因此 affinity cohort 无法可靠 bootstrap。
目标
- 无 capacity 或暂时无法满足约束的 Run 继续保持
Pending。 - 让 filters、scoring、reservations、binding、retry/wakeup behavior 都可以独立测试。
- 让选中的 assignment 在 status patch 完成前对后续 scheduling cycles 可见,而不把临时状态暴露到 Kubernetes API。
- 保持 scheduler/runtimed 边界:scheduler 决定 Runtime Pod;runtimed 负责 execution 和 local preparation。
- 为 priority 和 fairness 提供扩展点。
非目标
- 对全局所有 Pending Runs 进行一次优化 pass。
- Workflow-aware scheduling 或解释 Workflow job labels。
- 在本 PR 中修改 public Run affinity type。
- 取代 Kubernetes 对 Runtime Pods 自身的调度。
调度范围与队列
controller-runtime workqueue 为每条 eligible Pending Run 保存一个 (namespace, name) Run key,并合并同一
key 的重复激活。dequeue 一个 key 时,只为该 Run 执行一次 scheduling cycle。当前排序遵循
controller-runtime 的 event 和 requeue ordering;kruntimes 尚未额外施加 creation-time 或 UID ordering。
未来经过 review 的 priority policy 可以定义 ordering、aging 和 fairness。
以下 events 会创建或重新激活 queue entries:
- 一条 Run 变为 eligible to schedule;
- 一条 Runtime Pod 变为 ready、unavailable,或其 capacity 发生变化;或
- 一条 assigned Run 离开 active set 并释放 capacity。
对于 Runtime Pod 和 capacity events,scheduler 使用 controller-runtime local-cache 的 Run.spec.runtime
field index 查找引用该 Runtime 的 Pending Runs,并将它们的 Run keys 加入 queue。这不是 Kubernetes API Server
field selector,因此该查询必须继续使用 manager cache client,而不能改用 API reader。event handler 不会选择
Runtime Pod 或 patch Run;只有 queue worker dequeue 单条 Run key 后才会执行这些操作。
Planning Cycle
对一个 dequeue 的 Run,scheduler 执行:
- Snapshot:读取该 Run、其 namespace/runtime key 的 ready Runtime Pods、active assignments 和 assumed assignments。
- PreFilter:校验 scheduler 可见的 Run inputs,并对每个 Run 只编译一次 selector 或 resource state。
invalid data 是永久 configuration failure;防御性处理应记录带 actionable reason 的 terminal
Failedstatus。 - Filter:移除不 ready、runtimed readiness stale、没有 unreserved capacity、违反 bound workspace placement,或违反 required affinity/anti-affinity term 的 Pods。
- Score:按 preferred affinity、available capacity 和 least loaded 对 eligible Pods 打分。Pod name 用于 stable tie breaking。
- Reserve and Assume:在 scheduler-local assumed cache 中记录选中的 Pod 并消耗 capacity。后续 Run cycles 可以看到这个 tentative assignment。
- Bind:将该 Run patch 为带 Pod name 和 UID 的
Scheduled。resource-version conflict 或 stale Pod observation 会释放该 Run 的 reservation 并重新 enqueue 它;这不是 terminal failure。
Filter 插件
scheduler 会对 snapshot 中的每个 Runtime Pod 执行已注册的 Filter 插件。一个插件接收不可变的 scheduling snapshot、该 Run 预计算后的 state 和一个 Pod,并返回 feasible 或一个有界的 rejection reason。插件不能 patch Kubernetes object、修改 assumed-reservation cache,或自行作出 placement decision。
初始 registry 包含两个独立 filter,并按照确定性的注册顺序执行:
- RuntimePodAvailability:校验 Runtime Pod readiness、runtimed heartbeat freshness,以及完整 logical resource request 是否能被 effective capacity 满足。
- RunAffinity:针对 actual 和 assumed targets 计算 required Run affinity 与 anti-affinity,其中包括 Run 间亲和性 bootstrap rule。
planner 在 PreFilter 阶段对每个插件的 Run-specific state 只准备一次;随后对每个 Pod 运行 filters,在第一个 rejection 后停止处理该 Pod。只有被所有 filter 接受的 Pods 才会进入 Score。rejection reasons 只能聚合成有界的 Pending status message 和 metrics,不能暴露 Pod names、selectors 或 Run names。
preferred affinity 仍是 scoring concern,而不是 Filter plugin。它只表示 feasible Pods 之间的偏好,不能使其他原本 feasible 的 Pod 变为不可调度。Reserve、Assume 和 Bind 仍是唯一允许修改 scheduler-local placement state 的操作。
Score 插件
scheduler 会对每个通过 Filter 的 Pod 应用每个已注册 Score plugin。一个 plugin 接收不可变 snapshot、预计算后的 Run state 和一个 candidate Pod,并返回一个 score。它不会移除 candidates、选择 Pod、patch Kubernetes object 或修改 reservation state。
score 使用包含 0..100 的闭区间,数值越大越好。如果 plugin 需要在 feasible Pod 集合中比较它的结果,它可在所有 raw
score 已得出后,选择实现 framework-owned 的 normalization。normalized value 仍必须在 0..100 范围。framework 将每个 normalized
plugin score 乘以已注册的正整数 weight,为每个 Pod 累加这些乘积,再按 total 降序排名。对 total 相同的 Pod,选择
lexicographically 最小的 Pod name。不同于 Kubernetes scheduler 对 equal-score 的随机选择,这个 final tie break 特意保持确定性,以便
Run placement 与测试可重复。
初始 registry 和 fixed internal weights 为:
- PreferredRunAffinity(weight
1)为每个 candidate 累加匹配的 preferred affinity 和 anti-affinity term weights,然后在 feasible Pods 之间对 raw values normalization。满足更多 preferred terms 的 Pod 会获得更高分。 - LeastLoaded(weight
1)评估分配 Run 后的 projected complete logical-resource utilization。它比较 dominant utilization,再比较 total utilization,并将这个比较 normalization,使更低的 projected utilization 获得更高分。
这会改变之前 preferred affinity 与 least-loaded placement 之间的 strict precedence:两个 signal 现在共同贡献于一个 weighted total。 plugin registration 和 weights 在 v0.x 仍是 internal implementation details。暴露 user-configurable scoring policy 需要单独的 API design。
Score plugins 必须显式返回自己的 errors。scoring error 会中止 planning cycle,不创建 assumption,也不写入 Run status;normal controller retry 负责处理 transient errors。Filters 继续负责 unschedulability 和有界的 Pending messages。
每个 reservation 属于一条 Run。与 Kubernetes 一样,assumed placement 让后续 scheduling cycles 在 status patch 完成前看到 capacity consumption 和 affinity target。bind 失败会释放 reservation 并移除 assumed assignment;成功 patch 后,该 Run 最终会作为 actual assignment 被观察到。reservations 不会以 annotation、 capacity counter 或 user-visible status 字段持久化。
Reservation 生命周期
assumed cache 按 immutable 的 (namespace, Run UID) 建键。每一项保存 Run name、选中的 Pod name 以及完整 logical
resource request。传入 Bind 的 Run object 保留了观察到的 resource version,因此 Kubernetes 的 status update 自身会
检测并发变更。mutex 保护 reserve、release 和 snapshot accounting,使并发 queue workers 不会重复预留同一份 capacity。
snapshot accounting 为:
effectiveUsage[pod] = activeAssignedRunUsage[pod] + unconfirmedAssumedUsage[pod]
在加入 assumed usage 前,scheduler 将 cache 与 snapshot 中的 Run list 对账。当对应 Run 被观察为同一 Pod 上的
active assignment 时,删除 assumption,因为此时 active Run usage 接管 reservation。若同一 Run UID 已 terminal、
不存在,或被分配到其他 Pod,也删除 assumption。观察到 Pending 时必须保留 assumption:informer 可能尚未看到
成功的 Bind,若在此窗口删除会允许 overcommit。这样 assumed usage 到 actual usage 的 handoff 是精确的,不会
double-count。
Reserve 会原子地确认选中 Pod 在考虑全部现有 assumptions 后仍有 capacity,然后写入完整 request。Bind
将 Run status patch 为选中 Pod 的 name 和 UID。成功 patch 后故意不释放 assumption:它保留到 informer 观察到
actual Run assignment。包括 resource-version conflict 在内的任何 failed patch 都立即释放 assumption。conflict
时 Run 保持 Pending 并 requeue;non-conflict API error 按普通 controller retry 返回。
如果 scheduler process 在 reserve 与 bind 之间停止,leader failover 从空的 assumed cache 启动。新 leader 仅从 persisted active Run assignments 重建 capacity。因此未持久化的 assumption 会消失;成功的 status patch 在被观察后 成为 actual usage;不需要单独的 reservation recovery object 或 TTL。
高可用
高可用与 queue 和 affinity 语义分离。初版 implementation 要求整个集群只有一个 active scheduler planner。Helm deployment 已启用 controller-manager leader election,因此 standby replicas 不会消费 Run keys,也不会写入 Run assignments。
leader failover 后,新 active planner 从空的 assumed cache 开始:它从 Pending Runs 重建 queue,并从 assigned active Runs 重建 capacity。尚未完成 status patch 的 assumed assignment 会因此消失;已成功 patch 的 Run 则会作为 actual assignment 被观察到。未来若要 scheduler sharding,需要独立的 ownership design;本文不 隐含这一能力。
Affinity 语义
required 和 preferred affinity terms 继续使用现有 namespace-local Run labels 和
kruntimes.io/runtime-pod topology。每个 scheduling cycle 中,term 可以匹配:
- actual target:已经 assignment 到 Runtime Pod 的 active Run;或
- assumed target:一条具有 scheduler-local reservation、但 status patch 尚未完成的 Run。
这样后续 Run cycle 可以和较早的 tentative assignment co-locate,同时仍遵守 capacity。
Run 间亲和性
如果 required runAffinity term 没有 actual 或 assumed matching target,当前 Run 只有在自己的 labels 也
匹配该 term selector 时,才能作为 cohort seed。scheduler 随后选择满足其余 hard constraints 的任意 Pod
并记录 assumed assignment。后续 matching Runs 可以使用这个 assumed target。
该规则遵循 Kubernetes 对第一个 matching workload 的 bootstrap exception。在 kruntimes 中称为Run 间亲和性: 没有 matching member 时,第一个 member 可以被调度,前提是它匹配该 term 自身。它避免 homogeneous affinity cohort 永远等待,同时保留 required constraint 的含义。
此规则不能让无关的 label dependency 自动满足。如果 Run A 只要求 Run B 拥有的 labels,而 Run B 只要求
Run A 拥有的 labels,二者都不能成为 placement seed。它们会带着 affinity waiting reason 保持 Pending,直到
出现 matching 的 actual 或 assumed target。
Status 与 Retry 语义
| 情况 | Run 状态 | Scheduler action |
|---|---|---|
| 没有 ready Pod、capacity 或当前可满足的 required affinity | Pending | 记录有界 waiting reason,并在相关变化或 backoff 到期时重新激活。 |
| scheduler-visible constraint 非法 | Failed | 记录 actionable terminal reason;不能 hot-loop。 |
| 无法满足 preferred affinity | 存在其他 feasible Pod 时为 Scheduled | 继续正常 scoring;preference 不是 hard constraint。 |
| Bind conflict 或 stale snapshot | Pending | 释放 assumed assignment 并重新 enqueue Run。 |
| Bind 后 Runtime Pod 不健康 | 现有 retry/reassignment flow | scheduler 不再创造独立 retry engine。 |
任何 terminal transition 都必须使用 shared terminal-status helper,保证 conditions 和 completion time 保持 normalized。
可扩展性
framework 有显式的 internal extension points:
- Queue ordering:当前使用 controller-runtime event/requeue ordering;未来 priority design 可以定义 priority classes、aging、quotas 和 fairness。
- PreFilter/Filter:Runtime readiness/capacity 和 required affinity 是独立注册的 Filter plugins, 而不是一个 reconciler 中的 branches。未来 hard predicates 也遵循该 contract。
- Score:plugin 独立为每个 feasible Pod 打分;framework 在需要时 normalization,应用已注册的 weight 并聚合 total,之后执行确定性 Pod-name tie breaking。future user-configurable scoring policy 需要单独的 API design。
- Reserve/Assume/Bind:assumed assignment 会让选中的 Runtime Pod 和已消耗的 capacity 在 Run bind 前 对后续 Run cycles 可见。
可观测性
实现已保留 scheduling-cycle duration、Run queue duration 和 scheduling-result metrics。 framework 增加以下 counters:
kruntimes_scheduler_filter_rejections_total{plugin,reason}:每当一个 Runtime Pod evaluation 被 Filter plugin 拒绝时增加一次。两个 label 都来自已注册的内部 plugin 及其有界 rejection-reason set。kruntimes_scheduler_reservation_conflicts_total{stage}:当Reserve发现已选择的 Pod 不再有 capacity,或者Bind遇到 Kubernetes resource-version conflict 时增加。stage只有reserve和bind两个有界值;其他 API failure 仍然是 controller error,而不是 reservation conflict。kruntimes_scheduler_pending_run_wakeups_total{source}:Runtime Pod event 或 capacity release event 每发出一个 Pending Run key 时增加一次。source只有runtime_pod和capacity_released两个有界值。 这个 counter 测量 requested queue activation;controller-runtime 随后可能合并同一个 key 的重复 request。
Metric labels 不能包含 Runtime name、Run name、Pod name、namespace、selector、resource name 或其他 user-controlled value。这样 time series 的数量只由 scheduler implementation 决定并保持有界。
实现顺序
- Review 本架构,并更新 Run affinity design,使本文成为 scheduling execution semantics 的权威来源。
- 在 controller-runtime queue 和 snapshot/planning interface 后重构 scheduler internals,同时保留当前 one-Run observable behavior 与 existing metrics。该 core 步骤已完成;它不会引入 assumed reservation 或 affinity semantics。
- 实现 Reserve/Assume 和 Bind,并增加 deterministic selection、assumed capacity accounting、handoff 到 actual assignments 以及 bind conflicts 的 unit tests。此步骤已完成。
- 实现独立的 RuntimePodAvailability 和 RunAffinity filters、assumed-target matching 以及 Run 间亲和性 bootstrap,并增加 unit、integration 与 E2E coverage。此步骤已完成。
- 为 Pending Run wakeups 增加 Runtime field index,同时保持现有 coalesced-key 和 one-Run-cycle behavior。 此步骤已完成。
- 引入用于 preferred affinity 和 capacity placement 的 weighted Score plugins,包括 score normalization、framework-owned aggregation 以及确定性 Pod-name tie breaking。此步骤已完成。
- 增加 filter rejection、reservation conflicts 和 Pending Run wakeups 的有界 metrics。 此步骤已完成。
- priority 是 v1.0 工作;只有经过独立 API 与 fairness design review 后,才加入。