Repository Reading Site
第三课:调度器如何选节点,为什么 Pod 会 Pending
上一课你已经看到: 但这条链路里有一个环节最容易被“跳过去”: 如果这一步你不理解,后面遇到: 你就只能靠试错,而不是靠判断。 所以这一课的目标是: 1. 讲清 Scheduler 的基本工作方式 2. 讲清常见过滤条件 3. 用你真实集群里的 3 个实验样本,讲清 `FailedScheduling` 的阅读方法 --- Scheduler 的核心流程,可
第三课:调度器如何选节点,为什么 Pod 会 Pending
为什么这一课必须紧跟在 kubectl apply 主链路之后
上一课你已经看到:
- Deployment 变成 ReplicaSet
- ReplicaSet 变成 Pod
- Scheduler 给 Pod 选节点
- kubelet 在目标节点上启动容器
但这条链路里有一个环节最容易被“跳过去”:
Scheduler 到底是怎么决定一个 Pod 能不能上某个节点、应该上哪个节点的?
如果这一步你不理解,后面遇到:
- Pod 一直
Pending - 有的 Pod 总跑到错误节点
- 控制面节点为什么默认不上业务 Pod
- 为什么资源明明还有空闲,Pod 却调度不上
你就只能靠试错,而不是靠判断。
所以这一课的目标是:
- 讲清 Scheduler 的基本工作方式
- 讲清常见过滤条件
- 用你真实集群里的 3 个实验样本,讲清
FailedScheduling的阅读方法
先给结论:调度不是“随机分配”,而是“先过滤再打分”
Scheduler 的核心流程,可以先记成两步:
- 过滤 Filtering
- 打分 Scoring
第一步:过滤
先把“不可能”的节点排除掉。
常见过滤条件包括:
- 节点资源不够
- Pod 的
nodeSelector不匹配 - Pod 的 affinity 不满足
- 节点有 taint,Pod 没有 toleration
- 卷亲和或端口冲突
第二步:打分
在剩下“可以放”的节点里,选一个更优的。
评分会考虑:
- 资源均衡
- 亲和性偏好
- 拓扑分散
- 其他调度插件策略
你必须记住的一句话
调度不上,优先看过滤原因;调度到哪里,才去看打分结果。
因为如果第一步都没过,第二步根本不会发生。
我们这次不空讲,直接拿你的真实集群做 3 个调度实验
实验文件在仓库里:
实验对象都放在:
- Namespace:
learn-k8s
这 3 个实验分别教你 3 种典型调度判断:
- 节点选择命中了,但被 taint 挡住
- 加上 toleration 后,调度成功
- 没有节点资源够用,因
requests过大而 Pending
先认识你集群里调度器会用到的“节点事实”
调度器不是凭感觉做决定,它看的都是 Node 对象里的事实。
我们实际看到的关键事实包括:
1. 控制面节点有 taint
你的控制面节点 us480851516617a 有:
node-role.kubernetes.io/control-plane:NoSchedule
这是什么意思?
key = node-role.kubernetes.io/control-planeeffect = NoSchedule
意思是:
默认情况下,不允许没有对应 toleration 的 Pod 调度到这台节点。
这是一种“保护控制面”的机制。
2. 节点都有内置标签
例如:
kubernetes.io/hostnamekubernetes.io/oskubernetes.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 都不匹配。
所以它们在过滤阶段就出局了。
这说明了什么
这是一条非常漂亮的调度证据链:
nodeSelector把候选范围缩小到 1 个节点- 那个节点又被 taint 过滤掉
- 最终 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
为什么这次成功
因为现在它同时满足了两件事:
nodeSelector命中了控制面节点- toleration 允许它接受这个节点的
NoScheduletaint
于是过滤阶段通过,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 memoryInsufficient cpu
4. 其他系统约束
例如:
- PVC 未绑定
- hostPort 冲突
- Volume zone 约束
这些在更复杂场景里也很常见。
为什么 Pending 排查第一站永远是 kubectl describe pod
这不是经验主义,而是因为 Scheduler 已经把失败原因写进事件里了。
你这次已经看到最典型的两个例子
例子一:控制面 taint
had untolerated taint
例子二:资源不足
Insufficient memory
这比什么都直接
因为你不需要猜:
- 是不是 CNI 问题
- 是不是 kubelet 问题
- 是不是容器运行时问题
如果 Pod 连调度都没完成,那问题压根还没走到这些层。
这就是分层排障的意义。
requests 和 limits 到底怎么影响调度与运行时
这是每个初学者必须真正吃透的一组概念。
requests
作用:
- 参与调度
- 表示“至少给我保留这么多资源”
调度器根据 requests 判断一个节点能不能接得下这个 Pod。
limits
作用:
- 主要影响运行时上限
例如:
- CPU 超 limit:被节流
- Memory 超 limit:可能被 OOMKill
关键结论
调度器看 requests,不看 limits。
所以一个 Pod 即使 limit 设得很大,只要 request 很小,调度器仍可能认为它“放得下”。
反过来,如果 request 太夸张,哪怕程序实际根本用不到,也会调度不上。
这就是为什么资源声明必须基于真实基线,而不是乱拍。
NoSchedule、PreferNoSchedule、NoExecute 分别是什么
既然我们这次碰到了 NoSchedule,你就顺手把 taint effect 三兄弟也一起学掉。
NoSchedule
含义:
- 不允许新的、不容忍该 taint 的 Pod 调度上来
这是你控制面节点现在用的模式。
PreferNoSchedule
含义:
- 尽量别来,但不是绝对禁止
更像“软约束”。
NoExecute
含义:
- 不仅不让新 Pod 来,已有不容忍的 Pod 也可能被驱逐
常用于节点异常时的自动驱逐逻辑。
nodeSelector 和 affinity 有什么区别
这次我们用的是 nodeSelector,因为它最直接、最适合入门。
nodeSelector
特点:
- 简单
- 精确匹配
- 全是“必须满足”
Node Affinity
更强大,分两类:
requiredDuringSchedulingIgnoredDuringExecutionpreferredDuringSchedulingIgnoredDuringExecution
区别在于:
required:不满足就不能调度preferred:满足更好,但不是硬要求
所以你可以把关系记成:
nodeSelector是最简单的硬匹配,Node Affinity 是更灵活的匹配系统。
Scheduler 日志重要吗?重要,但很多时候你先不用看
很多人一遇到 Pending 就想看 Scheduler 日志。
这不是不行,但通常不是第一步。
为什么第一步先看 describe
因为绝大多数常见调度失败,事件已经写得很清楚了。
Scheduler 日志更适合:
- 深入排查复杂插件行为
- 排查调度扩展器
- 分析特殊调度策略
对于日常工作,优先级通常是:
kubectl describe podkubectl get nodes- 看节点标签 / taint / allocatable
- 需要时再深挖 Scheduler 组件日志
你现在已经可以读懂这句经典错误了
0/5 nodes are available
很多初学者看到这里就慌。
其实你现在应该知道,这只是一个总括句,关键是后面的分解原因。
例如:
1 node(s) had untolerated taint4 node(s) didn't match Pod's node affinity/selector4 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,先按这个顺序:
kubectl describe pod <name>- 看
Events中的FailedScheduling - 看
nodeSelector/ affinity / toleration - 看节点 taint
- 看节点 allocatable 和 Pod requests
- 再决定要不要去看更深层的调度器日志
如果你按这个顺序来,绝大多数调度问题都能很快缩小范围。
你现在必须能回答的 12 个问题
- 调度器为什么不是随机挑节点?
- 过滤和打分分别解决什么问题?
- 为什么
nodeSelector命中了节点,Pod 仍可能 Pending? - toleration 到底是“允许”还是“强制”?
- 为什么控制面节点默认不上普通 Pod?
requests和limits哪个参与调度?- 为什么
100Girequest 会直接导致 Pending? 0/5 nodes are available这句错误的真正重点在哪里?NoSchedule和NoExecute的区别是什么?nodeSelector和 Node Affinity 的区别是什么?- 为什么
kubectl describe pod是 Pending 排查第一站? - 什么情况下才值得去看 Scheduler 自身日志?
下一课预告
你现在已经把:
- 主链路
- 调度过滤
这两块最关键的骨架搭起来了。
下一步最自然的主题是:
Service、ClusterIP、kube-proxy 和 Endpoints 到底怎么把流量送到 Pod?
也就是:
- Service 的虚拟 IP 为什么能工作
- kube-proxy 做了什么
- 为什么 Service 不通先看 Endpoints
- Pod IP、Service IP、NodePort 到底是什么关系
这会把“对象世界”和“网络世界”真正连起来。
第三课总结
这节课最重要的结论可以浓缩成一句话:
Pod 调度不是随机抽签,而是先用标签、taint、资源等条件做硬过滤,再从剩余候选节点里选出更合适的目标。
而你排查 Pending 的关键,不是猜,而是学会读懂调度器已经写进 Events 的“淘汰原因清单”。