想象你是一家大型房屋租赁公司的金牌中介。每天都有形形色色的租客上门:有程序员要求必须养猫、有家庭需要三居室朝南、有创业公司要一整层楼并自带机房。作为中介,你的工作分成两个固定步骤:第一步“硬性筛选”——养猫的必须有开放宠物政策的房源,要机房的必须电容量足够,不满足的一律不看;第二步“软性评分”——在剩余房源里,离地铁近的加分、采光好的加分、楼龄新的加分,最后把最高分的那把钥匙交给租客。 Kubernetes 里的调度器就是这样一位“金牌中介”。租客叫 Pod(运行应用的最小单元),房源叫 Node(集群里的机器),整套匹配流程叫“调度”。但真正的挑战在于:每家公司对“好房子”的定义都不一样——做 AI 大模型的想要 GPU 机房,做在线交易的想让服务靠近用户,做灾备的想让副本分散在不同城市。为此,K8s 提供了 Scheduler Framework,把调度流程拆成一个个可插拔的“环节”,允许你像定制中介服务手册一样,加入自己的筛选和打分规则。这篇文章,我们就从“中介手册”的视角,把 K8s 调度器扩展机制讲透。
掌握 K8s 调度器扩展,是云原生架构师设计高并发资源池、降低推理成本、保障服务高可用的关键能力——它能让你把“好钢”精确地用在“刀刃”上。
可视化演示
展示调度器过滤与打分两阶段为Pod选择最优Node、调度流程的完整链路、以及Scheduler Framework扩展点与GPU感知调度机制。
本节配有交互动效(进入视口后自动加载,约 3KB)。
过滤与打分两阶段 Filter删除不满足约束的节点 Score对剩余节点打分选最高 Filter → Score → 最优 为Pod选择资源均衡的Node
调度基础流程
中介接到租客张先生的电话后,先在台账里圈出 50 套房源,再快速扫一眼硬性要求:必须朝南、预算不超过五千。这第一步动作,就是把“整本房源册”缩小成几页候选清单。K8s 调度器干的是同一件事:它不需要亲自把画像挂上墙,只需要回答一个核心问题——这个 Pod 应该放到哪台机器上?
在 K8s 里,租客是 Pod,房源是 Node,金牌中介是控制平面中的 kube-scheduler 组件。它不运行任何业务容器,只负责在集群里寻找最优节点。寻找过程被拆成两个标准阶段:先过滤再打分。
过滤阶段筛出满足硬性条件的节点集合,打分阶段再按加权得分挑出唯一最优节点。这个“筛选→评分→选定”的过程构成一个调度周期(scheduling cycle)。每次有新的 Pod 创建,调度器就运行一个周期;Pod 停留在 Pending 状态,就是还在等中介回话。
就像中介最终只会交出一把钥匙,调度器在整个集群里也只会确定唯一节点。理解了“先筛后评”的骨架,后续所有扩展,都不过是在为这两个阶段增加新的“筛选条目”和“评分维度”。
- 候选集合 C(p)$\mathcal{C}(p)$ 表示对 Pod $p$ 而言,全部节点 $\mathcal{N}$ 中通过所有硬性条件的节点子集。
- 过滤判定 f$\mathcal{F}$ 是全部过滤插件的集合;$f(n,p)=\text{True}$ 表示房源 $n$ 满足租客 $p$ 的硬性约束。
- 打分插件 score_i$\text{score}_i(n)$ 是第 $i$ 个打分插件对节点 $n$ 给出的评分。
- 权重 w_i$w_i$ 是该打分插件的权重,数值越大对最终总分影响越大。
- argmax 求解$n^{*}$ 是所有候选节点中加权总分的最大者,即最终 Pod 要绑定的目标节点。
过滤阶段
中介把“朝南、预算五千、允许养猫”写在黑板最上方,任何一条不满足的房源直接划掉。这不是偏好,而是铁律——租客入住后才发现没天然气,纠纷成本极高。过滤阶段就是调度器的“黑板铁律”。
在 Scheduler Framework 中,过滤对应 Filter 扩展点。内置的过滤器包括资源满足(ResourceFit)、端口不冲突、存储卷可用、节点选择器等。每一个过滤器对某个节点只回答“行”或“不行”,所有过滤器都回答“行”,节点才有资格进入下一轮。
其中资源过滤是最核心的账本:调度器为每个节点维护“可分配量-已请求量”的余量。注意,它看的不是 Pod 的真实用量,而是 Pod 在申请书上声明的请求量——就像中介看的是租客承诺的月薪,而不是他实际怎么花钱。这是调度与监控的本质区别:调度基于“承诺”,监控基于“现实”。
过滤的本质是把“绝对不能住的房子”提前踢出名单。硬性约束越多,候选池越小,后续打分的计算开销也越低——这正是中介先铁律后偏好的经济学。
- 资源类型 r$\mathcal{R}$ 是 CPU、内存、GPU、临时存储等资源类型的集合。
- 可分配量 R_alloc$R_{\text{alloc}}(n, r)$ 是节点 $n$ 上类型 $r$ 的总可分配量。
- 已使用量 R_used$R_{\text{used}}(n, r)$ 是当前已被其他 Pod 请求占用的量。
- 申请量 R_req$R_{\text{req}}(p, r)$ 是 Pod $p$ 在申请书中声明的 $r$ 类型资源量。
- 剩余量判定只有当“剩余量 ≥ 申请量”时过滤才放行,否则节点出局。
打分阶段
过滤完还剩 5 套房源,中介开始多维度打分:离地铁 200 米 +20,精装修 +15,临街嘈杂 −10。总分最高者拿钥匙。打分阶段对应 Scheduler Framework 的 Score 与 NormalizeScore 扩展点。
内置打分策略五花八门:NodeResourcesFit 看资源拟合度,ImageLocality 看镜像是否已缓存在节点上,NodeAffinity 看节点标签偏好。每个打分插件给出一个原始分,调度器把各插件分数归一化后乘以权重,再累加成总分。
为了让量纲不同的插件分数可以相加,必须先归一化到同一刻度。假如“镜像本地化”满分 10 分,“资源均衡”满分 100 分,直接相加会让后者完全主导。归一化之后,权重才真正表达“公司战略”:比如平台团队想让集群更均衡,就把均衡类插件权重调高。
打分环节定义了“好房子”的标准。生产环境里,平台团队通过调节内置插件权重、甚至注册自定义打分插件,把“便宜”“近 GPU”“负载均衡”等公司战略翻译成调度器听得懂的语言。
- 原始分 score_i第 $i$ 个打分插件给出的原始评分,量纲各不相同。
- 归一化分子$\text{score}_i(n) - \text{min}_i$ 表示该节点在指标 $i$ 上超过最低分的幅度。
- 归一化分母$\text{max}_i - \text{min}_i$ 是最大值与最小值的跨度,用于消除量纲差异。
- 权重与累加每个归一化分数乘以 $w_i$ 后求和,得到最终总分。
- 最优节点总分最高的候选节点胜出,成为绑定目标。
框架扩展点
中介公司把接待流程拆成标准化环节:取号排队、初筛、带看、谈租金、签合同、交钥匙。房东如果想加一条“必须本人持身份证签约”,只需要在签约环节加规则,而不必重写整个公司。Scheduler Framework 就是这套标准化环节。
它定义了一系列扩展点(Extension Points):QueueSort 决定 Pod 排队顺序;PreFilter/Filter/PostFilter 负责过滤阶段;PreScore/Score/NormalizeScore 负责打分阶段;Reserve/Permit/PreBind/Bind/PostBind 负责绑定阶段。每个扩展点上都可以挂载多个插件。
插件在注册时声明自己实现哪个扩展点,然后在 KubeSchedulerConfiguration 里启用。多个过滤插件挂在同一扩展点时,调度器按顺序调用,且遵循“逻辑与”:任何一个返回 False,这个节点就出局。正是这种“插槽式”设计,让自定义调度变成配置而非重写。
Framework 的意义在于“可插拔”:社区维护了几十个插件,你可以开启、关闭、调权重,也可以自己实现插件注册进去。就像中介公司不断新增“评分细则模板”,而无需更换门店招牌。
- 全称与符号$\bigwedge$ 表示对集合中每个元素逐一做逻辑与运算。
- 过滤插件集合$\mathcal{F}_{\text{filter}}$ 是全部注册在 Filter 扩展点上的插件集合。
- 单个判定$f(p, n)$ 是插件 $f$ 对 Pod $p$ 与节点 $n$ 的匹配结果。
- 整体通过条件只有所有插件同时返回 True,$\text{pass}(p, n)$ 才为 True。
绑定与预占
租客看中房子后,中介要“锁房”避免被抢,再请房东“终审”,通过后“签合同”,最后“交钥匙”。对应到调度器,就是 Reserve、Permit、Bind 三个扩展点接力完成从“决定”到“落定”的闭环。
Reserve 在调度周期内临时预占节点资源,防止两个并发调度决策同时看中同一块资源。Permit 允许外部控制器做绑定前审批:可以同意、拒绝或暂时挂起。只有 Permit 通过,调度器才会执行 PreBind 和 Bind,把 Pod 的 nodeName 写进 API Server。
Bind 完成后,真正行动的却是另一套人马——kubelet。它 watch 到 Pod 被绑定到自己节点,才拉镜像、启动容器。调度器是“决策者”,kubelet 是“执行者”。如果 Permit 拒绝了 Pod,调度器还会回调 Unreserve 释放预占,保证账目清爽。
“决策与执行分离”是分布式系统最优雅的设计之一。中介只负责带看和签约,搬家和入住由物业完成——各司其职,系统才不会因为某个环节卡顿而整体瘫痪。
- Pending 等待调度Pod 创建后尚未分配节点,停留在 Pending 状态。
- Reserve 预占资源调度器在选定节点上临时记账,状态变为 Reserved。
- Permit 审批放行Permit 插件允许后进入 Approved,拒绝则触发 Unreserve 回滚。
- Bind 写绑定调度器把 Pod 的 nodeName 设为 $n$,状态变为 Bound。
- kubelet 执行kubelet 观察到 Bound 后启动容器,Pod 进入 Running。
拓扑感知调度
一家公司的员工如果分散在全国各地,开会就要忍受高延迟。中介会建议同公司的人住同一栋楼,但也避免把所有人都塞进同一层——万一着火整层断电,全公司就瘫痪了。这就是“拓扑感知”的朴素直觉。
拓扑域(Topology Domain)就是 K8s 里的“楼栋与楼层”:常用维度有 region(城市)、zone(机房)、hostname(机器)。拓扑感知调度通过节点亲和(nodeAffinity)、Pod 亲和(podAffinity)与反亲和(podAntiAffinity)、以及拓扑分布约束(topologySpreadConstraints)来调节 Pod 的落位。
亲和让相关服务“抱团”:网关与后端服务放同一个可用区,网络延迟骤降。反亲和让冗余副本“分篮”:三副本分布到三个可用区,单区故障不影响整体可用性。两种力量在同一张拓扑地图上角逐,而 maxSkew 参数就是控制“分散程度”的阀门。
亲和是“抱团取暖”,反亲和是“鸡蛋分篮”。生产环境里二者需按业务权衡:低延迟多抱团,高可用多分散——调度配置就是在这种张力中寻找平衡点。
- 拓扑域集合$\mathcal{T}$ 是所有可用区、节点等拓扑域的集合。
- 已存数量$\text{Pods}(t)$ 是拓扑域 $t$ 中已经运行的同类 Pod 数量。
- 最大差值遍历所有拓扑域对,找出 Pod 数量差的最大值。
- maxSkew 容忍偏差只要最大差值不超过 maxSkew,节点就满足分布约束;超限则过滤或降分。
GPU亲和调度
有些租客是特殊职业——AI 研究员需要带实验室的房源:必须有大功率供电、通风系统、甚至指定品牌设备。普通房源再多他们也不住。中介必须能识别这类“特殊需求”。K8s 里的 GPU 节点就是这样一批“实验室房源”。
K8s 通过设备插件(Device Plugin)机制把 GPU 以扩展资源(如 nvidia.com/gpu)的形式上报给节点,调度器在过滤阶段检查 Pod 申请的 GPU 数量是否满足。但“有 GPU”不等于“适合跑这个模型”:显存规格、GPU 型号、卡间互联都会影响训练与推理性能。
生产实践通常把 GPU 调度做成自定义 Score 插件:优先选择空闲 GPU 多的节点、同型号卡的节点、或靠近数据集的节点。同时配合拓扑感知,把分布式训练任务的多个 worker 调度到同一台 8 卡机器,避免跨机通信损耗。
GPU 是很贵的“实验室房源”。好的调度让每一块卡都物尽其用:训练任务聚集、推理任务分散、型号严格匹配——这些策略就是平台团队与算法团队共同打磨出的“中介手册”。
- 空闲比例$g_{\text{free}}(n) / g_{\text{total}}(n)$ 是节点 $n$ 上 GPU 空闲比例,值越大越不容易排队。
- 型号匹配指示函数$\mathbb{1}[\cdot]$ 在方括号内条件成立时取 1,否则取 0。
- 权重 alpha$\alpha$ 控制空闲率的重要性,体现“资源充足优先”。
- 权重 beta$\beta$ 控制型号匹配的加分强度,体现“硬件一致优先”。
- 总分决策两项加权求和,得分最高的节点被选中。
AI 应用场景
大模型推理平台的 GPU 配额调度
阶段:分布式调度与资源管理
解决的问题:多团队共享 GPU 集群时,人工排队分配频繁冲突,资源利用率低,模型常因“卡不对”而失败。
具体案例:某公司搭建 LLM 推理平台,注册自定义 Filter 插件校验 GPU 显存与型号,注册 Score 插件优先选同型号且空闲率高的节点,再用 Pod 反亲和让同一服务的多个副本分散到不同节点。结果:单卡故障不再拖垮整个服务,集群利用率提升约 40%。
电商异地多活的拓扑感知调度
阶段:分布式调度与资源管理
解决的问题:大促流量需要就近接入,但订单服务的三个副本又必须跨可用区部署,避免单机房故障。
具体案例:用 topologySpreadConstraints 把订单服务三副本均匀分布到三个 zone,用节点亲和把依赖的 Redis 缓存调度到同 zone 节点,将跨可用区网络延迟从 8ms 降到 1ms,同时保障容灾能力。
分时混部的优先级调度
阶段:分布式调度与资源管理
解决的问题:白天在线推理峰值、夜间训练任务排队,集群资源利用率波动大。
具体案例:通过自定义 QueueSort 插件实现优先级队列(在线任务 > 离线任务),离线任务白天占用空闲资源,高峰期被 Permit 插件延迟放行。平台整体装箱率从 35% 提升到 70%。
Python 演示
class Node: def __init__(self, name, cpu, gpu, zone, gpu_type): self.name = name self.cpu = cpu self.gpu = gpu self.zone = zone self.gpu_type = gpu_type class Pod: def __init__(self, name, cpu, gpu, gpu_type): self.name = name self.cpu = cpu self.gpu = gpu self.gpu_type = gpu_type def filter_resources(node, pod): return node.cpu >= pod.cpu and node.gpu >= pod.gpu def filter_gpu_type(node, pod): return pod.gpu == 0 or node.gpu_type == pod.gpu_type def score_gpu_free(node, pod): return min(node.gpu, 8) * 2 def score_gpu_type(node, pod): return 30 if node.gpu_type == pod.gpu_type else 0 class Scheduler: def __init__(self): self.filters = [filter_resources, filter_gpu_type] self.scores = [score_gpu_free, score_gpu_type] def schedule(self, pod, nodes): candidates = [] for node in nodes: if all(f(node, pod) for f in self.filters): candidates.append(node) if not candidates: return None return max(candidates, key=lambda n: sum(s(n, pod) for s in self.scores)) if __name__ == "__main__": nodes = [ Node("node-a", cpu=16, gpu=8, zone="zone-1", gpu_type="A100"), Node("node-b", cpu=16, gpu=0, zone="zone-2", gpu_type="none"), Node("node-c", cpu=8, gpu=4, zone="zone-1", gpu_type="V100"), ] pod = Pod("llm-infer", cpu=4, gpu=1, gpu_type="A100") best = Scheduler().schedule(pod, nodes) print(best.name if best else "no suitable node")
self.filters = [filter_resources, filter_gpu_type]把过滤插件注册进调度器,对应 Scheduler Framework 的插件注册机制。all(f(node, pod) for f in self.filters)逻辑“与”:所有过滤器都通过,节点才进入候选池。score_gpu_free空闲 GPU 越多得分越高,体现资源充足优先策略。score_gpu_type型号匹配加 30 分,体现硬件一致优先策略。max(candidates, key=lambda n: sum(...))取总分最高的节点,对应打分阶段的 argmax。print(best.name if best else "no suitable node")无候选时调度失败,Pod 停留在 Pending 状态。
常见误区
误区我以为调度器只需要检查 CPU 和内存够不够,端口、磁盘、标签都是 kubelet 的事。
正解K8s 原生的资源过滤远不止 CPU 与内存:端口冲突、存储卷拓扑、节点选择器、亲和反亲和都会在过滤阶段检查。GPU 这类扩展资源更是通过设备插件上报后才参与调度。过滤是一整套“硬性条款”,不是单一账本。
为何常见:初学教程多以 CPU/内存举例,容易把其中一条规则当成全部规则。
误区我觉得选出来的节点就是资源最充足的节点,分数代表资源富余程度。
正解打分为多个插件加权聚合的结果:负载均衡、镜像本地化、亲和性等都参与。一个节点可能资源不是最足,但因为镜像已存在、与依赖服务同 zone,总分反而更高。高分是“综合最合适”,不是“单一最空闲”。
为何常见:把“分数”与单一指标(空闲率)绑定,忽略了加权聚合机制。
误区公司要特殊调度策略,我得 fork kube-scheduler 或从零实现一个调度器。
正解Scheduler Framework 允许在保留内置调度逻辑的前提下注册自定义插件,通过 KubeSchedulerConfiguration 启用,无需从零实现。大部分定制只需要 Filter 和 Score 两个扩展点就能完成。
为何常见:对 Framework 的了解停留在概念层,不知道配置化插件的开发模式。
误区Bind 把 Pod 放到节点上,调度器负责启动它。
正解Bind 只是把 spec.nodeName 写入 API Server。真正执行业务容器的是 kubelet,它 watch 到绑定变更后才拉镜像、调容器运行时。调度决策与节点执行是彻底分离的两套系统。
为何常见:混淆了控制面“决策者”与数据面“执行者”的职责边界。
误区我给所有服务都加上反亲和,让副本尽量分散,这样最安全。
正解过度分散会导致 Pod 碎片化、集群装箱率下降、跨机通信增加,成本和延迟反而上升。反亲和与亲和/装箱需要按业务权衡,并用 maxSkew 控制分散程度——不是越散越好。
为何常见:高可用(分散)与高性能(聚集)天然矛盾,初学者容易只看单边价值。
面试题
Q1. Kubernetes 调度器为 Pod 选择节点的两个核心阶段是什么?
- A.拉取与推送
- B.过滤与打分← 正确答案
- C.探测与注册
- D.编译与执行
答案:B。调度器先过滤出满足硬性条件的候选节点,再打分选出最优。拉取/推送是镜像与日志的传输动作,探测与注册是节点入池时的动作,编译与执行与调度决策无关。
Q2. 过滤阶段中资源的比较发生在哪两个量之间?
- A.可分配量与已请求量← 正确答案
- B.CPU 型号与主频
- C.磁盘品牌与容量
- D.内存条数与频率
答案:A。调度器维护的是“可分配量 − 已请求量”的剩余账本,新 Pod 的申请量必须小于等于剩余量。型号、品牌、条数都不是调度器做资源判定的依据。
Q3. Pod 长时间处于 Pending 状态,最可能说明什么?
- A.容器启动过程失败
- B.节点正在重启宕机
- C.尚未选出可绑定的节点← 正确答案
- D.镜像拉取超时重试中
答案:C。Pending 表示调度器尚未完成“选房”,即过滤后候选池为空或打分后未能绑定。容器启动失败对应 CrashLoopBackOff,镜像拉取问题对应 ImagePullBackOff,都不属于 Pending 的典型原因。
Q4. 关于打分阶段,下列说法正确的是?
- A.任一打分不满足则节点出局
- B.多个打分插件加权求和← 正确答案
- C.总分越高代表节点越空闲
- D.打分阶段只看 CPU 资源
答案:B。打分是多个插件加权求和的结果,权重表达平台策略;打分不满足是“分低”而不是“出局”,出局只发生在过滤阶段。总分越高是“综合最合适”,不代表一定最空闲。
Q5. 以下哪些属于 Scheduler Framework 的扩展点?
- A.QueueSort 排队扩展点← 正确答案
- B.Filter 过滤扩展点← 正确答案
- C.Score 打分扩展点← 正确答案
- D.Sidecar 边车容器
答案:ABC。QueueSort 决定 Pod 排队顺序,Filter 负责硬性过滤,Score 负责软性打分,三者都是 Scheduler Framework 的扩展点。Sidecar 是服务网格中的边车容器模式,与调度扩展点无关。
Q6. Reserve 扩展点的核心作用是什么?
- A.把 Pod 绑定关系写入服务端
- B.预占节点资源防止并发覆盖← 正确答案
- C.启动容器运行时进程调度
- D.拉取容器镜像到目标节点
答案:B。Reserve 在调度周期内临时预占资源,避免多个调度决策并发覆盖同一块资源;若最终未绑定,Unreserve 会回滚预占。写绑定是 Bind 的职责,启动容器是 kubelet 的职责。
Q7. 关于 GPU 亲和调度,以下做法正确的有?
- A.检查节点 GPU 型号是否匹配← 正确答案
- B.优先选择 GPU 空闲率高的节点← 正确答案
- C.把训练任务调度到多卡节点← 正确答案
- D.忽略 Pod 申请的显存大小
答案:ABC。GPU 调度要检查型号匹配、优先空闲率高的节点,并把分布式训练任务聚集到多卡节点降低通信损耗。忽略显存大小会导致 OOM,是错误做法。
Q8. 拓扑感知调度可以通过哪些方式实现?
- A.节点亲和性选择← 正确答案
- B.日志自动采集
- C.拓扑分布约束← 正确答案
- D.Pod 间反亲和← 正确答案
答案:ACD。节点亲和让 Pod 落到指定 zone,拓扑分布约束用 maxSkew 控制副本散布,Pod 反亲和避免副本挤在同一节点。日志采集与调度决策无关。
Q9. 调度器完成 Bind 之后,真正启动业务容器的是?
- A.节点上的 kubelet← 正确答案
- B.集群里的 etcd
- C.网关层 apiserver
- D.控制面控制器
答案:A。Bind 只把 nodeName 写入 API Server,kubelet watch 到绑定变更后拉镜像并启动容器。etcd 是存储层,apiserver 是 API 网关,controller-manager 负责其他控制器逻辑。
Q10. 自定义 Permit 插件拒绝了某 Pod 后,调度器会调用哪个回调释放已预占资源?
- A.Unreserve 释放预占资源← 正确答案
- B.PostBind 完成善后处理
- C.PreFilter 重跑过滤逻辑
- D.QueueSort 重新排序等待
答案:A。Permit 拒绝后,调度器必须回滚 Reserve 做的资源记账,对应回调是 Unreserve。PostBind 只在绑定成功后触发,PreFilter 与 QueueSort 都在调度周期更早阶段生效,均不负责资源释放。