想象你是一家大型房屋租赁公司的金牌中介。每天都有形形色色的租客上门:有程序员要求必须养猫、有家庭需要三居室朝南、有创业公司要一整层楼并自带机房。作为中介,你的工作分成两个固定步骤:第一步“硬性筛选”——养猫的必须有开放宠物政策的房源,要机房的必须电容量足够,不满足的一律不看;第二步“软性评分”——在剩余房源里,离地铁近的加分、采光好的加分、楼龄新的加分,最后把最高分的那把钥匙交给租客。 Kubernetes 里的调度器就是这样一位“金牌中介”。租客叫 Pod(运行应用的最小单元),房源叫 Node(集群里的机器),整套匹配流程叫“调度”。但真正的挑战在于:每家公司对“好房子”的定义都不一样——做 AI 大模型的想要 GPU 机房,做在线交易的想让服务靠近用户,做灾备的想让副本分散在不同城市。为此,K8s 提供了 Scheduler Framework,把调度流程拆成一个个可插拔的“环节”,允许你像定制中介服务手册一样,加入自己的筛选和打分规则。这篇文章,我们就从“中介手册”的视角,把 K8s 调度器扩展机制讲透。

岗位关联

掌握 K8s 调度器扩展,是云原生架构师设计高并发资源池、降低推理成本、保障服务高可用的关键能力——它能让你把“好钢”精确地用在“刀刃”上。

目录
  1. 可视化演示
  2. 调度基础流程
  3. 过滤阶段
  4. 打分阶段
  5. 框架扩展点
  6. 绑定与预占
  7. 拓扑感知调度
  8. GPU亲和调度
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示调度器过滤与打分两阶段为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)={n∈N∣∀f∈F:f(n,p)=True}\displaystyle \mathcal{C}(p) = \{ n \in \mathcal{N} \mid \forall f \in \mathcal{F}: f(n, p) = \text{True} \}
n∗=arg⁡max⁡n∈C(p)∑i=1kwi⋅scorei(n)\displaystyle n^{*} = \arg\max_{n \in \mathcal{C}(p)} \sum_{i=1}^{k} w_i \cdot \text{score}_i(n)
第一条公式算“哪些房子有资格”,第二条公式算“哪套房子最合适”。
  1. 候选集合 C(p)$\mathcal{C}(p)$ 表示对 Pod $p$ 而言,全部节点 $\mathcal{N}$ 中通过所有硬性条件的节点子集。
    C(p)⊆N\displaystyle \mathcal{C}(p) \subseteq \mathcal{N}
  2. 过滤判定 f$\mathcal{F}$ 是全部过滤插件的集合;$f(n,p)=\text{True}$ 表示房源 $n$ 满足租客 $p$ 的硬性约束。
    f(n,p)∈{True,False}\displaystyle f(n, p) \in \{ \text{True}, \text{False} \}
  3. 打分插件 score_i$\text{score}_i(n)$ 是第 $i$ 个打分插件对节点 $n$ 给出的评分。
    scorei:N→R\displaystyle \text{score}_i: \mathcal{N} \to \mathbb{R}
  4. 权重 w_i$w_i$ 是该打分插件的权重,数值越大对最终总分影响越大。
    wi≥0,∑i=1kwi=1\displaystyle w_i \ge 0, \quad \sum_{i=1}^{k} w_i = 1
  5. argmax 求解$n^{*}$ 是所有候选节点中加权总分的最大者,即最终 Pod 要绑定的目标节点。
    n∗=arg⁡max⁡n∈C(p)  TotalScore(n)\displaystyle n^{*} = \arg\max_{n \in \mathcal{C}(p)} \; \text{TotalScore}(n)

过滤阶段

中介把“朝南、预算五千、允许养猫”写在黑板最上方,任何一条不满足的房源直接划掉。这不是偏好,而是铁律——租客入住后才发现没天然气,纠纷成本极高。过滤阶段就是调度器的“黑板铁律”。

