K8s Lab 把当前仓库文档整理成一个可阅读的网页站点

Repository Reading Site

第三课:调度器如何选节点,为什么 Pod 会 Pending

上一课你已经看到: 但这条链路里有一个环节最容易被“跳过去”: 如果这一步你不理解,后面遇到: 你就只能靠试错,而不是靠判断。 所以这一课的目标是: 1. 讲清 Scheduler 的基本工作方式 2. 讲清常见过滤条件 3. 用你真实集群里的 3 个实验样本,讲清 `FailedScheduling` 的阅读方法 --- Scheduler 的核心流程,可

Markdown03-第三课-调度器如何选节点与为什么Pod会Pending.md2026年4月9日 17:47

第三课:调度器如何选节点,为什么 Pod 会 Pending

为什么这一课必须紧跟在 kubectl apply 主链路之后

上一课你已经看到:

  • Deployment 变成 ReplicaSet
  • ReplicaSet 变成 Pod
  • Scheduler 给 Pod 选节点
  • kubelet 在目标节点上启动容器

但这条链路里有一个环节最容易被“跳过去”:

Scheduler 到底是怎么决定一个 Pod 能不能上某个节点、应该上哪个节点的?

如果这一步你不理解,后面遇到:

  • Pod 一直 Pending
  • 有的 Pod 总跑到错误节点
  • 控制面节点为什么默认不上业务 Pod
  • 为什么资源明明还有空闲,Pod 却调度不上

你就只能靠试错,而不是靠判断。

所以这一课的目标是:

  1. 讲清 Scheduler 的基本工作方式
  2. 讲清常见过滤条件
  3. 用你真实集群里的 3 个实验样本,讲清 FailedScheduling 的阅读方法

先给结论:调度不是“随机分配”,而是“先过滤再打分”

Scheduler 的核心流程,可以先记成两步:

  1. 过滤 Filtering
  2. 打分 Scoring

第一步:过滤

先把“不可能”的节点排除掉。

常见过滤条件包括:

  • 节点资源不够
  • Pod 的 nodeSelector 不匹配
  • Pod 的 affinity 不满足
  • 节点有 taint,Pod 没有 toleration
  • 卷亲和或端口冲突

第二步:打分

在剩下“可以放”的节点里,选一个更优的。

评分会考虑:

  • 资源均衡
  • 亲和性偏好
  • 拓扑分散
  • 其他调度插件策略

你必须记住的一句话

调度不上,优先看过滤原因;调度到哪里,才去看打分结果。

因为如果第一步都没过,第二步根本不会发生。


我们这次不空讲,直接拿你的真实集群做 3 个调度实验

实验文件在仓库里:

实验对象都放在:

  • Namespace: learn-k8s

这 3 个实验分别教你 3 种典型调度判断:

  1. 节点选择命中了,但被 taint 挡住
  2. 加上 toleration 后,调度成功
  3. 没有节点资源够用,因 requests 过大而 Pending

先认识你集群里调度器会用到的“节点事实”

调度器不是凭感觉做决定,它看的都是 Node 对象里的事实。

我们实际看到的关键事实包括:

1. 控制面节点有 taint

你的控制面节点 us480851516617a 有:

node-role.kubernetes.io/control-plane:NoSchedule

这是什么意思?

  • key = node-role.kubernetes.io/control-plane
  • effect = NoSchedule

意思是:

默认情况下,不允许没有对应 toleration 的 Pod 调度到这台节点。

这是一种“保护控制面”的机制。

2. 节点都有内置标签

例如:

  • kubernetes.io/hostname
  • kubernetes.io/os
  • kubernetes.io/arch

这些标签可用于:

  • nodeSelector
  • affinity

例如控制面节点主机名标签是:

kubernetes.io/hostname=us480851516617a

3. 你的节点还有拓扑标签

我们实际看到:

Node region zone
us480851516617a la la-1
us590068728056 la la-1
cp-3 la la-2
hk652699382121 hk hk-1
wk-1 hk hk-1

这意味着你的集群具备了做:

  • 多可用区分散
  • 跨地域调度偏好
  • TopologySpreadConstraints

的基础条件。

4. 节点 allocatable 资源并不一样

当前我们看到的 allocatable 大致是:

  • hk652699382121: 4 CPU, ~8Gi 内存
  • 其他大多数节点:16 CPU, ~16Gi 内存

这说明你的集群是异构的。

所以调度时不能只想着“节点有 5 台”,而要想着:

  • 每台节点的可调度容量不一样

实验一:命中控制面节点,但没有 toleration

我们写了什么

cp-only-no-toleration 这个 Pod 的核心约束是:

nodeSelector:
  kubernetes.io/hostname: us480851516617a

也就是说:

它只允许调度到控制面节点。

但它没有写任何 toleration。

结果

Pod 一直 Pending

真实 describe 证据

事件里写得非常清楚:

0/5 nodes are available:
1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: },
4 node(s) didn't match Pod's node affinity/selector.

这句话怎么读

你必须学会逐段拆:

0/5 nodes are available

5 个节点里,没有任何一个满足条件。

