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

Repository Reading Site

本轮操作记录:调度实验与 `Pending` 排查

上一轮已经把: 这条主链路串起来了。 这一轮的目标是沿着主链路继续往下钻: 为了让这个问题不只是理论解释,我这轮做了三件事: 1. 先读取节点标签、taint、allocatable 等调度事实 2. 再在 `learn-k8s` 命名空间里创建 3 个调度实验 Pod 3. 抓取 `describe` 和 Events,形成带证据的调度结论 --- 因为调

Markdown03-操作记录-调度实验与Pending排查.md2026年4月9日 17:47

本轮操作记录:调度实验与 Pending 排查

本轮目标

上一轮已经把:

  • kubectl apply
  • API Server
  • Controller
  • Scheduler
  • kubelet
  • Service / Endpoints

这条主链路串起来了。

这一轮的目标是沿着主链路继续往下钻:

Scheduler 到底怎么选节点?为什么有的 Pod 会一直 Pending

为了让这个问题不只是理论解释,我这轮做了三件事:

  1. 先读取节点标签、taint、allocatable 等调度事实
  2. 再在 learn-k8s 命名空间里创建 3 个调度实验 Pod
  3. 抓取 describe 和 Events,形成带证据的调度结论

Step 1: 先看调度器到底拥有哪些“节点事实”

实际命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes -o jsonpath='...'
KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory,PODS:.status.allocatable.pods
KUBECONFIG=~/.kube/config-k8s-lab kubectl describe node us480851516617a | rg '^Taints:|^Roles:|^Labels:|control-plane|topology' -n
KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes -o jsonpath='...region/zone...'

为什么先看节点,而不是先创建 Pod

因为调度器不是看你的愿望,它看的是节点事实。

如果你不先知道节点有什么:

  • 标签
  • taint
  • 拓扑信息
  • 可分配资源

那你后面看到 Pending 根本没法判断为什么。

为什么这里用了 jsonpath

因为我需要精确抽出:

  • taints
  • 某些标签
  • region/zone

如果直接 kubectl get nodes --show-labels,信息量太大,不适合教学提炼。

jsonpath 的优势是:

  • 只拿你要的字段
  • 输出稳定
  • 适合做“事实摘要”

为什么用了 custom-columns

因为 allocatable 资源更适合表格看。

相比 YAML 或 JSON:

  • 可读性更强
  • 更适合肉眼对比节点容量

Step 2: 从节点输出里读到了什么

1. 控制面节点有 taint

我读到:

us480851516617a taints=node-role.kubernetes.io/control-plane:NoSchedule;

这说明:

  • 控制面节点默认不欢迎普通 Pod

这是后面第一个调度实验的核心前提。

2. 节点都有 kubernetes.io/hostname

例如:

hostname=us480851516617a
hostname=wk-1

这让我们可以不修改节点,而直接利用现成标签做 nodeSelector 实验。

3. 节点有 region/zone 标签

我读到:

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

这说明你的集群已经具备了做拓扑调度和拓扑分散实验的条件。

4. 节点 allocatable 资源明显异构

我看到:

  • hk652699382121 只有约 8Gi 内存
  • 其他多数节点约 16Gi

这非常适合做“资源请求过大导致 Pending”的实验,因为结果会很明确。


Step 3: 为什么新增 3 个调度实验 YAML

我新增了:

设计原则

我尽量只用:

  • 现成节点标签
  • 现成控制面 taint
  • 现成节点容量事实

而不去:

  • 给节点打新标签
  • 临时改 taint
  • 修改调度器配置

这么做的原因是:

我希望你学到的是“调度器如何基于真实集群事实做决策”,而不是人为造出来的演示环境。


Step 4: 一次性 apply 调度实验对象

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/03-scheduling

结果

pod/cp-only-no-toleration created
pod/cp-only-with-toleration created
pod/huge-memory-request created

为什么这里可以目录 apply

因为这 3 个对象:

  • 都属于同一个学习主题
  • 都在已有命名空间 learn-k8s
  • 没有对象间强依赖

所以非常适合统一下发。


Step 5: 先看 3 个 Pod 的总体状态

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get pods -l lesson=scheduling -o wide

为什么这条命令先看

因为它能先给全貌:

  • 哪些 Pod Running
  • 哪些 Pending
  • 谁被调度到哪个节点

实际结果

cp-only-no-toleration     Pending
cp-only-with-toleration   Running   node=us480851516617a
huge-memory-request       Pending

我从这里得到的初步结论

这符合预期:

  • 没有 toleration 的控制面定向 Pod 调度失败
  • 有 toleration 的版本成功落到控制面
  • 超大内存请求的 Pod 调度失败

接下来要做的,就是把“为什么失败/成功”用证据链确认出来。


Step 6: 抓 cp-only-no-tolerationdescribe

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s describe pod cp-only-no-toleration | tail -n 60

为什么这里看 describe 而不是 logs

因为 Pod 还没调度成功,容器根本没启动。

没启动就没有容器日志。

这就是分层排障的重要性:

  • 调度前的问题,看 Scheduler 事件
  • 启动后的问题,才看容器日志

我看到的关键证据

Status: Pending
PodScheduled: False
Node-Selectors: kubernetes.io/hostname=us480851516617a
Events:
  FailedScheduling
  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.

我得到的结论

这个 Pod 的失败原因非常清楚:

  1. 它只想去控制面节点
  2. 控制面节点有 NoSchedule taint
  3. 它没有 toleration
  4. 所以调度失败

这一步体现的原则

调度失败的第一手证据,通常已经在 describe 的 Events 里。


Step 7: 抓 cp-only-with-tolerationdescribe

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s describe pod cp-only-with-toleration | tail -n 60

为什么这一步也重要