在 Scheduler Framework 中,过滤对应 Filter 扩展点。内置的过滤器包括资源满足(ResourceFit)、端口不冲突、存储卷可用、节点选择器等。每一个过滤器对某个节点只回答“行”或“不行”,所有过滤器都回答“行”,节点才有资格进入下一轮。

其中资源过滤是最核心的账本:调度器为每个节点维护“可分配量-已请求量”的余量。注意,它看的不是 Pod 的真实用量,而是 Pod 在申请书上声明的请求量——就像中介看的是租客承诺的月薪,而不是他实际怎么花钱。这是调度与监控的本质区别:调度基于“承诺”,监控基于“现实”。

过滤的本质是把“绝对不能住的房子”提前踢出名单。硬性约束越多,候选池越小,后续打分的计算开销也越低——这正是中介先铁律后偏好的经济学。

∀r∈R:Ralloc(n,r)−Rused(n,r)≥Rreq(p,r)\displaystyle \forall r \in \mathcal{R}: \quad R_{\text{alloc}}(n, r) - R_{\text{used}}(n, r) \ge R_{\text{req}}(p, r)
节点上每种资源的剩余量,都必须大于等于新租客的申请量。
  1. 资源类型 r$\mathcal{R}$ 是 CPU、内存、GPU、临时存储等资源类型的集合。
    r∈{cpu,memory,gpu,…}\displaystyle r \in \{ \text{cpu}, \text{memory}, \text{gpu}, \ldots \}
  2. 可分配量 R_alloc$R_{\text{alloc}}(n, r)$ 是节点 $n$ 上类型 $r$ 的总可分配量。
    Ralloc(n,r)=Capacity(n,r)−Reserved(n,r)\displaystyle R_{\text{alloc}}(n, r) = \text{Capacity}(n, r) - \text{Reserved}(n, r)
  3. 已使用量 R_used$R_{\text{used}}(n, r)$ 是当前已被其他 Pod 请求占用的量。
    Rused(n,r)=∑p′∈Pods(n)Rreq(p′,r)\displaystyle R_{\text{used}}(n, r) = \sum_{p' \in \text{Pods}(n)} R_{\text{req}}(p', r)
  4. 申请量 R_req$R_{\text{req}}(p, r)$ 是 Pod $p$ 在申请书中声明的 $r$ 类型资源量。
    Rreq(p,r)≥0\displaystyle R_{\text{req}}(p, r) \ge 0
  5. 剩余量判定只有当“剩余量 ≥ 申请量”时过滤才放行,否则节点出局。
    Ravail(n,r)=Ralloc(n,r)−Rused(n,r)\displaystyle R_{\text{avail}}(n, r) = R_{\text{alloc}}(n, r) - R_{\text{used}}(n, r)

打分阶段

过滤完还剩 5 套房源,中介开始多维度打分:离地铁 200 米 +20,精装修 +15,临街嘈杂 −10。总分最高者拿钥匙。打分阶段对应 Scheduler Framework 的 Score 与 NormalizeScore 扩展点。

内置打分策略五花八门:NodeResourcesFit 看资源拟合度,ImageLocality 看镜像是否已缓存在节点上,NodeAffinity 看节点标签偏好。每个打分插件给出一个原始分,调度器把各插件分数归一化后乘以权重,再累加成总分。

为了让量纲不同的插件分数可以相加,必须先归一化到同一刻度。假如“镜像本地化”满分 10 分,“资源均衡”满分 100 分,直接相加会让后者完全主导。归一化之后,权重才真正表达“公司战略”:比如平台团队想让集群更均衡,就把均衡类插件权重调高。

打分环节定义了“好房子”的标准。生产环境里,平台团队通过调节内置插件权重、甚至注册自定义打分插件,把“便宜”“近 GPU”“负载均衡”等公司战略翻译成调度器听得懂的语言。