1 node(s) had untolerated taint

那 1 个被 nodeSelector 命中的控制面节点,本来是候选节点。

但它有:

  • node-role.kubernetes.io/control-plane:NoSchedule

而 Pod 没有 toleration,所以被挡住。

4 node(s) didn't match Pod's node affinity/selector

剩下 4 个 worker 节点,连 nodeSelector 都不匹配。

所以它们在过滤阶段就出局了。

这说明了什么

这是一条非常漂亮的调度证据链:

  1. nodeSelector 把候选范围缩小到 1 个节点
  2. 那个节点又被 taint 过滤掉
  3. 最终 0 个节点可用

你从中必须学到的原理

nodeSelector 是“吸引”机制:

  • Pod 说“我只去符合这些标签的节点”

taint/toleration 是“排斥”机制:

  • 节点说“没有容忍资格的 Pod 别过来”

两者是可以叠加的。

这是 Kubernetes 调度里最经典的一组组合规则。


实验二:同样只选控制面节点,但加了 toleration

我们写了什么

cp-only-with-toleration 的关键区别只有这一段:

tolerations:
- key: node-role.kubernetes.io/control-plane
  operator: Exists
  effect: NoSchedule

结果

这个 Pod 成功调度并运行在:

Node: us480851516617a/10.10.0.1
IP:   10.244.248.70
Status: Running

事件里可以看到:

Successfully assigned learn-k8s/cp-only-with-toleration to us480851516617a

为什么这次成功

因为现在它同时满足了两件事:

  1. nodeSelector 命中了控制面节点
  2. toleration 允许它接受这个节点的 NoSchedule taint

于是过滤阶段通过,Scheduler 就可以完成绑定。

但这里有一个非常重要的认知

toleration 不是“强制调度上去”,而只是“允许调度上去”。

这句话很多人会混淆。

也就是说:

  • 没有 toleration:一定不能去
  • 有 toleration:可以去,但不代表一定去

在这个实验里它最终去了控制面,是因为:

  • nodeSelector 已经把候选范围限定死了

所以“可以去”的那个节点,也就成了唯一可去节点。


实验三:内存请求过大导致 Pending

我们写了什么

huge-memory-request 的关键是:

resources:
  requests:
    memory: 100Gi

为什么故意只写 requests

因为调度器看的核心是 requests,不是 limits

这点非常关键。

很多初学者误以为:

  • limits 决定调度

其实不是。

调度器要回答的问题是:

我需要先给这个 Pod 预留多少资源,才能保证它有地方可放?

这个“保证值”就是 requests。

结果

这个 Pod 一直 Pending

真实 describe 证据

事件里写的是:

0/5 nodes are available:
1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: },
4 Insufficient memory.

这句话怎么读

控制面节点

控制面节点依然因为 NoSchedule taint 被排除。

4 个 worker 节点

都被判定:

  • Insufficient memory

也就是说,没有一个 worker 的 allocatable memory 能满足 100Gi 请求。

你要立刻联想到什么

我们之前查到的实际 allocatable memory 大约是:

  • 小节点约 8Gi
  • 其他多数约 16Gi

而你现在请求的是:

  • 100Gi

所以这不是“资源用量当前不够”,而是:

任何节点的理论可分配上限都不够。

这是调度层面的硬失败。

这教你什么

很多 Pending 不是“集群坏了”,而是你的资源声明根本不现实。

所以排查 Pending 时,第一件事一定是:

  • describe
  • FailedScheduling
  • 对照节点 allocatable

不要一上来就去怀疑网络、镜像、容器运行时。


从这 3 个实验里,提炼出 Scheduler 最常见的 4 类过滤条件

1. 标签/选择器不匹配

例如:

  • nodeSelector
  • required affinity

表现为:

  • didn't match Pod's node affinity/selector

2. taint / toleration 不匹配

表现为:

  • had untolerated taint

3. 资源不足

表现为:

  • Insufficient memory
  • Insufficient cpu

4. 其他系统约束

例如:

  • PVC 未绑定
  • hostPort 冲突
  • Volume zone 约束

这些在更复杂场景里也很常见。


为什么 Pending 排查第一站永远是 kubectl describe pod

这不是经验主义,而是因为 Scheduler 已经把失败原因写进事件里了。

你这次已经看到最典型的两个例子

例子一:控制面 taint

had untolerated taint

例子二:资源不足

Insufficient memory

这比什么都直接

因为你不需要猜:

  • 是不是 CNI 问题
  • 是不是 kubelet 问题
  • 是不是容器运行时问题

如果 Pod 连调度都没完成,那问题压根还没走到这些层。

这就是分层排障的意义。


requestslimits 到底怎么影响调度与运行时

这是每个初学者必须真正吃透的一组概念。

requests

作用:

  • 参与调度
  • 表示“至少给我保留这么多资源”

调度器根据 requests 判断一个节点能不能接得下这个 Pod。

limits

作用:

  • 主要影响运行时上限

例如:

  • CPU 超 limit:被节流
  • Memory 超 limit:可能被 OOMKill

关键结论

调度器看 requests,不看 limits。