因为教学里不能只抓失败案例,也要抓“改一个条件之后为什么成功”。

我看到的关键证据

Node: us480851516617a/10.10.0.1
Status: Running
Tolerations:
  node-role.kubernetes.io/control-plane:NoSchedule op=Exists
Events:
  Successfully assigned ... to us480851516617a
  Pulling image "busybox:1.36"
  Created container
  Started container

我得到的结论

这个 Pod 之所以成功,不是因为换了镜像,也不是因为运气好,而是因为:

  • 唯一候选节点仍然是控制面
  • 这次 Pod 具备了容忍该 taint 的资格

于是调度通过。

这一步体现的原则

toleration 解决的是“准入资格”,不是“强制绑到这个节点”。

在这个实验里它会落到控制面,是因为我们同时用 nodeSelector 把目标节点锁死了。


Step 8: 抓 huge-memory-requestdescribe

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s describe pod huge-memory-request | tail -n 60

我看到的关键证据

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

我得到的结论

这个 Pod 的问题不是镜像、不是命令、不是网络。

它根本还没进入运行阶段。

原因是:

  • 控制面节点被 taint 排除
  • 剩下 4 个 worker 节点都满足不了 100Gi 的 request

这里最重要的教学点

调度器看的不是“当前瞬时使用量”,而是:

  • allocatable
  • requests

所以这是一个声明层面的硬失败。


Step 9: 为什么还要再看整个事件流

命令

KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get events --sort-by=.metadata.creationTimestamp

为什么这一轮也看全局 Events

因为 learn-k8s 命名空间里已经同时包含:

  • 上一轮的主链路实验
  • 这一轮的调度实验

全局事件时间线能让我们看到:

  • 正常调度的对象会产生 Scheduled
  • 调度失败的对象会产生 FailedScheduling
  • 成功调度之后才会继续出现 Pulling / Created / Started

我从事件流里确认了什么

cp-only-no-toleration

只有:

  • FailedScheduling

没有:

  • Pulling
  • Created
  • Started

这说明它根本没走到节点执行阶段。

cp-only-with-toleration

有完整成功链:

  • Scheduled
  • Pulling
  • Pulled
  • Created
  • Started

huge-memory-request

只有:

  • FailedScheduling

这再次说明它卡在调度前。

这一步体现的原则

Events 的顺序,能帮助你判断故障卡在哪一层。


Step 10: 为什么这轮没有去看 Scheduler 日志

这是一个值得讲清楚的取舍。

很多人会问:

  • “既然是调度问题,为什么不直接看 kube-scheduler 日志?”

答案是:

因为当前问题已经被一线证据讲清楚了

我们已经拿到了:

  • Pod spec
  • Node taint
  • Node labels
  • Allocatable 资源
  • FailedScheduling 事件

这些足够形成闭环。

工程上要避免“过度下潜”

不是所有问题都需要看组件日志。

先从最接近用户面、最直白的证据出发,效率更高。

只有在这些证据不足以解释现象时,才值得继续往调度器日志下钻。


Step 11: 这轮留下的实验对象说明

当前 learn-k8s 命名空间里保留了这几个实验对象:

  • 上一轮的 hello-deploy / hello-svc
  • 这一轮的 3 个调度实验 Pod

为什么我暂时不删除

因为这批对象本身就是学习材料。

你后面可以直接复查:

  • kubectl get pods -n learn-k8s
  • kubectl describe pod ...
  • kubectl get events -n learn-k8s

这比只看截图或文档更有价值。

如果后续你要做一次“实验环境清理课”,我可以再统一带你做清理,并顺便讲清:

  • delete
  • 垃圾回收
  • ownerReferences
  • finalizers

Step 12: 将结果写成第三课文档

基于这一轮证据,我写入了:

这份文档重点讲清了:

  • 调度器先过滤再打分
  • nodeSelector、taint/toleration、requests` 如何参与过滤
  • 为什么 describe 是排查 Pending 的第一站
  • 为什么 requests 才是调度核心,而不是 limits

而且每一个结论都对应你这轮实验中的真实对象和真实事件。


本轮命令清单

KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes -o jsonpath='...'
KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes -o custom-columns=NAME:.metadata.name,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory,PODS:.status.allocatable.pods
KUBECONFIG=~/.kube/config-k8s-lab kubectl describe node us480851516617a | rg '^Taints:|^Roles:|^Labels:|control-plane|topology' -n
KUBECONFIG=~/.kube/config-k8s-lab kubectl get nodes -o jsonpath='...region/zone...'

KUBECONFIG=~/.kube/config-k8s-lab kubectl apply -f manifests/03-scheduling
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get pods -l lesson=scheduling -o wide
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s describe pod cp-only-no-toleration | tail -n 60
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s describe pod cp-only-with-toleration | tail -n 60
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s describe pod huge-memory-request | tail -n 60
KUBECONFIG=~/.kube/config-k8s-lab kubectl -n learn-k8s get events --sort-by=.metadata.creationTimestamp

本轮最重要的结论

结论一:Scheduler 的第一步不是选最优,而是先排除不可能

很多调度问题都死在过滤阶段。

结论二:Pending 的真正原因,通常已经写在 FailedScheduling 事件里

这让 kubectl describe pod 成为排查第一站。

结论三:requests 参与调度,limits 主要影响运行时

这点必须彻底记住。

结论四:taint/toleration 和 nodeSelector 经常一起决定最终命运

一个是允许集合,一个是目标集合。


下一步建议

继续推进时,最自然的下一个主题是网络入口这一层:

  • ClusterIP 为什么能工作
  • kube-proxy 做了什么
  • Service / Endpoints / NodePort 是什么关系
  • 为什么服务不通先看 Endpoints

这样你就会把:

  • 控制面主链路
  • 调度
  • 服务网络

三条主线连起来。