Kubernetes 跨调度器抢占:机制、冲突根因与选型实践
Kubernetes 跨调度器抢占:机制、冲突根因与选型实践
Kubernetes 的 kube-scheduler 原生支持"基于优先级的抢占",由调度框架的 PostFilter 扩展点和 DefaultPreemption 插件实现;但"跨调度器抢占"在社区中并没有统一标准——每个调度器只能基于全局(且往往是滞后的)资源视图独立决策,互相感知不到对方的优先级语义和绑定动作,多调度器并存时会出现决策冲突、重复抢占甚至资源超卖的问题。下面从机制、冲突根因、调度器对比和典型实例四个层面展开。
一、kube-scheduler 原生抢占机制回顾
调度框架把每个 Pod 的调度过程分为 scheduling cycle 和 binding cycle,而抢占发生在 Filter 失败之后的 PostFilter 扩展点。DefaultPreemption 插件的核心目标是:为一台节点找到一组"最优受害者 Pod",它们被删除后能腾出足够资源让高优先级的抢占者调度成功,同时尽量不违反 PDB、优先级尽量低、造成的 Pod churn 最小。
几个关键规则值得先明确:
- 触发条件:抢占必须发生在集群资源不足、且待调度 Pod(抢占者)优先级高于受害者的情况下;不存在待调度 Pod、或优先级不高于受害者时不应触发。
- 优先级来源:PriorityClass 中的整数值被解析后写入
podSpec.priority,这是全局唯一的排序依据。 - PDB 是尽力而为:调度器会尽量挑选不会违反 PodDisruptionBudget 的受害者,但若找不到这样的组合,仍会强行删除低优先级 Pod,即使其 PDB 被违反。
- 受害者挑选策略:多个节点可用时优先选择受害者优先级最低的节点;若这些 Pod 被 PDB 保护,调度器可能转而选择"优先级更高的受害者"所在的节点,但仍必须低于抢占者优先级。此外调度器还会考虑受害者之间的亲和性依赖(比如同节点上 server 依赖 database,删 database 就必须连带删 server)。
- nominatedNodeName:抢占决定后,抢占者的
status.nominatedNodeName会被置为候选节点,但 Pod 不一定最终落在这台节点上——如果等待受害者退出期间来了更高优先级的 Pod,nominatedNodeName会被清空,重新参与抢占。 - 两级抢占:Preemption(调度器主动)与 kubelet 的 node-pressure eviction(节点资源压力驱逐,基于 QoS 和实际用量)是两套独立机制,抢占逻辑本身不把 QoS 纳入选择依据。
二、跨调度器抢占:为什么"没有原生支持"
这是调研中最核心、也最容易被误解的部分。社区对多调度器并存的定位很明确:Kubernetes 只保证一个 Pod 由一个调度器调度(通过 spec.schedulerName 匹配 KubeSchedulerProfile.schedulerName,所有调度器名称必须唯一),但不提供跨调度器的抢占协调机制。早在 2017 年就有 issue 直指这一问题:如果调度器 A 调度的 Pod a 与调度器 B 调度的 Pod b 竞争同一节点资源,谁来执行抢占?如果各调度器有自己的优先级体系,会发生什么?社区当时的结论是多个调度器必须遵循同一套 Pod 优先级整数规则,否则无解。
根因可以归纳为四点:
- 共享全局资源视图但无分布式锁。每个调度器通过 watch API Server 看到的是同一份节点和 Pod 数据,但各自维护本地缓存,从决策到写回 Binding 之间存在时间窗口,天然可能把同一份"空闲资源"分配给两个不同 Pod。
- 优先级语义不互通。kube-scheduler 看的是
pod.spec.priority的整数值;Volcano 看的是 Queue 级别的 deserved 配额和 PodGroup 优先级;KAI-scheduler 有自己的拓扑/亲和扩展。同一个 Pod 在不同调度器眼中的"可抢占性"可能完全相反。 - 抢占执行路径各自独立。kube-scheduler 通过 API 删除受害者 Pod 触发驱逐;Volcano 的 preempt/reclaim action 只在自己的 session 内遍历 jobs;任何一方都不知道对方刚刚做出了什么抢占决策,极易出现"你抢我的、我抢你的"循环。
- 为规避冲突而切分节点池会带来新的代价。实践中大家常用节点 Label 把不同调度器限制在不同 Pool 里(比如在线业务走 kube-scheduler、AI 训练走 Volcano),这样虽然避免了资源冲突,但节点池彼此隔离后整体资源利用率下降,运维成本上升。
社区目前没有专门的 KEP 解决"跨调度器抢占协调",相关演进都在"让单个调度器内部的抢占更智能"这个方向上:
- KEP-4832(异步抢占):把抢占的 API 调用从调度周期中解耦,提升调度吞吐。
- KEP-4671 / v1.35 的 Workload API + Gang Scheduling:引入 PodGroup、minCount 等 all-or-nothing 语义,从 Pod 粒度走向组粒度调度。
- KEP-5710(Workload-Aware Preemption,v1.36 alpha):针对 MPI/AI 训练等紧耦合负载的"部分抢占"痛点,引入 preemption unit 和 delayed preemption——在确认高优 Pod 能完整放下之前,先不杀死现有 Pod,把一组 Pod 当作整体来抢占。
- scheduler-plugins 的 Preemption Toleration:从受害者一侧扩展 PriorityClass,定义"容忍抢占"的策略(比如保证运行时间窗口内不被抢),是对原生抢占的补充。
三、不同调度器的抢占能力对比
| 调度器 | 抢占实现位置 | 抢占粒度 | 优先级模型 | 跨调度器兼容性 | 主要风险点 |
|---|---|---|---|---|---|
| kube-scheduler(默认) | PostFilter → DefaultPreemption 插件 | 单 Pod | PriorityClass 全局整数 | 不感知其他调度器;只能靠节点池隔离 | 与第三方调度器共存时易被"抢"或"超卖"资源 |
| Volcano | session 内的 preempt(同 Queue 内)与 reclaim(跨 Queue)action | PodGroup / Job / Queue | Queue deserved 配额 + PodGroup 优先级,v1.15 起支持 gang 粒度抢占 | 通常与 kube-scheduler 按节点 Label 划分资源池共存;需 MutatingWebhook 改 schedulerName | Webhook 单点风险、节点池隔离导致资源利用率下降 |
| KAI-scheduler | 自有调度循环,调度与 binding 通过自定义 CR 解耦 | Pod / 节点组 | 拓扑/亲和感知的自有体系 | 与默认调度器并存时实测出现 OutOfCpu / OutOfMemory(绑定间隙被抢资源) | 轮询式资源视图滞后 + 绑定窗口冲突 |
| 自研/第二调度器 | 完全自定义 | 自定义 | 自定义 | 必须与所有其他调度器共用同一套 PriorityClass 整数规则 | 与默认调度器同时部署时会看到同一节点被两个 Pod 分别绑定 |
四、典型实例
案例 1:kube-scheduler 单调度器内的标准抢占
先定义两个 PriorityClass,再提交一个高优先级 Pod,当节点资源不足时,DefaultPreemption 会删除低优先级受害者并设置 nominatedNodeName。
1 | apiVersion: scheduling.k8s.io/v1 |
场景:节点 CPU 容量为 10,节点上已运行优先级分别为 0/1/2/3、request 分别为 3/1/5/1 的 Pod,此时提交优先级为 10、request 为 5 的高优 Pod。调度器会只抢占优先级 2 的那个 Pod(释放 5 个单位足够放下抢占者),而保留优先级 1 和 0 的 Pod 继续运行——目标是"最小化被抢占 Pod 的数量"。
案例 2:KAI-scheduler 与默认调度器共存导致的资源超卖(真实 issue)
KAI-scheduler 0.10.3 与默认调度器在同一 v1.28 集群并行运行后,KAI 调度的 Pod 频繁出现 OutOfCpu / OutOfMemory。根因分析发现:KAI 通过周期性轮询刷新节点资源视图(而非 watch API Server),存在滞后;且 KAI 的调度与 binding 阶段通过自定义 CR 解耦——KAI 选定节点后、binding controller 写入 Binding 之前,默认调度器可能已经把同一份"空闲资源"分配给了别的 Pod,导致 KAI 的 Pod 到达 kubelet 时资源实际已被占满。这就是跨调度器"决策窗口竞争"最典型的翻车现场,即使把调度周期压缩到 1ms 也无法根除。
案例 3:Volcano 与 kube-scheduler 按节点池隔离共存的通用模式
这是业界目前最主流的"跨调度器共存"做法:AI/大数据训练负载走 Volcano(通过 MutatingAdmissionWebhook 把这些 namespace 的 spec.schedulerName 改写为 volcano),在线服务走默认 kube-scheduler,并用节点 Label(如 scheduler=volcano)把两者限制在各自的节点池中。代价是:两个池之间资源不能互借,整体利用率低于单池;Volcano 自身的抢占(preempt)只在同一 Queue 内生效,跨 Queue 资源回收走 reclaim action,前提是目标 Queue 的 spec.reclaimable: true,并且 reclaimed 后原 Queue 的 Pod 要能满足"继续运行"的要求。Volcano v1.15 进一步把抢占粒度从 Pod 升级到 Gang,官方明确不建议在同一调度器中同时配置新旧两套 preempt/reclaim action。
五、实践建议
关于多调度器"能否共存、如何分工、有哪些坑"的系统性实践指南,见 一个集群,多个调度器:Kubernetes 多调度器共存实践指南。如果目标是"混合部署在线 + 离线/AI 负载",比单纯多调度器并存更稳妥的路径有三条:
- 留在单一 kube-scheduler 内,通过 v1.35+ 的 Workload API / PodGroup(KEP-4671)和 v1.36 的 Workload-Aware Preemption(KEP-5710)获得组粒度调度与抢占能力,避免引入第二个调度器;
- 采用 Volcano 这类成熟的多调度器方案时,务必配合节点 Label 划分资源池并接受利用率折损,华为 CCE 的文档也明确这是防止多调度器资源冲突的标准做法;
- 若必须引入自研调度器,需要严格对齐 PriorityClass 语义、缩短调度到 binding 的窗口,并对"另一调度器可能在窗口内抢走资源"这一事实做防御性设计(比如容忍失败重调度)。
跨调度器抢占目前仍是 Kubernetes 调度体系中一块"社区没有原生答案"的空白地带,工程上靠的是资源池隔离、优先级体系对齐和失败重试兜底;而社区演进的方向——从 Pod 粒度走向 Workload/PodGroup 粒度、把组作为整体抢占和延迟抢占——本质上都是在减少"需要一个以上调度器"的动机。

