第 6 章 调度器:把 Pod 放到哪台机器
墨:老哥,前面咱们把”状态从哪来、谁在调和”讲透了。现在一个最具体的问题跳出来:期望状态里写着”要 3 个 Pod”,但具体落哪台机器? 这决策谁做?怎么做?你先想想,如果让你写,你会怎么落?
6.1 调度器是干啥的(划清边界)
先划清:调度器只做一个动作——给”还没有 nodeName 的 Pod”挑一个节点,然后把节点名写进 Pod 的 spec.nodeName。写完,它的活就结束了。真正在节点上把容器拉起来的是 kubelet(第 8 章)。
墨:那为什么不让 API Server 直接写 nodeName?为什么单独搞个调度器?
因为”挑节点”是个有策略的决策,不是简单的”存一下”。它要综合考虑资源够不够、亲和规则、拓扑、优先级、抢盘……这些逻辑放 API Server 里会把它撑爆,而且 API Server 的职责是”门面+规则”,不含”资源分配策略”。所以 K8s 把”分配决策”独立成调度器这个组件,通过状态板(Pod 的 nodeName 字段)和别的组件协作。又是一次关注点分离:写入口 ≠ 分配决策。
6.2 最朴素的调度:随机挑一台能跑的不就行了?
你第一反应可能是:扫描节点列表,找一台”内存够、CPU 够”的,放上去。这在小集群没问题。但现实中”能跑”只是及格线,调度要回答更难的问题:
- A 机器剩 1G 内存,Pod 要 512M,能放(及格);但 B 机器剩 8G,放 B 更”平衡”,放 A 可能下个 Pod 就挤爆 A——放哪更优?
- 用户说”这个 Pod 要和 redis 放一起(亲和)“或”别和同事放一起(反亲和)“——怎么满足?
- 用户说”这个节点是 GPU 机,只给训练任务用(污点 taint)“,普通 Pod 不能放——怎么拦?
- 100 个 Pod 同时来,集群满了,重要的放不进,不重要的挤掉了重要的——谁优先?
- 节点在南京,Pod 要低延迟访问上海的同城数据库——怎么感知位置?
调度器就是为这些”及格线之上”的决策而存在的。它的两个核心阶段,名字你一定要记住:过滤(Filter)和打分(Score)。
6.3 阶段一:过滤(Filter / Predicates)——“哪些节点有资格”
过滤的作用是淘汰不合法的节点,回答”这个 Pod 能不能放这台机器”。经典过滤器:
- 资源够不够:节点可分配资源(Allocatable)减去已分配给其他 Pod 的,是否 ≥ 本 Pod 的 requests?注意是比 requests 不是 limits(第 15 章讲为什么);
- 节点选择器(nodeSelector):Pod 声明”只要带 disktype=ssd 的节点”,不带这标签的淘汰;
- 污点容忍(Taint/Toleration):节点有
gpu=true:NoSchedule污点,Pod 没对应 toleration,淘汰; - 亲和性(Affinity):Pod 要求”和带 app=redis 的 Pod 同可用区”,不满足的淘汰;
- 端口冲突:Pod 要 hostPort 80,节点上 80 已被占,淘汰;
- 卷冲突:Pod 要挂某云盘,盘已在另一节点挂载且不支持多挂,淘汰。
过滤结果是”候选节点集合”。如果空集 → Pod Pending(调度失败),事件里写”0/10 nodes are available…”——这行你排障必看,它逐条告诉你每台机器为什么不行。
墨:你说”比 requests 不比 limits”,如果 Pod 没写 requests 会怎样? 这坑我们第 15 章专门炸,先记着:不写 requests,调度器以为它”不吃资源”,可能把一堆 Pod 堆一台机器,然后全被 OOM 或 CPU 饿死。
6.4 阶段二:打分(Score / Priorities)——“候选里谁最好”
过滤出的候选们”都能放”,调度器给每个打分,选最高分。常用打分函数:
- LeastRequested(最少请求优先):谁剩余资源多给谁高分 → 让负载分散,避免热点;
- BalancedResourceAllocation(资源平衡):CPU 和内存使用率都中等偏低的优先 → 避免”CPU 满内存闲”的畸形分配;
- ImageLocality(镜像本地性):节点已有 Pod 镜像的给高分 → 省拉镜像时间,Pod 起得快;
- NodeAffinity / PodAntiAffinity 打分:越满足亲和/反亲和规则的越高分;
- TopologySpread(拓扑打散):尽量把同服务的 Pod 均匀分布在不同可用区/节点 → 容灾。
墨:打分函数之间会打架吗? 会。比如 LeastRequested 想把 Pod 散开,ImageLocality 想往有镜像的节点堆。所以 K8s 给每个打分函数一个权重(weight),最后加权求和。权重怎么设,是调度策略的调音台——默认值适合通用场景,特殊负载(如 AI 训练要堆 GPU 节点)得调。
6.4.1 一个反直觉:调度目标是”好”不是”最优”
墨:你猜,调度器会不会算出”全局最优”的摆放?
不会,也不可能。 把 N 个 Pod 摆到 M 台机器,找”总通信成本最低 / 资源最均衡”的全局最优,是个 NP-hard 的组合优化问题——节点稍多就算法爆炸。所以 K8s 调度器是启发式(heuristic):用一组”足够好”的打分规则快速出结果,追求”局部优、整体可接受”,而非”数学最优”。
这给你的系统设计启示极深:分布式系统里,别追求全局最优,追求”足够好且快”。 因为全局最优的算力成本,在规模下超过它带来的收益。K8s 选了”单 Pod 独立打分选最优节点”,不考虑”我这么放了之后别人怎么办”——代价是可能不是全局最优,收益是毫秒级、可水平扩展。这是”近似算法换可扩展性”的经典权衡。
6.5 阶段三:绑定(Bind)——“盖章生效”
打分选出一个节点后,调度器执行 Bind:通过 API Server 把 nodeName 写进 Pod。写完,Pod 进入该节点的 kubelet 视野,kubelet 拉起容器。
墨:绑定前要”占坑”吗?并发下两个 Pod 会不会同时被判给同一台机器?
会,如果不防。所以调度框架里有 Reserve(预留) 阶段:选定节点后,先在调度器内存里”预留”这台节点的资源(记一笔”这个 Pod 占了 X 资源”),再 bind。如果 bind 失败(比如 API Server 写挂了),再 Unreserve 把预留撤掉。这避免了”两个 Pod 都以为自己拿到了最后 2G,结果都写成功、节点实际超卖”。
注意:这个”预留”是调度器内存里的乐观账本,不是 etcd 里的硬占用。所以理论上极端并发仍有窗口——但配合 resourceVersion(第 4 章)和优先级的串行处理,实际极少出问题。又是”用轻量乐观锁 + 重试”化解分布式冲突。
6.6 抢占(Preemption):重要的 Pod 没位置,怎么办
墨:如果高优先级的 Pod 来了,集群满了,过滤全是空集,怎么办?让它一直 Pending?
不肯。K8s 有 抢占(Preemption):调度器发现某高优 Pod 调度不上,会找”如果驱逐一些低优 Pod,高优 Pod 就能放下”的节点,选一台,把那台上的低优 Pod 删掉(驱逐),腾出空间给高优 Pod。
6.6.1 优先级从哪来:PriorityClass
Pod 通过 priorityClassName 关联一个 PriorityClass(集群级对象,带一个整数 priority)。数字越大越尊贵。抢占时,调度器按优先级排序,专挑”被牺牲者”里优先级最低、且数量刚够腾空间的那些。
⚠️ 坑: 抢占是”删 Pod 让它重建”,被删的 Pod 如果没控制器管(裸 Pod),就真的没了。而且抢占可能引发”驱逐风暴”——一堆低优被删重建,重建又抢资源,再被删……生产要设好 PriorityClass 和 PDB(PodDisruptionBudget,保护最少可用数)。
6.6.2 演化:抢占的”优雅化”
早期抢占就是”暴力删低优 Pod”。后来社区加了 PodDisruptionBudget 感知——调度器抢占时会尽量不违反 PDB(即保证被驱逐的服务还剩最小可用副本)。再后来 K8s 引入 优雅抢占 / 抢占让位(preemption via deletion then scheduling) 的细化策略,但仍是基于”删了重建”的语义。本质上,K8s 没做”在线迁移”(把运行中的 Pod 搬到别处),因为那超出调度器职责且极复杂。 这是个有意的边界:调度器只决定”放哪”,搬动由控制器(删除+重建)完成。
6.7 调度框架(Scheduling Framework):把决策做成插件
墨:你说打分函数权重可配,但我想加一个”自己的打分规则”(比如”优先放离用户近的机房”),得改 K8s 源码吗?
早期(K8s 1.15 前)确实很难——predicates/priorities 是写死在调度器里的,要扩展得改代码或写独立调度器。后来有了 Scheduling Framework:把调度流程拆成一系列扩展点(extension points),你写个 plugin 实现对应接口,注册进去,就成了调度逻辑的一部分。
扩展点(你扫一眼有多少个切面):
PreFilter/Filter/PostFilter(过滤前后及抢占)PreScore/Score/NormalizeScore(打分)Reserve/Unreserve(预留)Permit(准入闸门:可暂缓/批准/拒绝绑定,比如等某个外部条件)Bind(绑定,可自定义绑定逻辑)
意义: 调度器从”写死的策略集合”变成”插件宿主”。你想加”GPU 拓扑感知""亲近特定机房""基于实时水位调度”,写个 plugin 挂上即可,不动核心。这和第 3 章”控制器模式开放”、第 4 章”准入 webhook 开放”是同一哲学的三次落地:K8s 把每个核心环节依次做成”可插拔的”。
贯穿全书的元模式:K8s 的成长史 = 不断把”内置写死的能力”变成”对外可插拔的扩展点”。 API 模型(CRD)、策略(webhook)、调度(framework)、存储(CSI)、网络(CNI)、运行时(CRI)——六大接口全开放。记住这条线,你看 K8s 就不会觉得它是一坨,而是一套”留了无数插槽的骨架”。
6.8 调度队列:Pod 先排队,再调度
调度器不是来一个调度一个,它有个调度队列:
- 优先级队列:高 priority 的 Pod 排前面(也呼应抢占语义);
- backoff 机制:调度失败的 Pod 不会立刻无限重试(避免刷屏),而是退避一段时间再试;
- active / backoff / unschedulable 三态:暂时调度不了的进 unschedulable,等集群变化(节点加了、别的 Pod 删了)被唤醒重试。
墨:为什么失败的要 backoff 而不是马上重试?
因为”调度不上”往往不是瞬时的——集群就是满了。立刻重试只是空转 CPU、刷爆事件日志。backoff 让它在”资源可能释放”后再试,既省资源又不漏。又见”level-triggered 自愈”:不靠”调度失败事件”驱动,而靠”周期性重试 + 集群状态变化唤醒”。
6.9 调度性能与规模
大集群(数千节点、上万 Pod)调度吞吐是个硬指标。K8s 调度器做了不少优化:
- 等价类缓存(Equivalence cache,旧):过滤结果按节点属性缓存,相似 Pod 复用;
- 事件触发调度:节点变化才唤醒相关 Pod,而非全量重算;
- 并行打分:候选节点打分可并行(Go 的 goroutine 天然适合);
- 百分比调度(PercentageOfNodesToScore):超大集群不必过滤全部节点,抽样一部分够了(启发式再临场)。
演化看点: 调度器从”每次全量过滤所有节点”优化到”可配置只检查部分节点”,又是”用近似换规模”。默认在小集群检查 100% 节点,大集群自动降采样——因为”检查 50% 节点找到的合适机器”和”检查 100%“在质量上没差多少,但快一倍。
6.10 调度失败排查心智(先埋,第 17 章展开)
你将来排障高频场景:kubectl get pods 看到 Pending,describe 看到 Events: 0/10 nodes are available: 3 node(s) had untolerated taint, 5 Insufficient memory, 2 node(s) didn't match Pod anti-affinity...。
墨:这行事件其实在告诉你什么?
它在告诉你过滤阶段的淘汰原因逐条清单——哪台因为污点、哪台因为内存不够、哪台因为亲和。读懂这行,你就知道该加节点、放宽 requests、加 toleration 还是改亲和规则。排障不是玄学,是顺着调度器的”决策日志”反向推。先记着这个入口。
6.11 本章演化线小结
- 边界:调度器只”挑节点 + 写 nodeName”,拉起归 kubelet;
- 两阶段:Filter 淘汰不资格 → Score 加权选最优;
- 目标:启发式”足够好”,不追求 NP-hard 全局最优,用近似换可扩展;
- 预留/解预留:轻量乐观账本防并发超卖;
- 抢占:高优 Pod 靠驱逐低优腾位,受 PDB 约束,基于”删了重建”语义;
- 调度框架:predicates/priorities 写死 → 插件化扩展点,是”核心开放”哲学的落地;
- 队列:优先级 + backoff + 状态机,失败不空转;
- 性能:并行打分 + 大集群降采样,近似换吞吐。
6.12 本章思考题
🔧 思考题 1
假设集群 3 节点,资源各剩:A(CPU 1核/内存 1G)、B(CPU 4核/内存 8G)、C(CPU 4核/内存 8G)。来 3 个相同 Pod,每个 requests CPU 1核/内存 1G。按 LeastRequested 打分,它们会怎么分布?如果打分只看”剩余 CPU 比例”会怎样?
【参考答案】 按 LeastRequested(综合 CPU+内存剩余比例),初始:A 剩余比低(剩 1/总,假设总 4核/8G,则 A 剩 25%/12.5%),B、C 剩高(约 75%/100% 视已用)。第 1 个 Pod:B、C 分高,选其一(如 B)。预留后 B 剩 3核/7G。第 2 个:C 剩最高,选 C。第 3 个:A 剩比最低、B 次之、C 次之——但 A 还剩 1核/1G 刚好够,LeastRequested 给 A 的”剩余比例”其实更低(它剩的少),所以不会选 A,会选 B 或 C 中剩余比例高的。结果:3 个 Pod 全在 B、C(各 1~2 个),A 空闲。如果打分只看 CPU 剩余比例且忽略内存,可能误把 Pod 放 A(若 CPU 比例算错),导致 A 内存刚好够但无余量,下个稍大的 Pod 直接 Pending。这题说明:(1) 打分要综合多维度,否则维度盲区导致错配;(2) “散开”策略有意留有余量,避免节点贴边运行(贴边=下次调度易失败+运行时易 OOM)。
🔧 思考题 2
抢占会删除低优先级 Pod。如果这个被删的 Pod 是裸 Pod(没有 Deployment/RS 管),会发生什么?怎么避免”重要服务被误杀”?
【参考答案】 裸 Pod 被抢占删除后不会重建(没有控制器调和期望),等于这个服务实例永久消失——如果是重要服务,就是事故。避免:(1) 永远用控制器管 Pod(Deployment/StatefulSet),裸 Pod 只用于一次性调试;(2) 用 PriorityClass 区分:重要服务设高 priority,根本不会被选为牺牲者;(3) 用 PodDisruptionBudget(PDB):声明”某服务至少保持 N 个可用”,调度器抢占时会尽量不违反 PDB(若违反会导致低于最小值则放弃该节点抢占);(4) 关键 Pod 设 priority 高于普通,且配合 PDB,双保险。根本启示:抢占是”牺牲低优保高优”的机制,你必须在 PriorityClass + PDB 上显式表达”谁重要”,否则系统按默认(priority 0)一视同仁,重要服务也可能被牺牲。
🔧 思考题 3
Scheduling Framework 把调度做成插件。如果某公司要”优先把 Pod 调度到离请求来源用户最近的机房”,请描述你会实现哪个扩展点的 plugin,它的输入和输出是什么?
【参考答案】
这是个”打分偏好”类需求,应实现 Score / NormalizeScore 扩展点(也可加 PreFilter 先过滤掉不满足就近的节点)。输入:当前候选节点列表 + 待调度 Pod + 调度器缓存(节点元数据、机房标签、可能的用户位置标注)。输出:给每个候选节点打一个分数(越近越高)。实现要点:(1) 节点需带 topology.kubernetes.io/zone 或自定义 region 标签标识机房;(2) “用户就近”信息从哪来?Pod 可能没这信息——需通过注解/标签(如 scheduling.near=user-region:sh)或外部系统(如调度器 plugin 查请求来源)注入;(3) 打分后 NormalizeScore 归一到统一区间,再和内置权重合并。若”就近”是硬约束(必须同机房),则放 Filter 阶段(不满足直接淘汰)而非 Score。这题考你”需求是硬约束还是偏好”决定放 Filter 还是 Score——这是写调度 plugin 的第一判断。
🔧 思考题 4(进阶)
K8s 调度器追求”单 Pod 局部最优”而非”全局最优”。这在什么场景下会累积成明显问题(即局部贪心导致全局次优)?有没有补救机制?
【参考答案】 典型场景:亲和/反亲和的全局效应。比如要求”同服务 Pod 反亲和(别堆同节点)“,单 Pod 调度时只看”当前这台节点有没有同服务”,但贪心放置可能在集群整体不均衡时,把前 N 个都放少数节点(因为那些节点当时空),导致后续 Pod 因反亲和被 Pending,而别的节点其实更空。又如 bin-packing vs spreading 冲突:想省机器(bin-pack 往满里塞)和想容灾(spread 打散)是反向目标,单 Pod 贪心无法兼顾全局成本。补救:(1) TopologySpreadConstraints:显式声明”跨 zone/node 均匀分布”,把全局意图变成每 Pod 的硬/软约束,调度器按约束打分,逼近全局打散;(2) descheduler:独立组件,周期性把”放歪了”的 Pod 驱逐重调度(弥补贪心长期偏差);(3) 集群自动扩缩(CA):局部装不下就加节点,从外部消解压力。结论:单 Pod 贪心是必须的(规模所迫),全局偏差靠”约束 + 事后纠正(descheduler)+ 弹性资源”三层补救。这正是”近似算法 + 反馈校正”的工程实现。
6.13 小结
- 调度器只做”挑节点 + 写 nodeName”,拉起归 kubelet,关注点分离;
- 两阶段:Filter 淘汰不资格 → Score 加权选最优,目标是”足够好”非全局最优;
- Reserve/Unreserve 防并发超卖,Bind 经 API Server 落盘;
- 抢占靠驱逐低优腾位,受 PriorityClass + PDB 约束,语义是”删了重建”;
- Scheduling Framework 把调度做成插件,是”核心能力开放”哲学的又一落地;
- 队列用优先级 + backoff + 状态机,失败不空转;大集群降采样换吞吐;
- 调度失败看
0/N nodes are available事件行,是排障入口。
下一章,我们讲”控制器模式全集”——Deployment/StatefulSet/Job 这些你天天写的对象,每一个背后都是一类不同的工作负载难题。它们为什么不能一个通吃?