Score(n)=∑i=1kwi⋅scorei(n)−minimaxi−mini×100\displaystyle \text{Score}(n) = \sum_{i=1}^{k} w_i \cdot \frac{\text{score}_i(n) - \text{min}_i}{\text{max}_i - \text{min}_i} \times 100
每个插件先把自己的分数压缩到 0–100 的同一把尺子上,再按权重加权求和。
  1. 原始分 score_i第 $i$ 个打分插件给出的原始评分,量纲各不相同。
    scorei(n)∈[mini,maxi]\displaystyle \text{score}_i(n) \in [\text{min}_i, \text{max}_i]
  2. 归一化分子$\text{score}_i(n) - \text{min}_i$ 表示该节点在指标 $i$ 上超过最低分的幅度。
    Δi(n)=scorei(n)−mini\displaystyle \Delta_i(n) = \text{score}_i(n) - \text{min}_i
  3. 归一化分母$\text{max}_i - \text{min}_i$ 是最大值与最小值的跨度,用于消除量纲差异。
    normi(n)=Δi(n)maxi−mini∈[0,1]\displaystyle \text{norm}_i(n) = \frac{\Delta_i(n)}{\text{max}_i - \text{min}_i} \in [0, 1]
  4. 权重与累加每个归一化分数乘以 $w_i$ 后求和,得到最终总分。
    Total(n)=∑i=1kwi⋅normi(n)×100\displaystyle \text{Total}(n) = \sum_{i=1}^{k} w_i \cdot \text{norm}_i(n) \times 100
  5. 最优节点总分最高的候选节点胜出,成为绑定目标。
    n∗=arg⁡max⁡n∈CTotal(n)\displaystyle n^{*} = \arg\max_{n \in \mathcal{C}} \text{Total}(n)

框架扩展点

中介公司把接待流程拆成标准化环节:取号排队、初筛、带看、谈租金、签合同、交钥匙。房东如果想加一条“必须本人持身份证签约”,只需要在签约环节加规则,而不必重写整个公司。Scheduler Framework 就是这套标准化环节。

它定义了一系列扩展点(Extension Points):QueueSort 决定 Pod 排队顺序;PreFilter/Filter/PostFilter 负责过滤阶段;PreScore/Score/NormalizeScore 负责打分阶段;Reserve/Permit/PreBind/Bind/PostBind 负责绑定阶段。每个扩展点上都可以挂载多个插件。

插件在注册时声明自己实现哪个扩展点,然后在 KubeSchedulerConfiguration 里启用。多个过滤插件挂在同一扩展点时,调度器按顺序调用,且遵循“逻辑与”:任何一个返回 False,这个节点就出局。正是这种“插槽式”设计,让自定义调度变成配置而非重写。

Framework 的意义在于“可插拔”:社区维护了几十个插件,你可以开启、关闭、调权重,也可以自己实现插件注册进去。就像中介公司不断新增“评分细则模板”,而无需更换门店招牌。

pass(p,n)=⋀f∈Ffilterf(p,n)\displaystyle \text{pass}(p, n) = \bigwedge_{f \in \mathcal{F}_{\text{filter}}} f(p, n)
过滤插件的判定结果做逻辑与:全部为真,节点才通过。
  1. 全称与符号$\bigwedge$ 表示对集合中每个元素逐一做逻辑与运算。
    ⋀f∈Ff=f1∧f2∧⋯∧fk\displaystyle \bigwedge_{f \in \mathcal{F}} f = f_1 \land f_2 \land \cdots \land f_k
  2. 过滤插件集合$\mathcal{F}_{\text{filter}}$ 是全部注册在 Filter 扩展点上的插件集合。
    Ffilter={f1,f2,…,fk}\displaystyle \mathcal{F}_{\text{filter}} = \{ f_1, f_2, \ldots, f_k \}
  3. 单个判定$f(p, n)$ 是插件 $f$ 对 Pod $p$ 与节点 $n$ 的匹配结果。
    f(p,n)∈{True,False}\displaystyle f(p, n) \in \{ \text{True}, \text{False} \}
  4. 整体通过条件只有所有插件同时返回 True,$\text{pass}(p, n)$ 才为 True。
    pass(p,n)=True  ⟺  ∀f:f(p,n)=True\displaystyle \text{pass}(p, n) = \text{True} \iff \forall f: f(p, n) = \text{True}

