Repository Reading Site
本轮操作记录:调度实验与 `Pending` 排查
上一轮已经把: 这条主链路串起来了。 这一轮的目标是沿着主链路继续往下钻: 为了让这个问题不只是理论解释,我这轮做了三件事: 1. 先读取节点标签、taint、allocatable 等调度事实 2. 再在 `learn-k8s` 命名空间里创建 3 个调度实验 Pod 3. 抓取 `describe` 和 Events,形成带证据的调度结论 --- 因为调
本轮操作记录:调度实验与 Pending 排查
本轮目标
上一轮已经把:
kubectl apply- API Server
- Controller
- Scheduler
- kubelet
- Service / Endpoints
这条主链路串起来了。
这一轮的目标是沿着主链路继续往下钻:
Scheduler 到底怎么选节点?为什么有的 Pod 会一直
Pending?
为了让这个问题不只是理论解释,我这轮做了三件事:
- 先读取节点标签、taint、allocatable 等调度事实
- 再在
learn-k8s命名空间里创建 3 个调度实验 Pod - 抓取
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-toleration 的 describe
命令
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 的失败原因非常清楚:
- 它只想去控制面节点
- 控制面节点有
NoScheduletaint - 它没有 toleration
- 所以调度失败
这一步体现的原则
调度失败的第一手证据,通常已经在
describe的 Events 里。
Step 7: 抓 cp-only-with-toleration 的 describe
命令
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-request 的 describe
命令
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
有完整成功链:
ScheduledPullingPulledCreatedStarted
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-k8skubectl 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
这样你就会把:
- 控制面主链路
- 调度
- 服务网络
三条主线连起来。