所以一个 Pod 即使 limit 设得很大,只要 request 很小,调度器仍可能认为它“放得下”。

反过来,如果 request 太夸张,哪怕程序实际根本用不到,也会调度不上。

这就是为什么资源声明必须基于真实基线,而不是乱拍。


NoSchedulePreferNoScheduleNoExecute 分别是什么

既然我们这次碰到了 NoSchedule,你就顺手把 taint effect 三兄弟也一起学掉。

NoSchedule

含义:

  • 不允许新的、不容忍该 taint 的 Pod 调度上来

这是你控制面节点现在用的模式。

PreferNoSchedule

含义:

  • 尽量别来,但不是绝对禁止

更像“软约束”。

NoExecute

含义:

  • 不仅不让新 Pod 来,已有不容忍的 Pod 也可能被驱逐

常用于节点异常时的自动驱逐逻辑。


nodeSelector 和 affinity 有什么区别

这次我们用的是 nodeSelector,因为它最直接、最适合入门。

nodeSelector

特点:

  • 简单
  • 精确匹配
  • 全是“必须满足”

Node Affinity

更强大,分两类:

  • requiredDuringSchedulingIgnoredDuringExecution
  • preferredDuringSchedulingIgnoredDuringExecution

区别在于:

  • required:不满足就不能调度
  • preferred:满足更好,但不是硬要求

所以你可以把关系记成:

nodeSelector 是最简单的硬匹配,Node Affinity 是更灵活的匹配系统。


Scheduler 日志重要吗?重要,但很多时候你先不用看

很多人一遇到 Pending 就想看 Scheduler 日志。

这不是不行,但通常不是第一步。

为什么第一步先看 describe

因为绝大多数常见调度失败,事件已经写得很清楚了。

Scheduler 日志更适合:

  • 深入排查复杂插件行为
  • 排查调度扩展器
  • 分析特殊调度策略

对于日常工作,优先级通常是:

  1. kubectl describe pod
  2. kubectl get nodes
  3. 看节点标签 / taint / allocatable
  4. 需要时再深挖 Scheduler 组件日志

你现在已经可以读懂这句经典错误了

0/5 nodes are available

很多初学者看到这里就慌。

其实你现在应该知道,这只是一个总括句,关键是后面的分解原因。

例如:

  • 1 node(s) had untolerated taint
  • 4 node(s) didn't match Pod's node affinity/selector
  • 4 Insufficient memory

这些后缀就是调度器给你的“证据报告”。

你真正要学会的不是背几个状态,而是:

把这句错误拆成“候选节点为什么一个个被筛掉”的过程。


这节课里,3 个实验对象分别在教你什么

Pod 结果 教你的点
cp-only-no-toleration Pending nodeSelector 命中不等于能调度,taint 还能拦下
cp-only-with-toleration Running toleration 让节点从“禁止”变成“允许”
huge-memory-request Pending requests 直接参与调度,资源声明不现实时会被过滤

这比抽象讲解强很多,因为它们都是你集群里真实存在的对象。


实战排障口诀:Pod Pending 怎么查

以后你遇到 Pod Pending,先按这个顺序:

  1. kubectl describe pod <name>
  2. Events 中的 FailedScheduling
  3. nodeSelector / affinity / toleration
  4. 看节点 taint
  5. 看节点 allocatable 和 Pod requests
  6. 再决定要不要去看更深层的调度器日志

如果你按这个顺序来,绝大多数调度问题都能很快缩小范围。


你现在必须能回答的 12 个问题

  1. 调度器为什么不是随机挑节点?
  2. 过滤和打分分别解决什么问题?
  3. 为什么 nodeSelector 命中了节点,Pod 仍可能 Pending?
  4. toleration 到底是“允许”还是“强制”?
  5. 为什么控制面节点默认不上普通 Pod?
  6. requestslimits 哪个参与调度?
  7. 为什么 100Gi request 会直接导致 Pending?
  8. 0/5 nodes are available 这句错误的真正重点在哪里?
  9. NoScheduleNoExecute 的区别是什么?
  10. nodeSelector 和 Node Affinity 的区别是什么?
  11. 为什么 kubectl describe pod 是 Pending 排查第一站?
  12. 什么情况下才值得去看 Scheduler 自身日志?

下一课预告

你现在已经把:

  • 主链路
  • 调度过滤

这两块最关键的骨架搭起来了。

下一步最自然的主题是:

Service、ClusterIP、kube-proxy 和 Endpoints 到底怎么把流量送到 Pod?

也就是:

  • Service 的虚拟 IP 为什么能工作
  • kube-proxy 做了什么
  • 为什么 Service 不通先看 Endpoints
  • Pod IP、Service IP、NodePort 到底是什么关系

这会把“对象世界”和“网络世界”真正连起来。


第三课总结

这节课最重要的结论可以浓缩成一句话:

Pod 调度不是随机抽签,而是先用标签、taint、资源等条件做硬过滤,再从剩余候选节点里选出更合适的目标。

而你排查 Pending 的关键,不是猜,而是学会读懂调度器已经写进 Events 的“淘汰原因清单”。