绑定与预占

租客看中房子后,中介要“锁房”避免被抢,再请房东“终审”,通过后“签合同”,最后“交钥匙”。对应到调度器,就是 Reserve、Permit、Bind 三个扩展点接力完成从“决定”到“落定”的闭环。

Reserve 在调度周期内临时预占节点资源,防止两个并发调度决策同时看中同一块资源。Permit 允许外部控制器做绑定前审批:可以同意、拒绝或暂时挂起。只有 Permit 通过,调度器才会执行 PreBind 和 Bind,把 Pod 的 nodeName 写进 API Server。

Bind 完成后,真正行动的却是另一套人马——kubelet。它 watch 到 Pod 被绑定到自己节点,才拉镜像、启动容器。调度器是“决策者”,kubelet 是“执行者”。如果 Permit 拒绝了 Pod,调度器还会回调 Unreserve 释放预占,保证账目清爽。

“决策与执行分离”是分布式系统最优雅的设计之一。中介只负责带看和签约,搬家和入住由物业完成——各司其职,系统才不会因为某个环节卡顿而整体瘫痪。

Phase(p):Pending→ReserveReserved→PermitApproved→BindBound→kubeletRunning\displaystyle \text{Phase}(p): \quad \text{Pending} \xrightarrow{\text{Reserve}} \text{Reserved} \xrightarrow{\text{Permit}} \text{Approved} \xrightarrow{\text{Bind}} \text{Bound} \xrightarrow{\text{kubelet}} \text{Running}
Pod 从创建到运行,依次经过保留、审批、绑定、启动四个状态跃迁。
  1. Pending 等待调度Pod 创建后尚未分配节点,停留在 Pending 状态。
    Phase(p)=Pending\displaystyle \text{Phase}(p) = \text{Pending}
  2. Reserve 预占资源调度器在选定节点上临时记账,状态变为 Reserved。
    Phase(p)→ReserveReserved\displaystyle \text{Phase}(p) \xrightarrow{\text{Reserve}} \text{Reserved}
  3. Permit 审批放行Permit 插件允许后进入 Approved,拒绝则触发 Unreserve 回滚。
    Permit(p,n)=Allow⇒Phase(p)=Approved\displaystyle \text{Permit}(p, n) = \text{Allow} \Rightarrow \text{Phase}(p) = \text{Approved}
  4. Bind 写绑定调度器把 Pod 的 nodeName 设为 $n$,状态变为 Bound。
    spec.nodeName(p):=n\displaystyle \text{spec.nodeName}(p) := n
  5. kubelet 执行kubelet 观察到 Bound 后启动容器,Pod 进入 Running。
    Phase(p)=Running⇐kubelet(n)\displaystyle \text{Phase}(p) = \text{Running} \Leftarrow \text{kubelet}(n)

拓扑感知调度

一家公司的员工如果分散在全国各地,开会就要忍受高延迟。中介会建议同公司的人住同一栋楼,但也避免把所有人都塞进同一层——万一着火整层断电,全公司就瘫痪了。这就是“拓扑感知”的朴素直觉。

拓扑域(Topology Domain)就是 K8s 里的“楼栋与楼层”:常用维度有 region(城市)、zone(机房)、hostname(机器)。拓扑感知调度通过节点亲和(nodeAffinity)、Pod 亲和(podAffinity)与反亲和(podAntiAffinity)、以及拓扑分布约束(topologySpreadConstraints)来调节 Pod 的落位。

亲和让相关服务“抱团”:网关与后端服务放同一个可用区,网络延迟骤降。反亲和让冗余副本“分篮”:三副本分布到三个可用区,单区故障不影响整体可用性。两种力量在同一张拓扑地图上角逐,而 maxSkew 参数就是控制“分散程度”的阀门。

