【K8s 调度三阶段 · 避坑完全指南】过滤→打分→绑定,9 成 Pending 都卡在第一关

作为一个跟 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 CPUlimits: 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 做抢占式调度,除非你有充分理由并做了充分的隔离测试。你如果真的需要多种调度特性,建议用 VolcanoKueue 这类高级调度器,覆盖默认调度器无法做的一批调度(Gang Scheduling)、按负载感知调度、队列配额等场景。

调度器的可观测性:你盯着它了吗?

作为 SRE,调度器出了问题是很难定位的,尤其是它无声地做了错误的调度决策却没有显式错误时。我强烈建议在你的监控体系里加入调度器指标:

  • 调度延迟scheduler_schedule_attempts_totalscheduler_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?有没有遇到过调度器"选错节点"的奇葩案例?评论区见。

相关推荐
云游牧者2 小时前
深入理解K8S-Pod生命周期与资源管理-CSDN博客
kubernetes·探针·pod生命周期·pod优雅终止
keyipatience2 小时前
Linux进程调度与优先级机制解析
linux·运维·服务器
在角落发呆2 小时前
Windows 8系统下的IP转发:一台电脑如何变身网络桥梁
运维·服务器
日取其半万世不竭3 小时前
Tekton:Kubernetes 原生 CI/CD 流水线
ci/cd·kubernetes·tekton
SPC的存折3 小时前
14、K8S-NetworkPolicy
运维·云原生·容器·kubernetes
Splashtop高性能远程控制软件3 小时前
切屏时代终结,Splashtop 统一 IT 运维平台助力 MSP 高效运营
运维·自动化·远程控制·splashtop
9命怪猫3 小时前
[K8S小白问题集] - Flannel是K8S默认CNI吗?怎么实现的Overlay网络?
网络·容器·kubernetes
小此方3 小时前
Re:Linux系统篇(十二)工具篇 · 四:make与Makefile:高效管理 C++ 工程项目构建
linux·运维·c++·开发工具
隔窗听雨眠3 小时前
读懂AI自动化的两种范式
运维·人工智能·自动化