HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • 概览

    • K8s 实战学习实验室
    • 服务访问清单
    • K8s-Lab 学习总纲、仓库评估与专家路线图
  • 课程正文

    • 环境验证与第一课:认识你的真实集群
    • 第二课:kubectl apply 之后,到底发生了什么
    • 第三课:调度器如何选节点,为什么 Pod 会 Pending
    • 第四课:Kubernetes 网络、协议分层、VXLAN/IPIP/WireGuard 原理与排障
    • 第五课:NetworkPolicy、零信任网络与流量边界
    • 第六课:身份、认证、授权、准入与 ServiceAccount / RBAC 原理
    • 第七课:ConfigMap 与 Secret 注入模型、更新机制与安全边界
    • 第八课:存储持久化、PV / PVC / StorageClass 与 NFS 原理
    • 第九课:StatefulSet、Headless Service、稳定身份与存储原理
    • 第十课:探针、滚动更新、优雅终止与 PDB 原理
    • 第十一课:requests / limits、QoS、OOM 与驱逐原理
    • 第十二课:HPA、自动扩缩容、指标链路与副本伸缩原理
    • 第十三课:Service、EndpointSlice、kube-proxy、CoreDNS 与服务发现原理
    • 第十四课:Ingress-nginx、反向代理、Host / Path、NodePort 与北南向流量原理
    • 第十五课:HTTPS、TLS、SNI、证书信任与 Ingress 终止原理
    • 第十六课:cert-manager、Ingress 自动签发、证书生命周期与 ACME 工作流原理
    • 第十七课:ACME、Let's Encrypt、HTTP-01 / DNS-01、Orders / Challenges 与生产限制原理
    • 第十八课:大模型全生态,从数据到训练到部署到治理原理
    • 第十九课:大模型数据集、清洗、标注、切分、版本管理与质量治理原理
    • 第二十课:大模型训练、SFT、LoRA、Checkpoint、Adapter 与模型产物原理
    • 第二十一课:大模型推理、量化、KV Cache、vLLM、吞吐/延迟与部署发布链路原理
  • 实验操作记录

    • 本次仓库审查操作记录与命令原理
    • 本轮操作记录:环境验证、集群基线盘点与故障样本采集
    • 本轮操作记录:kubectl apply 主链路实验
    • 本轮操作记录:调度实验与 Pending 排查
    • 本轮操作记录:Kubernetes 网络原理、协议对比与调试实验
    • 本轮操作记录:NetworkPolicy 与零信任网络实验
    • 本轮操作记录:身份、认证、授权、准入实验
    • 本轮操作记录:ConfigMap 与 Secret 注入、更新与安全边界实验
    • 本轮操作记录:存储持久化、PV / PVC / StorageClass 与 NFS 实验
    • 本轮操作记录:StatefulSet、Headless Service 与稳定身份实验
    • 本轮操作记录:探针、滚动更新、优雅终止与 PDB 实验
    • 本轮操作记录:资源模型、QoS、OOM 与 CPU 节流实验
    • 本轮操作记录:HPA 自动扩缩容实验
    • 本轮操作记录:Service、EndpointSlice、CoreDNS 与服务发现排障实验
    • 本轮操作记录:Ingress-nginx、NodePort 与北南向流量实验
    • 本轮操作记录:HTTPS、TLS、自签证书与 Ingress 实验
    • 本轮操作记录:cert-manager 安装、CA 签发与 Ingress 自动证书实验
    • 本轮操作记录:ACME staging、HTTP-01 失败样本与排障实验
    • 本轮操作记录:大模型全生态与基础原理科普文撰写
    • 本轮操作记录:大模型数据集样本与治理文档编写
    • 本轮操作记录:大模型训练与模型产物概念文撰写
    • 本轮操作记录:大模型推理与服务发布概念文撰写

第三课:调度器如何选节点,为什么 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 个调度实验

实验文件在仓库里:

  • control-plane-no-toleration.yaml
  • control-plane-with-toleration.yaml
  • too-much-memory-pod.yaml

实验对象都放在:

  • 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. 你的节点还有拓扑标签

我们实际看到:

Noderegionzone
us480851516617alala-1
us590068728056lala-1
cp-3lala-2
hk652699382121hkhk-1
wk-1hkhk-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 连调度都没完成,那问题压根还没走到这些层。

这就是分层排障的意义。


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

更强大,分两类:

  • 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-tolerationPendingnodeSelector 命中不等于能调度,taint 还能拦下
cp-only-with-tolerationRunningtoleration 让节点从“禁止”变成“允许”
huge-memory-requestPendingrequests 直接参与调度,资源声明不现实时会被过滤

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


实战排障口诀: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. requests 和 limits 哪个参与调度?
  7. 为什么 100Gi request 会直接导致 Pending?
  8. 0/5 nodes are available 这句错误的真正重点在哪里?
  9. NoSchedule 和 NoExecute 的区别是什么?
  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 的“淘汰原因清单”。

Prev
第二课:kubectl apply 之后,到底发生了什么
Next
第四课:Kubernetes 网络、协议分层、VXLAN/IPIP/WireGuard 原理与排障