亲和是“抱团取暖”,反亲和是“鸡蛋分篮”。生产环境里二者需按业务权衡:低延迟多抱团,高可用多分散——调度配置就是在这种张力中寻找平衡点。

max⁡t,t′∈T∣Pods(t)−Pods(t′)∣≤maxSkew\displaystyle \max_{t, t' \in \mathcal{T}} \left| \text{Pods}(t) - \text{Pods}(t') \right| \le \text{maxSkew}
任意两个拓扑域里的同类 Pod 数量差,都不能超过 maxSkew。
  1. 拓扑域集合$\mathcal{T}$ 是所有可用区、节点等拓扑域的集合。
    T={zone-1,zone-2,zone-3,…}\displaystyle \mathcal{T} = \{ \text{zone-1}, \text{zone-2}, \text{zone-3}, \ldots \}
  2. 已存数量$\text{Pods}(t)$ 是拓扑域 $t$ 中已经运行的同类 Pod 数量。
    Pods(t)=∑p∈Pods1[zone(p)=t]\displaystyle \text{Pods}(t) = \sum_{p \in \text{Pods}} \mathbb{1}[\text{zone}(p) = t]
  3. 最大差值遍历所有拓扑域对,找出 Pod 数量差的最大值。
    skew=max⁡t,t′∣Pods(t)−Pods(t′)∣\displaystyle \text{skew} = \max_{t, t'} \left| \text{Pods}(t) - \text{Pods}(t') \right|
  4. maxSkew 容忍偏差只要最大差值不超过 maxSkew,节点就满足分布约束;超限则过滤或降分。
    skew≤maxSkew\displaystyle \text{skew} \le \text{maxSkew}

GPU亲和调度

有些租客是特殊职业——AI 研究员需要带实验室的房源:必须有大功率供电、通风系统、甚至指定品牌设备。普通房源再多他们也不住。中介必须能识别这类“特殊需求”。K8s 里的 GPU 节点就是这样一批“实验室房源”。

K8s 通过设备插件(Device Plugin)机制把 GPU 以扩展资源(如 nvidia.com/gpu)的形式上报给节点,调度器在过滤阶段检查 Pod 申请的 GPU 数量是否满足。但“有 GPU”不等于“适合跑这个模型”:显存规格、GPU 型号、卡间互联都会影响训练与推理性能。

生产实践通常把 GPU 调度做成自定义 Score 插件:优先选择空闲 GPU 多的节点、同型号卡的节点、或靠近数据集的节点。同时配合拓扑感知,把分布式训练任务的多个 worker 调度到同一台 8 卡机器,避免跨机通信损耗。

GPU 是很贵的“实验室房源”。好的调度让每一块卡都物尽其用:训练任务聚集、推理任务分散、型号严格匹配——这些策略就是平台团队与算法团队共同打磨出的“中介手册”。

GPU-Score(n)=α⋅gfree(n)gtotal(n)+β⋅1 ⁣[gpuType(n)=reqType(p)]\displaystyle \text{GPU-Score}(n) = \alpha \cdot \frac{g_{\text{free}}(n)}{g_{\text{total}}(n)} + \beta \cdot \mathbb{1}\!\left[ \text{gpuType}(n) = \text{reqType}(p) \right]
GPU 打分 = 空闲率贡献 + 型号匹配加分,权重由平台策略决定。
  1. 空闲比例$g_{\text{free}}(n) / g_{\text{total}}(n)$ 是节点 $n$ 上 GPU 空闲比例,值越大越不容易排队。
    FreeRatio(n)=gfree(n)gtotal(n)\displaystyle \text{FreeRatio}(n) = \frac{g_{\text{free}}(n)}{g_{\text{total}}(n)}
  2. 型号匹配指示函数$\mathbb{1}[\cdot]$ 在方括号内条件成立时取 1,否则取 0。
    1[gpuType(n)=reqType(p)]∈{0,1}\displaystyle \mathbb{1}[\text{gpuType}(n) = \text{reqType}(p)] \in \{ 0, 1 \}
  3. 权重 alpha$\alpha$ 控制空闲率的重要性,体现“资源充足优先”。
    α≥0\displaystyle \alpha \ge 0
  4. 权重 beta$\beta$ 控制型号匹配的加分强度,体现“硬件一致优先”。
    β≥0\displaystyle \beta \ge 0
  5. 总分决策两项加权求和,得分最高的节点被选中。
    n∗=arg⁡max⁡n∈CGPU-Score(n)\displaystyle n^{*} = \arg\max_{n \in \mathcal{C}} \text{GPU-Score}(n)

