作为一个跟 K8s 调度器斗智斗勇的 运维,我敢说一句:Pod 调度这事儿,可以说是集群里最容易让人头秃的环节之一。
你有没有遇到过这种情况------Pod 一直挂在 Pending 状态,kubectl describe 一看,满屏都是 FailedScheduling,但你死活看不出是哪里出了问题?或者明明节点资源还有很多,Pod 就是死活不上去?更别提那些线上流量尖峰时,新扩容的 Pod 迟迟不 ready。
今天这篇,我把 K8s 调度的核心三阶段------过滤(Filter)→ 打分(Score)→ 绑定(Bind)彻底掰开揉碎了讲。不搞那些教科书式的照本宣科,全都是我在生产环境踩过的坑和填坑经验。读完你将能:
- ✅ 精准定位 Pod Pending 的根本原因
- ✅ 理解调度器每一步到底在干什么
- ✅ 掌握过滤规则和打分算法的配置技巧
- ✅ 学会如何"驯服"调度器,让它按你的意图工作
顺便说一句,我下面用的版本是 k8s v1.29+。如果你的集群是 1.20 以下的老版本,概念类似但调度框架差异比较大,建议先升到 1.24+ 再参考。
太长不看版:调度三阶段一句话总结
|------------|------|-----------------|----------------------------|
| 阶段 | 简称 | 核心动作 | 决策逻辑 |
| 过滤(Filter) | 硬件检查 | 剔除不合格节点 | 只要有一个条件不满足,这个节点就出局 |
| 打分(Score) | 择优录取 | 为候选节点排座次 | 每个插件打了分之后按权重求和,分高者胜 |
| 绑定(Bind) | 落袋为安 | 把 Pod 和 Node 绑定 | 通过 API Server 写入 etcd,调度完成 |
关键认知:过滤是硬约束,有一条不满足就 pass;打分的权重和策略直接影响 Pod 最终落在哪里。
环节一:过滤(Filtering)------硬件检查,一票否决
它在干嘛?
过滤阶段负责把绝对不能用的节点直接踢出去。这类似于你要租房子------没有独立卫浴的、隔音太差的、离地铁太远的,直接 pass,根本不用考虑价格。
调度器会把这个 Pod 的参数和集群里每个节点的信息逐一比对,任何一个过滤器说不通过,这个节点就出局。
默认的过滤插件,你不得不知道的几个
新版调度框架的过滤逻辑通过一组插件实现。列几个最常用、也最容易出问题的:
|-----------------------|------------------------------|----------------------------------------------|
| 插件 | 检查什么 | 典型翻车现场 |
| NodeResourcesFit | CPU、内存是否够用(看 requests) | Pod 的 CPU request 是 4 核,所有节点都只剩 2 核 |
| NodeUnschedulable | 节点是否被 cordon 了 | 有人 kubectl cordon 之后忘了 uncordon |
| TaintToleration | Pod 能否容忍节点的污点 | 节点打了 gpu=true:NoSchedule,Pod 没配 toleration |
| NodeAffinity | 节点标签是否满足硬亲和规则 | Pod 要求 disk=ssd,但节点标签是 disk=hdd |
| PodTopologySpread | Pod 在不同拓扑域(zone/node)的分布是否超标 | 某个 zone 已经跑了太多实例,超出 maxSkew |
| NodePorts | 节点上的端口是否被占用了 | Pod 要求使用 80,但节点上已有其他应用占了这个端口 |
坑点 1:调度器只看 requests,不看 limits
这个坑我当初真的是被坑惨了。你以为 Pod 要调度到一个节点上,调度器会同时检查 requests 和 limits?No! 调度器只根据 requests 做决策,limits 只用于运行时 cgroup 限制。
这就导致一个经典场景:你给一个 Pod 配了 requests: 0.5 CPU 但 limits: 4 CPU,调度器觉得这个 Pod 很"省",随手塞到一个资源紧张的节点上。结果 Pod 一跑起来就想吃 4 核,直接跟邻居抢资源搞出 CPU throttling,整晚 P99 飙升......排查到天亮才发现是调度策略的问题。
解决办法 :关键业务的 requests 值要往真实需求上靠,不要为了"省资源"把 requests 压得太低。我的原则是:requests 设置为预期的 50~80%,limits 设得高一些但别太离谱。
坑点 2:NodeAffinity 的匹配语法容易写错
很多人在 NodeAffinity 里写 requiredDuringSchedulingIgnoredDuringExecution 的时候,会把 operator 写成字符串而不是 KV 字段。
# ❌ 错误示范
nodeSelectorTerms:
- key: "disk-type"
values: ["ssd"]
operator: In # operator 的位置错了
# ✅ 正确写法
nodeSelectorTerms:
- matchExpressions:
- key: "disk-type"
operator: In
values: ["ssd"]
如何定位过滤失败的问题?
当你看到 Pod Pending,第一个动作永远是:
kubectl describe pod <pod-name> -n <namespace>
看 Events 里类似这样的信息:
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning FailedScheduling 12s default-scheduler 0/3 nodes are available: 1 Insufficient cpu, 1 node(s) had taint {gpu: true}, that the pod didn't tolerate, 1 node(s) didn't match Pod's node affinity.
这三条信息对应三个被过滤掉的原因:
- 1 号节点:CPU 不够(NodeResourcesFit 没通过)
- 2 号节点:污点没容忍(TaintToleration 没通过)
- 3 号节点:节点亲和性不匹配(NodeAffinity 没通过)
这时候你就会知道,不是调度器不努力,是真的没有可用节点。
过滤顺序有讲究吗?
有的。调度器会按配置顺序调用过滤插件。但更重要的一个点是------调度器可以配置不扫描所有节点,只扫描一个抽样子集 ,这样能大幅提升调度性能,尤其是大集群。具体比例通过 percentageOfNodesToScore 控制。但这也会导致一个问题:如果抽样比例太低,可能错过最优节点。大集群里建议用默认值(根据节点数自动计算),小集群可以设成 100。
环节二:打分(Scoring)------择优录取,量化决策
它在干嘛?
过了过滤关的节点,至少都是"能用"的。但哪个最好?打分阶段来排座次。
每一个分数插件都给节点打一个 0--100 的原始分,然后按权重(weight)加权求和,得到最终分数。分数最高的节点就是胜出者。
默认的打分插件
|---------------------------------|----------------|-------|
| 插件 | 打分逻辑 | 权重 |
| NodeResourcesFit | 根据资源使用策略打分 | 1(可调) |
| NodeResourcesBalancedAllocation | CPU/内存比例越均衡分越高 | 1 |
| ImageLocality | 节点上是否已有容器镜像 | 1 |
| NodeAffinity | 软亲和规则越匹配分越高 | 1 |
| PodTopologySpread | 分布越均匀分越高 | 1 |
NodeResourcesFit 的两种打分策略,你得想清楚
NodeResourcesFit 插件有两种核心打分策略,方向和权重你得想清楚:
1. MostAllocated(偏爱资源利用率高的节点)
分高=节点已分配的资源多。相当于"紧凑装箱",把小 Pod 都往已有负载的节点上塞,把空闲节点留给大家伙。
scoringStrategy:
type: MostAllocated
resources:
- name: cpu
weight: 1
- name: memory
weight: 1
2. LeastAllocated(偏爱资源空闲多的节点)
分高=节点空闲资源多。优先把 Pod 往空闲节点上放,所有 Pod 均匀撒开。
我自己的偏好 :生产环境我通常用 MostAllocated + 适当的预留水位。因为这意味着老节点的资源利用率会更高,需要扩容时才用到新节点。混合部署环境(在线+离线)尤其推荐 MostAllocated,能让在线业务尽量占满节点,把空闲资源留给任务批处理。
RequestedToCapacityRatio 是什么?为什么我推荐你试试
如果不想要要么"极端紧凑"要么"极端分散"的二选一,可以自己定义打分函数 RequestedToCapacityRatio。
示例配置(从官方文档抄来改了一下):
scoringStrategy:
type: RequestedToCapacityRatio
requestedToCapacityRatio:
shape:
- utilization: 0
score: 0 # 利用率 0% 给 0 分
- utilization: 100
score: 10 # 利用率 100% 给满分
这样调出来的效果是:利用率越高分越高,比 MostAllocated 更"激进"。用得好可以进一步提高资源利用率,但调度延迟会稍稍上升,因为需要做更精细的分数计算。
补充一个我踩过的坑 :shape 数组默认会线性插值,但如果你写两个点之外再加一个 utilization: 50, score: 3,那就是非线性权重!调度器是按你定义的点连成折线来算分的,你可以用这个特性来实现"低利用率段给负激励、高利用率段给正激励"。
如果只想用某个打分插件怎么办?
修改调度器配置文件 KubeSchedulerConfiguration,删掉不想要的插件,或者置 disabled: true。或者更简单------如果你只是想调整权重,只修改对应插件的 weight 就行,不用重新编译。
环节三:绑定(Binding)------落袋为安,尘埃落定
它在干嘛?
选出了分数最高的节点之后,调度器会把 Pod 和这个节点绑定 在一起------其实就是修改 Pod 的 spec.nodeName 字段,把选出节点的名字填进去。
这个步骤是通过调用 Kubernetes API Server 完成的,写到 etcd 之后调度才算真正完成。然后目标节点的 kubelet 看到 Pod 被分配给了自己,就会拉取镜像并启动容器。
绑定不是终点
绑定完成只是在控制平面的视角里 Pod 已经落在一个节点上了。真正要等 kubelet 拉取镜像、创建容器、业务进程启动,Pod 才变成 Running。所以,Pod 状态从 Pending → Binding → Running 之间有一个时间差,如果有监控告警要考虑到这个延迟。
绑定失败怎么办?
绑定阶段也可能 fail,比如 APIServer 挂了、etcd 写入超时、节点的 Pod 数量超限导致 API Server 更新被拒绝。这时候调度器会把这个 Pod 重新丢回调度队列,等待下一个调度周期重试。
跟很多新手可能想的不一样,绑定失败不会让整个集群调度器崩溃,只要不是配置了多个独立调度器互相干扰,下次调度循环会自动重来。
多调度器场景:小心"抢 Pod"
如果你的集群里配置了多个调度器(比如 default-scheduler + volcano),某个调度器给 Pod 绑定了节点之后,另一个调度器如果没意识到状态变化,可能就会出现两个调度器都想绑定同一个 Pod 的情况------虽然 API Server 会做并发的乐观锁检查来避免真正冲突,但徒增很多无用的调度计算。
老实说,我不推荐在生产环境里同时启用 multiple scheduler profiles 做抢占式调度,除非你有充分理由并做了充分的隔离测试。你如果真的需要多种调度特性,建议用 Volcano 或 Kueue 这类高级调度器,覆盖默认调度器无法做的一批调度(Gang Scheduling)、按负载感知调度、队列配额等场景。
调度器的可观测性:你盯着它了吗?
作为 SRE,调度器出了问题是很难定位的,尤其是它无声地做了错误的调度决策却没有显式错误时。我强烈建议在你的监控体系里加入调度器指标:
- 调度延迟 :
scheduler_schedule_attempts_total和scheduler_scheduling_attempt_duration_seconds,预警超过 1s 的调度 - pending pods 数量:超过阈值直接触发告警
- filter 和 score 阶段耗时 :可以通过开启 scheduler 的 profiling 获取,如果不做详细 profiling,最简单的办法是周期性
kubectl get events -A --field-selector reason=FailedScheduling来统计哪些 namespace 的 Pod 在反复触发调度失败
几个常见问题速查
1. Pod Pending,Events 里有 NodeAffinity 不匹配
检查硬亲和 requiredDuringScheduling,通常是你写了 matchExpressions 但集群节点没有对应标签。
2. Pod Pending,Events 里显示 Insufficient cpu / memory
节点上已经排满了其他 Pod(按 requests 算的),要么扩容节点,要么减小 Pod requests 值(谨慎操作),要么借助 descheduler 做重调度把部分 Pod 迁移到其他节点。
3. Pod 被调度到了一个你很不想让它去的节点
看看你给这个 Pod 配置了什么 nodeSelector 或亲和性规则,以及打分阶段的权重设置。如果节点 A 资源特别空,打分插件很容易给它打高分。可以直接给不想让 Pod 去的节点打上 NoSchedule 污点,配合适当的容忍规则。
4. 为什么我的 Pod 调度特别慢?
检查三个方向:每 Pod 调度前有没有长时间的 PVC-bound 等待(某些存储卷绑定慢);调度器的 percentageOfNodesToScore 是否设得太低导致反复试探;还是 Node cache 较大但调度器没有访问延迟。一般超过 50ms 就值得优化。
彩蛋:一个你可能不知道的"惊喜"
v1.29 去掉了 selectorSpread 调度插件,全面用 podTopologySpread 来替代。如果你是从老版本升上来的,调度配置文件里如果还有 selectorSpread 引用,升级后调度器会直接报错启动不了。
(我记的某个大客户就是因为这个,集群升级后所有新建的 Deployment 全卡住了,找了两天才发现是调度器启动失败)
写在最后
调度三阶段------过滤、打分、绑定,核心逻辑其实不复杂:
过滤是硬性条件,一票否决;打分量体裁衣,按权重择优;绑定是最后一步,尘埃落定。
但真正复杂的,是什么时候该用硬亲和,什么时候该用软亲和;打分的权重怎么配才合理;集群资源紧张时如何避免调度倾斜。这些要在生产里反复摸爬滚打才知道。
你是用 MostAllocated 还是 LeastAllocated?有没有遇到过调度器"选错节点"的奇葩案例?评论区见。