AI 应用场景

大模型推理平台的 GPU 配额调度

阶段:分布式调度与资源管理

解决的问题:多团队共享 GPU 集群时,人工排队分配频繁冲突,资源利用率低,模型常因“卡不对”而失败。

具体案例:某公司搭建 LLM 推理平台,注册自定义 Filter 插件校验 GPU 显存与型号,注册 Score 插件优先选同型号且空闲率高的节点,再用 Pod 反亲和让同一服务的多个副本分散到不同节点。结果:单卡故障不再拖垮整个服务,集群利用率提升约 40%。

电商异地多活的拓扑感知调度

阶段:分布式调度与资源管理

解决的问题:大促流量需要就近接入,但订单服务的三个副本又必须跨可用区部署,避免单机房故障。

具体案例:用 topologySpreadConstraints 把订单服务三副本均匀分布到三个 zone,用节点亲和把依赖的 Redis 缓存调度到同 zone 节点,将跨可用区网络延迟从 8ms 降到 1ms,同时保障容灾能力。

分时混部的优先级调度

阶段:分布式调度与资源管理

解决的问题:白天在线推理峰值、夜间训练任务排队,集群资源利用率波动大。

具体案例:通过自定义 QueueSort 插件实现优先级队列(在线任务 > 离线任务),离线任务白天占用空闲资源,高峰期被 Permit 插件延迟放行。平台整体装箱率从 35% 提升到 70%。

Python 演示

python57 行
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")
目的:用 50 行代码模拟 K8s 调度器的“过滤 + 打分”两阶段决策,并演示 GPU 型号亲和与空闲优先的自定义插件扩展。
  1. self.filters = [filter_resources, filter_gpu_type]把过滤插件注册进调度器,对应 Scheduler Framework 的插件注册机制。
  2. all(f(node, pod) for f in self.filters)逻辑“与”:所有过滤器都通过,节点才进入候选池。
  3. score_gpu_free空闲 GPU 越多得分越高,体现资源充足优先策略。
  4. score_gpu_type型号匹配加 30 分,体现硬件一致优先策略。
  5. max(candidates, key=lambda n: sum(...))取总分最高的节点,对应打分阶段的 argmax。
  6. 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 选择节点的两个核心阶段是什么?

单选题 · easy

  • A.拉取与推送
  • B.过滤与打分← 正确答案
  • C.探测与注册
  • D.编译与执行

答案:B。调度器先过滤出满足硬性条件的候选节点,再打分选出最优。拉取/推送是镜像与日志的传输动作,探测与注册是节点入池时的动作,编译与执行与调度决策无关。

Q2. 过滤阶段中资源的比较发生在哪两个量之间?

单选题 · easy

  • A.可分配量与已请求量← 正确答案
  • B.CPU 型号与主频
  • C.磁盘品牌与容量
  • D.内存条数与频率

答案:A。调度器维护的是“可分配量 − 已请求量”的剩余账本,新 Pod 的申请量必须小于等于剩余量。型号、品牌、条数都不是调度器做资源判定的依据。

Q3. Pod 长时间处于 Pending 状态,最可能说明什么?

单选题 · mid

  • A.容器启动过程失败
  • B.节点正在重启宕机
  • C.尚未选出可绑定的节点← 正确答案
  • D.镜像拉取超时重试中

答案:C。Pending 表示调度器尚未完成“选房”,即过滤后候选池为空或打分后未能绑定。容器启动失败对应 CrashLoopBackOff,镜像拉取问题对应 ImagePullBackOff,都不属于 Pending 的典型原因。

Q4. 关于打分阶段,下列说法正确的是?

单选题 · mid

  • A.任一打分不满足则节点出局
  • B.多个打分插件加权求和← 正确答案
  • C.总分越高代表节点越空闲
  • D.打分阶段只看 CPU 资源

答案:B。打分是多个插件加权求和的结果,权重表达平台策略;打分不满足是“分低”而不是“出局”,出局只发生在过滤阶段。总分越高是“综合最合适”,不代表一定最空闲。

Q5. 以下哪些属于 Scheduler Framework 的扩展点?

多选题 · mid

  • A.QueueSort 排队扩展点← 正确答案
  • B.Filter 过滤扩展点← 正确答案
  • C.Score 打分扩展点← 正确答案
  • D.Sidecar 边车容器

答案:ABC。QueueSort 决定 Pod 排队顺序,Filter 负责硬性过滤,Score 负责软性打分,三者都是 Scheduler Framework 的扩展点。Sidecar 是服务网格中的边车容器模式,与调度扩展点无关。

Q6. Reserve 扩展点的核心作用是什么?

单选题 · hard

  • A.把 Pod 绑定关系写入服务端
  • B.预占节点资源防止并发覆盖← 正确答案
  • C.启动容器运行时进程调度
  • D.拉取容器镜像到目标节点

答案:B。Reserve 在调度周期内临时预占资源,避免多个调度决策并发覆盖同一块资源;若最终未绑定,Unreserve 会回滚预占。写绑定是 Bind 的职责,启动容器是 kubelet 的职责。

Q7. 关于 GPU 亲和调度,以下做法正确的有?

多选题 · mid

  • A.检查节点 GPU 型号是否匹配← 正确答案
  • B.优先选择 GPU 空闲率高的节点← 正确答案
  • C.把训练任务调度到多卡节点← 正确答案
  • D.忽略 Pod 申请的显存大小

答案:ABC。GPU 调度要检查型号匹配、优先空闲率高的节点,并把分布式训练任务聚集到多卡节点降低通信损耗。忽略显存大小会导致 OOM,是错误做法。

Q8. 拓扑感知调度可以通过哪些方式实现?

多选题 · hard

  • A.节点亲和性选择← 正确答案
  • B.日志自动采集
  • C.拓扑分布约束← 正确答案
  • D.Pod 间反亲和← 正确答案

答案:ACD。节点亲和让 Pod 落到指定 zone,拓扑分布约束用 maxSkew 控制副本散布,Pod 反亲和避免副本挤在同一节点。日志采集与调度决策无关。

Q9. 调度器完成 Bind 之后,真正启动业务容器的是?

单选题 · easy

  • A.节点上的 kubelet← 正确答案
  • B.集群里的 etcd
  • C.网关层 apiserver
  • D.控制面控制器

答案:A。Bind 只把 nodeName 写入 API Server,kubelet watch 到绑定变更后拉镜像并启动容器。etcd 是存储层,apiserver 是 API 网关,controller-manager 负责其他控制器逻辑。

Q10. 自定义 Permit 插件拒绝了某 Pod 后,调度器会调用哪个回调释放已预占资源?

单选题 · extreme

  • A.Unreserve 释放预占资源← 正确答案
  • B.PostBind 完成善后处理
  • C.PreFilter 重跑过滤逻辑
  • D.QueueSort 重新排序等待

答案:A。Permit 拒绝后,调度器必须回滚 Reserve 做的资源记账,对应回调是 Unreserve。PostBind 只在绑定成功后触发,PreFilter 与 QueueSort 都在调度周期更早阶段生效,均不负责资源释放。