Kubernetes生产运维03:Pod一直Pending怎么办,沿着调度器决策链系统排查
Kubernetes生产运维03:Pod一直Pending怎么办,沿着调度器决策链系统排查

写在前面
生产环境里看到Pod Pending,很多人的第一反应是:节点资源不够,先扩容。
这个判断有时正确,但远远不够。下面这些现象都可能被笼统地描述成Pod起不来:
- ReplicaSet因为ResourceQuota拒绝,实际上连Pod对象都没有创建出来
- Pod带有
schedulingGates,调度器还没有开始处理 - Pod指定了不存在的
schedulerName,默认调度器不会接管 - 调度器已经尝试过,但所有节点分别被资源、污点或亲和性过滤
- PVC与节点拓扑不兼容,VolumeBinding无法完成
- Pod已经绑定Node,只是还在拉镜像、创建Sandbox或执行Init Container
因此,Pending排查不能从原因清单开始,而要先确定对象处于哪一个阶段:
工作负载期望副本不足
→ Pod对象是否已经创建
→ Pod是否进入调度队列
→ 是否已经绑定Node
→ 哪些调度约束淘汰了候选节点
→ 绑定后为什么容器仍未启动
本文按照Kubernetes官方机制和命令参考整理。由于当前没有连接可验证的实验集群,命令没有在统一版本的真实集群完整执行,案例和终端输出均为C级模拟说明,不是生产原始记录。不同Kubernetes版本、调度器配置、存储插件、云厂商和准入策略可能改变字段、Events和行为,执行前应以目标集群的kubectl explain、命令帮助和组件配置为准。

一、第一步不是扩容,而是确认故障阶段
1.1 Pending不等于一定未调度
Kubernetes的Pod phase中,Pending表示Pod已经被集群接受,但一个或多个容器尚未准备好运行。它既可能包含等待调度的阶段,也可能包含调度完成后下载镜像、创建容器等时间。
先检查三个字段:
NS='<namespace>'
POD='<pod-name>'
kubectl get pod "$POD" -n "$NS" \
-o custom-columns='NAME:.metadata.name,PHASE:.status.phase,NODE:.spec.nodeName,SCHEDULED:.status.conditions[?(@.type=="PodScheduled")].status,REASON:.status.conditions[?(@.type=="PodScheduled")].reason'
判断方法:
| 观察结果 | 当前阶段 | 下一步 |
|---|---|---|
| Pod对象不存在 | 可能未创建、已删除或查错Namespace | 查Deployment、StatefulSet、Job和其控制器Events |
spec.nodeName为空,PodScheduled=False且Reason为Unschedulable | 调度器尝试过但没有合适节点 | 解析FailedScheduling事件和约束 |
spec.nodeName为空,存在schedulingGates | 尚未进入正常调度尝试 | 查哪个控制器负责移除Gate |
spec.nodeName有值,PodScheduled=True | 已完成调度绑定 | 转向镜像、Sandbox、Init Container、挂载和kubelet |
| phase为Pending但没有清晰Condition | 证据还不够 | 查YAML、Events、schedulerName和调度器状态 |
也可以直接提取关键字段:
kubectl get pod "$POD" -n "$NS" -o jsonpath='{.status.phase}{"\n"}{.spec.nodeName}{"\n"}{range .status.conditions[?(@.type=="PodScheduled")]}{.status}{"\t"}{.reason}{"\t"}{.message}{"\n"}{end}'
这里的停止条件很重要:一旦nodeName已经有值,就不要继续把所有时间花在调度器过滤规则上。此时真正的问题已经转到节点侧启动链路,后续文章会分别展开镜像、存储与节点故障。
1.2 Pod不存在时,先查控制器而不是查Pod
用户看到的往往是Deployment只有2/3副本,不一定真的存在第三个Pending Pod。
kubectl get deploy,rs,pod -n "$NS" -o wide
kubectl describe deployment '<deployment-name>' -n "$NS"
kubectl describe replicaset '<replicaset-name>' -n "$NS"
kubectl get events -n "$NS" \
--field-selector involvedObject.kind=ReplicaSet \
--sort-by=.metadata.creationTimestamp
如果ReplicaSet事件出现FailedCreate,应检查准入阶段:
kubectl get resourcequota,limitrange -n "$NS"
kubectl describe resourcequota -n "$NS"
kubectl describe limitrange -n "$NS"
kubectl auth can-i create pods -n "$NS"
ResourceQuota和LimitRange通常在API准入阶段检查新Pod。违反配额或范围时,Pod创建请求会被拒绝,控制器记录FailedCreate,而不是先创建一个Pending Pod等待资源释放。
这一区分能避免一个常见错误:在根本不存在的Pod上反复执行describe pod,同时遗漏ReplicaSet已经明确给出的拒绝原因。
1.3 先保存首轮现场
Events可能被聚合和过期,工作负载也可能被流水线继续修改。开始处理前先保存只读证据:
OUT="pending-${NS}-${POD}-$(date +%Y%m%d-%H%M%S)"
mkdir -p "$OUT"
umask 077
kubectl get pod "$POD" -n "$NS" -o yaml >"$OUT/pod.yaml"
kubectl describe pod "$POD" -n "$NS" >"$OUT/pod-describe.txt"
kubectl get events -n "$NS" \
--field-selector "involvedObject.kind=Pod,involvedObject.name=$POD" \
--sort-by=.metadata.creationTimestamp >"$OUT/pod-events.txt"
kubectl get nodes -o wide >"$OUT/nodes-wide.txt"
这些命令不改变工作负载,但输出可能包含内部地址、镜像、标签、配置引用和故障信息,分享前仍需脱敏。
二、Pending排查决策树
下面的决策树按处理顺序组织,不是把所有命令执行一遍:
期望副本不足
│
├─ 没有对应Pod对象
│ └─ 查控制器FailedCreate
│ ├─ ResourceQuota / LimitRange
│ ├─ RBAC / Admission Webhook / Policy
│ └─ 控制器自身异常
│
└─ Pod对象存在且phase=Pending
│
├─ spec.nodeName已有值
│ └─ 已调度,查镜像、Sandbox、Init Container、Volume Mount和kubelet
│
└─ spec.nodeName为空
│
├─ schedulingGates非空
│ └─ 查Gate所有者和移除条件
│
├─ schedulerName异常
│ └─ 查对应调度器及Profile
│
└─ PodScheduled=False / Unschedulable
├─ requests与Node Allocatable
├─ taints与tolerations
├─ nodeSelector与nodeAffinity
├─ podAffinity与podAntiAffinity
├─ topologySpreadConstraints
├─ PVC、PV、StorageClass和存储拓扑
└─ Priority、preemption与调度器插件
最有价值的第一条证据通常是最新FailedScheduling事件:
kubectl describe pod "$POD" -n "$NS"
kubectl get events -n "$NS" \
--field-selector "involvedObject.kind=Pod,involvedObject.name=$POD" \
--sort-by=.metadata.creationTimestamp
说明性示例:
Warning FailedScheduling 0/6 nodes are available:
2 Insufficient cpu,
2 node(s) had untolerated taint {dedicated: platform},
2 node(s) didn't match Pod's node affinity/selector.
preemption: 0/6 nodes are available: preemption is not helpful for scheduling.
这段信息不能简单归纳成CPU不足。它表示不同候选节点可能被不同条件过滤,必须找出哪些节点原本应该承载该工作负载,以及这些节点具体在哪一道过滤条件失败。
Events是组件在某个时间点给出的观察,不是永久保存的完整审计记录。没有事件不等于没有发生过调度失败,事件文本也可能随版本和插件而变化。

三、先排除调度还没真正开始
3.1 Scheduling Gates
Pod Scheduling Readiness允许Pod创建时携带schedulingGates。只要列表非空,Pod不会进入正常调度尝试。
kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{range .spec.schedulingGates[*]}{.name}{"\n"}{end}'
如果输出Gate名称,应回答三个问题:
- 是哪个控制器或准入Webhook添加的?
- 什么条件满足后应该移除?
- 负责移除Gate的控制器是否运行正常、是否有权限更新Pod?
Gate通常只能在创建时添加,创建后由相关组件逐项移除。直接删除并重建Pod不是默认解决方案,如果外部条件或控制器故障没有修复,新Pod仍可能再次被Gate阻塞。
3.2 schedulerName是否存在
kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{.spec.schedulerName}{"\n"}'
默认通常是default-scheduler。如果工作负载指定了自定义名称:
kubectl get deploy,statefulset,daemonset -A \
-o custom-columns='NAMESPACE:.metadata.namespace,KIND:.kind,NAME:.metadata.name,SCHEDULER:.spec.template.spec.schedulerName'
然后确认对应调度器是否部署、健康并监控这个名称。指定一个不存在的schedulerName时,默认调度器不会自动接管。
调度器还可能配置多个Profile。Profile中的addedAffinity可以给一组Pod隐式增加NodeAffinity,这些约束不直接出现在Pod YAML里。如果Pod本身看不出亲和性问题,但事件持续提示NodeAffinity不匹配,就需要由集群管理员检查KubeSchedulerConfiguration,而不是只盯着业务清单。
3.3 调度器本身是否健康
这一步只在多个不相关工作负载同时无法调度、Events不再更新或自定义调度器异常时优先检查。
托管集群可能无法直接访问控制面组件,应使用云厂商控制面日志和健康接口。自建集群可按部署方式检查调度器Pod、日志和指标。不要因为单个Pod Unschedulable就先重启kube-scheduler,调度器正常拒绝不满足约束的Pod本身就是正确行为。
四、资源不足:调度器看的是requests账本,不是实时usage
4.1 先看Pod申请了什么
kubectl get pod "$POD" -n "$NS" \
-o custom-columns='NAME:.metadata.name,CPU:.spec.containers[*].resources.requests.cpu,MEMORY:.spec.containers[*].resources.requests.memory,EPHEMERAL:.spec.containers[*].resources.requests.ephemeral-storage'
kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{range .spec.containers[*]}{.name}{"\t"}{.resources.requests.cpu}{"\t"}{.resources.requests.memory}{"\t"}{.resources.requests.ephemeral-storage}{"\n"}{end}'
不要只看主容器。Sidecar也参与Pod requests计算,GPU等扩展资源同样会参与调度:
kubectl get pod "$POD" -n "$NS" -o yaml
重点查看:
- 所有普通容器的CPU、内存、临时存储requests
- Init Container的requests
- GPU、HugePages等扩展资源
- RuntimeClass与Pod Overhead
- Pod数量上限和HostPort等其他调度约束
4.2 Init Container不是简单全部相加
调度时,普通应用容器的requests按总和计算。Init Container顺序执行,资源计算还会考虑单个Init Container的有效峰值,再与应用容器有效总需求比较。可重启Sidecar形式的Init Container会使计算进一步复杂,应以目标版本的资源管理文档和Pod状态为准。
因此,看到主容器只申请500m CPU,不能直接断言该Pod只需要500m。一个高request的Init Container、多个Sidecar或RuntimeClass Overhead都可能提高调度器看到的有效需求。
Pod Overhead来自RuntimeClass:
RUNTIME_CLASS=$(kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{.spec.runtimeClassName}')
if [[ -n "$RUNTIME_CLASS" ]]; then
kubectl get runtimeclass "$RUNTIME_CLASS" -o yaml
fi
该检查是只读的。当前身份可能没有读取RuntimeClass的集群级权限,此时应记录证据缺口,而不是把空输出解释成没有Overhead。
4.3 再看Node可以分配多少,以及已经承诺多少
kubectl get nodes \
-o custom-columns='NAME:.metadata.name,CPU_ALLOCATABLE:.status.allocatable.cpu,MEM_ALLOCATABLE:.status.allocatable.memory,PODS_ALLOCATABLE:.status.allocatable.pods,GPU_ALLOCATABLE:.status.allocatable.nvidia\.com/gpu'
kubectl describe node '<node-name>'
kubectl describe node底部的Allocated resources适合快速查看该节点requests和limits汇总。需要精确审计时,应基于API对象结构化计算节点上所有Pod的有效requests,不能依赖截断或人眼估算。
调度器主要根据Node Allocatable和已经调度Pod的requests判断候选节点是否还能承载新Pod,不会用Metrics Server的实时CPU、内存利用率替代这个账本。
因此,下面两种情况都可能出现:
| 现象 | 原因 |
|---|---|
kubectl top node只有30%,新Pod仍提示Insufficient cpu | 已有Pod的CPU requests已经承诺了大部分Allocatable |
kubectl top node接近90%,新Pod仍可能被调度 | 已有Pod实际使用高,但requests账本仍有空间 |
kubectl top可以帮助理解运行压力,但不能证明调度容量充足,也不能替代历史监控和requests审计。
4.4 正常与异常怎么判断
资源分支至少要形成下面的证据链:
FailedScheduling提示具体资源不足
+ Pod有效request明确
+ 目标节点Allocatable明确
+ 已调度Pod requests账本明确
+ 其他约束没有先把目标节点全部过滤
= 才能支持容量不足或requests不合理的判断
如果只是top很高或很低,不足以确认根因。
4.5 资源不足的处理边界
可选处理不是只有扩容:
- requests明显高于长期基线:通过容量评审后修正requests
- 集群真实容量不足:扩容Node Pool或等待Cluster Autoscaler
- 工作负载可延迟:保持Pending并明确SLO与超时边界
- 非关键任务占用资源:评估优先级与抢占策略
- 发布需要新旧副本并存:检查RollingUpdate的
maxSurge与峰值容量
降低requests会影响调度密度和节点过载风险;扩容涉及成本、配额和启动时间;强制删除其他Pod会扩大故障。所有变更都应先检查副本、PDB、优先级、容量和回退方式。
五、污点与容忍:有资源不代表允许进入
5.1 对比Node taints和Pod tolerations
kubectl get nodes \
-o custom-columns='NAME:.metadata.name,TAINTS:.spec.taints'
kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{range .spec.tolerations[*]}{.key}{"\t"}{.operator}{"\t"}{.value}{"\t"}{.effect}{"\t"}{.tolerationSeconds}{"\n"}{end}'
三类常见effect:
| Effect | 对新Pod调度 | 对已运行Pod |
|---|---|---|
NoSchedule | 没有匹配容忍时禁止调度 | 不主动驱逐 |
PreferNoSchedule | 尽量避免,属于软约束 | 不主动驱逐 |
NoExecute | 没有匹配容忍时禁止调度 | 还可能驱逐已有Pod |
Toleration只表示Pod可以容忍污点,不代表一定会被吸引到该Node。要把专用工作负载限制到专用节点,通常还要配合标签与NodeAffinity。
5.2 不要为了恢复直接删除污点
下面的动作会改变整个节点对新工作负载的准入边界:
kubectl taint nodes '<node-name>' dedicated=platform:NoSchedule-
它不应作为默认排障命令。执行前必须确认:
- 污点是否用于GPU、控制面、安全域或关键业务隔离
- 移除后哪些Pending Pod可能同时涌入
- 节点容量和DaemonSet是否允许
- 谁负责恢复污点,如何确认原值
更安全的顺序是先确认工作负载是否本来就应该进入该节点池。如果应该进入,优先在版本化清单中增加精确Toleration并走发布;如果不应该进入,则修正亲和性或扩容正确节点池。
六、nodeSelector与NodeAffinity:标签拼写也能让全体节点失配
6.1 同时读取Pod约束和Node标签
kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{.spec.nodeSelector}{"\n"}{.spec.affinity.nodeAffinity}{"\n"}'
kubectl get nodes --show-labels
生产集群标签很多,可以针对实际key查看:
kubectl get nodes -L '<label-key-1>,<label-key-2>'
判断时区分:
nodeSelector:所有键值都必须满足requiredDuringSchedulingIgnoredDuringExecution:硬约束,不满足就不能调度preferredDuringSchedulingIgnoredDuringExecution:软偏好,影响打分但不保证满足
required中的多个nodeSelectorTerms通常是OR关系;一个term里的多个matchExpressions是AND关系。排查时必须按原结构展开,不能把整段YAML凭印象读成全部AND。
6.2 使用正常Pod做对照
kubectl get pods -n "$NS" -l 'app=<app-label>' -o wide
kubectl get pod '<normal-pod>' -n "$NS" -o yaml >normal-pod.yaml
kubectl get pod "$POD" -n "$NS" -o yaml >pending-pod.yaml
diff -u normal-pod.yaml pending-pod.yaml
完整Pod YAML包含大量动态字段,真正对比时应重点收敛到:
- Controller revision和镜像版本
- nodeSelector、affinity、tolerations
- resources和RuntimeClass
- volumes与PVC
- schedulerName、priorityClassName和schedulingGates
不要直接修改运行中的Pod调度字段做试验。多数调度字段不可变或受限制,应修改Deployment、StatefulSet、Job或GitOps源清单,并通过新的Pod验证。
6.3 Pod Affinity与Anti-Affinity
Pod间亲和性依赖其他Pod标签和Node拓扑标签。检查:
kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{.spec.affinity.podAffinity}{"\n"}{.spec.affinity.podAntiAffinity}{"\n"}'
kubectl get pods -A --show-labels -o wide
常见问题包括:
- labelSelector写错,找不到作为参照的Pod
- topologyKey对应的Node标签缺失
- 硬性PodAntiAffinity要求每个拓扑域最多一个副本,但可用域不足
- Namespace范围与预期不一致
跨Namespace查看可能包含敏感工作负载信息,应遵循最小权限。数据量大时按目标标签和Namespace缩小范围。
七、拓扑分布约束:maxSkew不是一句均匀分布就能解释
读取配置:
kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{range .spec.topologySpreadConstraints[*]}{.topologyKey}{"\t"}{.maxSkew}{"\t"}{.whenUnsatisfiable}{"\t"}{.minDomains}{"\n"}{end}'
关键字段:
| 字段 | 作用 |
|---|---|
topologyKey | 用哪个Node标签定义拓扑域,例如zone或hostname |
labelSelector、matchLabelKeys | 哪些Pod参与计数,具体能力受版本影响 |
maxSkew | 允许的分布偏斜上限,计算与合格拓扑域有关 |
whenUnsatisfiable: DoNotSchedule | 硬约束,无法满足时不调度 |
whenUnsatisfiable: ScheduleAnyway | 软约束,倾向减少偏斜但不阻止调度 |
minDomains | 参与计算所需的最小合格域数量,适用条件受版本约束 |
排查步骤:
- 列出所有Node的
topologyKey标签。 - 先应用NodeAffinity、NodeSelector和污点等条件,确认哪些拓扑域实际合格。
- 按
labelSelector确认哪些已有Pod参与计数。 - 根据目标版本规则计算新增Pod后是否超过
maxSkew。 - 检查缺失拓扑标签的Node是否被排除。
kubectl get nodes -L topology.kubernetes.io/zone,kubernetes.io/hostname
kubectl get pods -n "$NS" -l '<selector>' -o wide
不能只看集群总共有几个可用区。一个可用区有Node,不代表这些Node同时满足该Pod的资源、污点、亲和性和存储条件。
八、PVC与VolumeBinding:计算资源够了,存储拓扑仍可能阻塞
8.1 找出Pod引用的PVC
kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{range .spec.volumes[?(@.persistentVolumeClaim)]}{.name}{"\t"}{.persistentVolumeClaim.claimName}{"\n"}{end}'
kubectl get pvc -n "$NS"
kubectl describe pvc '<pvc-name>' -n "$NS"
继续检查PV和StorageClass:
kubectl get pvc '<pvc-name>' -n "$NS" \
-o jsonpath='{.status.phase}{"\t"}{.spec.volumeName}{"\t"}{.spec.storageClassName}{"\n"}'
kubectl get pv '<pv-name>' -o yaml
kubectl get storageclass '<storage-class-name>' -o yaml
重点关注:
- PVC是否Bound
- StorageClass provisioner是否正常
volumeBindingMode是Immediate还是WaitForFirstConsumer- PV的NodeAffinity与Pod候选Node是否冲突
- AccessModes、容量和VolumeMode是否匹配
- CSI控制器、云盘配额和可用区容量是否正常
8.2 两种绑定模式的区别
Immediate通常在PVC创建后立即完成绑定或制备。在多可用区环境中,如果卷先落到某个拓扑域,Pod的其他约束又要求另一个域,可能发生冲突。
WaitForFirstConsumer会推迟绑定或制备,让系统结合Pod的调度约束选择存储拓扑。它不是保证一定成功,如果目标域没有容量、CSI异常或其他硬约束冲突,Pod仍会Pending。
不要为了让Pod启动而随意删除PVC。PVC删除可能触发PV回收策略并造成数据丢失,属于高风险操作。任何重建都必须先确认数据所有权、备份、ReclaimPolicy、快照和回退方案。
九、优先级与抢占:高优先级不等于一定能调度
kubectl get pod "$POD" -n "$NS" \
-o custom-columns='NAME:.metadata.name,PRIORITY_CLASS:.spec.priorityClassName,PRIORITY:.spec.priority,PREEMPTION_POLICY:.spec.preemptionPolicy,NOMINATED_NODE:.status.nominatedNodeName'
kubectl get priorityclass
当高优先级Pod无法调度时,调度器可能尝试抢占低优先级Pod。nominatedNodeName表示被提名的候选节点,不是已经完成绑定的保证。被抢占Pod的优雅终止、PDB、资源形状和其他约束都可能让高优先级Pod继续等待。
事件提示preemption is not helpful通常意味着即使移除较低优先级Pod,也无法消除当前约束。例如:
- 亲和性标签不匹配
- 存储拓扑冲突
- 不可容忍污点
- 单节点资源上限仍放不下Pod
- 没有合适的低优先级牺牲者
不要把创建超高PriorityClass当成常规容量方案。错误的优先级设计可能驱逐关键服务并形成级联故障。应先做业务分级、PDB评审、抢占演练和容量验证。
十、从FailedScheduling文本映射到下一步
事件文本会随Kubernetes版本和插件变化,下面是排障映射,不是稳定API契约:
| 常见事件线索 | 当前假设 | 立即检查 | 不能直接得出的结论 |
|---|---|---|---|
Insufficient cpu/memory | requests账本放不下 | Pod有效requests、Node Allocatable、已承诺requests | 不能用当前top证明节点绝对没资源 |
Too many pods | Node Pod容量达到上限 | allocatable pods、CNI地址和节点Pod数量 | 不一定是CPU或内存不足 |
untolerated taint | Pod不被目标节点池接纳 | Node taints、Pod tolerations、节点池用途 | 不应直接删除污点 |
didn't match node affinity/selector | 硬标签约束不满足 | Pod约束、Node标签、调度器addedAffinity | 不一定是节点标签单方面错误 |
didn't match pod anti-affinity rules | Pod间反亲和性无法满足 | 参照Pod标签、Namespace、topologyKey | 不应直接删除反亲和性 |
topology spread constraints相关提示 | 新Pod会超过硬性偏斜限制 | 合格域、参与计数Pod、maxSkew | 不能只数可用区总数 |
unbound immediate PersistentVolumeClaims | PVC绑定尚未满足 | PVC、SC、provisioner、Events | 不一定需要删除PVC |
volume node affinity conflict | 卷拓扑与候选Node冲突 | PV NodeAffinity、Pod约束、zone标签 | 不能靠抢占解决 |
preemption is not helpful | 移除低优先级Pod仍无解 | 非资源约束和资源形状 | 不代表调度器故障 |
如果同一事件列出多类原因,要把Node分组,而不是只处理第一行:
kubectl get nodes \
-o custom-columns='NAME:.metadata.name,UNSCHEDULABLE:.spec.unschedulable,TAINTS:.spec.taints,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory'
然后针对原本符合业务设计的节点池逐一验证标签、污点、资源和存储条件。
十一、C级生产化重建案例:节点利用率不高,为什么滚动发布仍然卡在Pending
11.1 先说明哪些是真的,哪些是重建的
下面不是作者声称亲历的生产事故,而是根据Kubernetes调度、Deployment滚动更新和NodeAffinity机制构造的生产化重建。它的目的不是编一个故事,而是展示真实值班过程中如何从发布现象走到可验证根因。
证据边界如下:
| 内容 | 证据属性 |
|---|---|
| Deployment、ReplicaSet、Pending Pod和FailedScheduling之间的机制 | Kubernetes官方机制 |
maxUnavailable: 0、maxSurge: 1下旧副本保留、新副本Pending导致发布停滞 | 机制一致的重建场景 |
| Namespace、资源名、Node数量、相对时间和终端输出 | 为讲解构造的说明性信息 |
NodeAffinity值onlne的拼写错误 | 模拟根因,不是作者生产记录 |
| 修正单一变量后验证新Pod调度 | 受控验证设计,不声称已在当前集群执行 |
所有输出都按真实对象关系编排,但没有连接实际集群采集。读者可以复用排查方式,不能把下面的时间、数量或输出引用为真实事故数据。
11.2 现场卡片:业务尚未中断,但发布已经无法继续
场景设定为一个托管Kubernetes生产集群,某无状态API使用Deployment运行3个副本。滚动策略为:
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
一次Helm发布修改了Pod模板。新ReplicaSet创建出第一个Pod后,该Pod一直Pending;三个旧Pod仍然Ready,因此当前已知事实是发布停滞,不能据此虚构请求错误率或用户影响。
重建的相对时间线:
| 相对时间 | 观察或动作 | 当时能够得出的结论 |
|---|---|---|
| T+00 | 发布系统开始更新Deployment | 只能确认发生了配置变更 |
| T+01 | 新ReplicaSet创建一个Pod | 控制器创建链路正常,Pod对象确实存在 |
| T+03 | 新Pod仍为Pending,旧副本保持Ready | 发布无法继续,当前不是旧副本全部失效 |
| T+05 | 有人看到Node实时CPU不高,提出调度器异常 | 这是待验证假设,不是结论 |
| T+08 | FailedScheduling指向NodeAffinity和污点 | 排查转向调度约束,不再以实时CPU为主线 |
| T+12 | 新旧模板对照发现新增硬亲和性值拼错 | 得到可验证的主假设 |
| T+验证 | 只修正该值,观察新Pod是否绑定普通节点 | 用单变量验证因果关系 |
这里使用相对时间是为了展示排查顺序,不代表真实恢复时长。
11.3 先确认是哪个层次卡住
首先查看Deployment和ReplicaSet,而不是直接登录Node:
NS=example-prod
DEPLOY=checkout-api
kubectl get deployment "$DEPLOY" -n "$NS"
kubectl get replicaset -n "$NS" -l app=checkout-api \
-o custom-columns='NAME:.metadata.name,DESIRED:.spec.replicas,CURRENT:.status.replicas,READY:.status.readyReplicas,AVAILABLE:.status.availableReplicas'
kubectl rollout status deployment/"$DEPLOY" -n "$NS" --timeout=10s
机制一致的说明性输出:
NAME READY UP-TO-DATE AVAILABLE AGE
checkout-api 3/3 1 3 240d
NAME DESIRED CURRENT READY AVAILABLE
checkout-api-oldhash 3 3 3 3
checkout-api-newhash 1 1 <none> <none>
Waiting for deployment "checkout-api" rollout to finish:
1 out of 3 new replicas have been updated...
error: timed out waiting for the condition
这些证据支持:
- Deployment控制器已经创建新ReplicaSet和新Pod,不是ResourceQuota导致的
FailedCreate - 旧副本仍然可用,
maxUnavailable: 0阻止控制器先减少旧副本 - 新副本没有Ready,发布无法向后推进
- 目前还不知道新Pod是在调度、拉镜像还是启动阶段失败
找出新ReplicaSet的Pod:
kubectl get pods -n "$NS" -l app=checkout-api \
-o custom-columns='NAME:.metadata.name,HASH:.metadata.labels.pod-template-hash,PHASE:.status.phase,NODE:.spec.nodeName,READY:.status.containerStatuses[*].ready'
机制一致的说明性输出:
NAME HASH PHASE NODE READY
checkout-api-oldhash-a1 oldhash Running worker-a true
checkout-api-oldhash-b2 oldhash Running worker-b true
checkout-api-oldhash-c3 oldhash Running worker-c true
checkout-api-newhash-d4 newhash Pending <none> <none>
再检查Pending Pod的调度Condition:
POD=checkout-api-newhash-d4
kubectl get pod "$POD" -n "$NS" \
-o custom-columns='NAME:.metadata.name,PHASE:.status.phase,NODE:.spec.nodeName,SCHEDULED:.status.conditions[?(@.type=="PodScheduled")].status,REASON:.status.conditions[?(@.type=="PodScheduled")].reason'
机制一致的说明性输出:
NAME PHASE NODE SCHEDULED REASON
checkout-api-newhash-d4 Pending <none> False Unschedulable
nodeName为空且PodScheduled=False,证明问题发生在调度绑定之前。此时查容器日志、镜像拉取和应用启动参数都不是主线。
为什么这里没有应用日志
案例输出要完整,但不能为了形式完整而伪造不存在的应用日志。这个Pod尚未分配Node,容器进程从未启动,因此执行下面的命令不会得到业务启动日志:
kubectl logs "$POD" -n "$NS" --all-containers=true --tail=50
机制一致的说明性输出,具体错误文本可能随版本变化:
Error from server (BadRequest): pod checkout-api-newhash-d4 does not have a host assigned
这条输出本身也是证据:
- 它与
spec.nodeName=<none>相互印证,说明请求还没有进入Node上的容器运行阶段 - 此时没有
--previous日志,因为不存在上一个已启动后终止的容器实例 - 它不能说明应用镜像和启动命令一定正确,只能说明现在还没到验证它们的阶段
因此,未调度Pending的主要日志数据不是应用日志,而是Scheduler产生的FailedScheduling Event。Events已经能够解释过滤结果时,不必为了增加一段日志而先访问控制面。
Events不足时,再补调度器日志
只有当Events缺失、被聚合、信息不足,或者多个无关工作负载同时异常时,才需要继续查调度器日志。自建集群可以按实际标签读取kube-scheduler日志:
kubectl get pods -n kube-system -l component=kube-scheduler -o wide
kubectl logs -n kube-system -l component=kube-scheduler --since=15m --prefix --tail=500 | grep -F "$POD"
托管集群不一定允许读取控制面Pod,应使用云厂商控制面日志功能。查询前要确认Scheduler日志已经启用、当前身份有权限,并限制时间窗口和Pod名称,避免拉取全量控制面日志。
下面是对不同版本日志字段归一化后的说明性片段,不是某个真实版本的原始日志格式:
<timestamp> scheduler pod="example-prod/checkout-api-newhash-d4" result="unschedulable"
<timestamp> scheduler plugin="NodeAffinity" rejectedNodes="worker-a,worker-b,worker-c,worker-d,platform-a,platform-b"
<timestamp> scheduler plugin="TaintToleration" rejectedNodes="platform-a,platform-b" taint="dedicated=platform:NoSchedule"
<timestamp> scheduler preemption="not-helpful" nominatedNode=""
它与Pod Event应形成一致关系:NodeAffinity淘汰全部Node,TaintToleration又淘汰两个专用Node,抢占不能改变这些约束。如果日志与Events不一致,应先核对时间窗口、Pod UID、调度器Profile和是否发生了新一轮调度,不能选择性相信支持当前猜测的那一份输出。
11.4 为什么Node实时CPU不高仍然不能调度
值班人员查看资源面板,发现普通Node实时CPU并不高。为了避免只凭监控截图下结论,可以用下面的只读命令保存近期视图:
kubectl top nodes
kubectl get nodes \
-o custom-columns='NAME:.metadata.name,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory,TAINTS:.spec.taints'
机制一致的说明性输出节选:
NAME CPU(cores) CPU% MEMORY(bytes) MEMORY%
worker-a 1220m 31% 6850Mi 44%
worker-b 980m 25% 6120Mi 39%
worker-c 1360m 34% 7010Mi 45%
worker-d 1100m 28% 6420Mi 41%
这只能说明采样时实际CPU不高,不能证明Pod满足所有调度条件,也不能证明requests账本一定有余量。更关键的证据来自Pod Events:
kubectl get events -n "$NS" \
--field-selector "involvedObject.kind=Pod,involvedObject.name=$POD" \
--sort-by=.metadata.creationTimestamp
机制一致的说明性输出:
TYPE REASON OBJECT MESSAGE
Warning FailedScheduling pod/checkout-api-newhash-d4 0/6 nodes are available: 2 node(s) had untolerated taint {dedicated: platform}, 6 node(s) didn't match Pod's node affinity/selector. preemption: 0/6 nodes are available: preemption is not helpful for scheduling.
注意,原因中的节点计数可能重叠,不能把2和6相加后认为集群有8个节点。两个专用节点既可能不匹配Affinity,也可能同时有不可容忍污点。
当前证据支持:
- 所有候选Node都不满足新Pod的硬亲和性
- 其中两个Node还存在不可容忍污点
- 抢占低优先级Pod不能改变标签和污点,因此没有帮助
- 事件没有把CPU不足列为过滤原因,当前不支持资源容量是主要阻塞条件
- kube-scheduler是在按配置正确拒绝不合格节点,不能据此判断调度器故障
11.5 用正常对象和异常对象做对照
旧Pod正常、新Pod异常,是非常有价值的对照组。但不要直接diff完整Pod YAML,因为动态状态、UID和时间戳会制造大量噪声。先只提取调度相关字段:
NORMAL_POD=checkout-api-oldhash-a1
printf '%s\n' '旧Pod调度配置:'
kubectl get pod "$NORMAL_POD" -n "$NS" \
-o jsonpath='{.spec.nodeSelector}{"\n"}{.spec.affinity.nodeAffinity}{"\n"}{.spec.tolerations}{"\n"}'
printf '%s\n' '新Pod调度配置:'
kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{.spec.nodeSelector}{"\n"}{.spec.affinity.nodeAffinity}{"\n"}{.spec.tolerations}{"\n"}'
kubectl get nodes -L workload-tier
机制一致的说明性对照:
旧Pod调度配置:
map[]
<none>
[默认容忍项省略]
新Pod调度配置:
map[]
requiredDuringSchedulingIgnoredDuringExecution:
workload-tier In [onlne]
[默认容忍项省略]
NAME WORKLOAD-TIER
worker-a online
worker-b online
worker-c online
worker-d online
platform-a online
platform-b online
此时已经形成主假设:新版本增加了硬性NodeAffinity,但值onlne与所有Node的实际值online不一致。
还要回到声明式配置确认差异来源,避免只修现场生成物。先确认Helm Release名称,再保存Values、渲染结果和当前Deployment:
helm list -n "$NS"
RELEASE=checkout-api
kubectl get deployment "$DEPLOY" -n "$NS" -o yaml >deployment-current.yaml
helm get values "$RELEASE" -n "$NS" >release-values.yaml
helm get manifest "$RELEASE" -n "$NS" >release-manifest.yaml
机制一致的helm list说明性输出,Revision、Chart和应用版本均为构造信息:
NAME NAMESPACE REVISION STATUS CHART APP VERSION
checkout-api example-prod 42 deployed checkout-api-1.8.3 2.7.4
从release-values.yaml提取本次调度配置:
sed -n '/nodeAffinity:/,/tolerations:/p' release-values.yaml
机制一致的说明性输出:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: workload-tier
operator: In
values:
- onlne
tolerations: []
再确认错误值已经进入Helm渲染结果,而不是只存在于某个未生效的Values文件:
grep -n -A 12 -B 3 'workload-tier' release-manifest.yaml
机制一致的说明性输出:
84- nodeSelectorTerms:
85- - matchExpressions:
86- - key: workload-tier
87- operator: In
88- values:
89- - onlne
最后对比Git中的已知可用配置和本次候选配置。下面的引用名是占位符,必须替换为经过确认的Commit或Tag:
git diff '<known-good-ref>..<candidate-ref>' -- values-prod.yaml
机制一致的说明性输出:
+nodeAffinity:
+ requiredDuringSchedulingIgnoredDuringExecution:
+ nodeSelectorTerms:
+ - matchExpressions:
+ - key: workload-tier
+ operator: In
+ values:
+ - onlne
Helm输出和Deployment YAML可能包含内部镜像、地址与配置引用,归档和分享前必须脱敏。这里的输出只能证明错误值进入了发布配置;它还需要与FailedScheduling、Node标签和修复后的单变量验证组合,才能闭环根因。
如果Git变更也显示本次发布把Affinity从空值改成onlne,那么证据链是:
发布后只有新Revision异常
+ 新Pod尚未绑定Node
+ FailedScheduling明确指向硬亲和性
+ 所有Node标签值均为online
+ 新模板要求onlne
+ 旧模板没有该硬约束
= 强烈支持本次模板拼写错误是调度阻塞原因
这仍然是主假设,不应在受控验证前直接写成根因已经闭环。
11.6 先止损还是继续验证
当前三个旧副本仍Ready,最安全的止损通常是暂停后续发布动作,避免有人手工删除旧Pod。若继续删除旧副本,maxUnavailable: 0提供的保护可能被人为绕过,业务容量会下降。
可以选择回滚到已知可用的上一版,也可以在变更窗口内修正当前版本。两种方式都属于变更操作,执行前至少确认:
- 旧副本是否真实Ready且业务指标正常
- PDB、
maxUnavailable和maxSurge - 上一版镜像与配置是否仍可用
- 数据库或消息格式是否向后兼容
- Git、Helm或发布平台中的可回退版本
如果决定回滚,命令应使用目标环境确认过的Revision,不能照抄示例编号:
helm history "$DEPLOY" -n "$NS"
helm rollback "$DEPLOY" '<verified-revision>' -n "$NS" --wait --timeout 5m
影响:回滚会再次修改Deployment模板并创建或替换Pod。回退方式:重新发布经过验证的修正版,但不能在未确认兼容性时来回切换。
11.7 单变量验证,而不是同时增加Toleration和扩容
如果选择验证修正版,只把NodeAffinity值从onlne改为online,不同时修改requests、Tolerations、Priority、Node标签或副本数:
spec:
template:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: workload-tier
operator: In
values:
- online
验证步骤需要把修复后的对象状态、调度事件和应用启动结果分别保存,不能只写一句发布成功:
kubectl rollout status deployment/"$DEPLOY" -n "$NS" --timeout=5m
kubectl get deployment "$DEPLOY" -n "$NS"
kubectl get pods -n "$NS" -l app=checkout-api -o wide
机制一致的说明性输出,不是当前集群实测:
deployment "checkout-api" successfully rolled out
NAME READY UP-TO-DATE AVAILABLE AGE
checkout-api 3/3 3 3 240d
NAME READY STATUS RESTARTS NODE
checkout-api-fixedhash-e5 1/1 Running 0 worker-b
checkout-api-fixedhash-f6 1/1 Running 0 worker-c
checkout-api-fixedhash-g7 1/1 Running 0 worker-d
再单独确认修正版Pod已经完成调度,而不是只依赖Deployment摘要:
NEW_POD=checkout-api-fixedhash-e5
kubectl get pod "$NEW_POD" -n "$NS" -o custom-columns='NAME:.metadata.name,PHASE:.status.phase,NODE:.spec.nodeName,SCHEDULED:.status.conditions[?(@.type=="PodScheduled")].status,READY:.status.conditions[?(@.type=="Ready")].status'
机制一致的说明性输出:
NAME PHASE NODE SCHEDULED READY
checkout-api-fixedhash-e5 Running worker-b True True
调度与启动阶段Events应出现新的成功链路:
kubectl get events -n "$NS" --field-selector "involvedObject.kind=Pod,involvedObject.name=$NEW_POD" --sort-by=.metadata.creationTimestamp
机制一致的说明性输出,镜像地址和时间已省略:
TYPE REASON FROM MESSAGE
Normal Scheduled default-scheduler Successfully assigned example-prod/checkout-api-fixedhash-e5 to worker-b
Normal Pulling kubelet Pulling image "<redacted-image>"
Normal Pulled kubelet Successfully pulled image "<redacted-image>"
Normal Created kubelet Created container app
Normal Started kubelet Started container app
这时容器已经启动,应用日志才成为有效证据:
kubectl logs "$NEW_POD" -n "$NS" -c app --tail=20 --timestamps
机制一致的说明性应用日志,不代表真实应用框架或原始生产输出:
<timestamp> INFO [main] application - configuration loaded
<timestamp> INFO [main] server - listening on port 8080
<timestamp> INFO [main] readiness - application is ready to receive traffic
这些输出分别证明不同范围的事实:
| 输出 | 能够支持 | 不能单独证明 |
|---|---|---|
rollout status成功 | Deployment满足滚动完成条件 | 所有业务请求和外部依赖绝对健康 |
PodScheduled=True | 修正版Pod已经绑定Node | 应用已经Ready |
Ready=True | Readiness检查当前通过 | 长时间运行稳定、无性能问题 |
Scheduled Event | 调度器已选择Node | 原因只能是Affinity修正 |
| 启动日志 | 进程完成当前启动路径 | 所有接口、下游依赖和业务数据均正常 |
根因闭环还应满足:
- 修正版与失败版只有亲和性值这一项主要差异。
- 修正后新Pod能够绑定带
workload-tier=online的普通Node。 - 不需要删除污点,也不需要扩容Node。
- FailedScheduling不再重复出现同一Affinity原因。
- 后续发布观察窗口内没有再次发生同类阻塞。
如果修正后仍然Pending,就必须停止把拼写错误当作唯一根因,重新读取最新Events,检查requests、PVC和其他约束。
11.8 为什么这个案例接近真实生产现场
它不是因为使用了精确数字而真实,而是保留了生产排障中的关键矛盾:
- 业务副本暂时可用,但滚动发布已经卡死
- 实时CPU看起来有空间,却与调度结果不一致
- 同一个Node可能同时命中多个失败原因
- 旧Pod正常并不能证明新模板正确
- 删除污点、扩容和降低requests都可能让现象变化,但不能验证真正的配置差异
- 正确修复位置在Helm或Git源配置,不是在运行中的Pod上临时打补丁
案例仍然是C级重建。以后如果获得真实事件、模板差异和验证记录,可以保留这套结构,把说明性输出替换为脱敏证据,再升级为A级案例。

十二、修复方案要分六层
| 层次 | 本文场景中的动作 | 关键边界 |
|---|---|---|
| 应急止损 | 暂停有问题的发布,必要时回退上一版Pod模板 | 先检查可用副本、PDB、兼容性和回退版本 |
| 现场取证 | 保存Pod、Events、工作负载模板、Node标签、污点和调度字段 | Events会过期,输出需脱敏 |
| 根因验证 | 只修正一个亲和性值,在受控范围创建新Pod验证 | 不同时改Toleration、requests和节点标签 |
| 永久修复 | 修复Git/Helm源清单,而不是只改现场对象 | 防止下次发布覆盖 |
| 监控预防 | 监控Pending Pod、FailedScheduling和调度队列 | 告警要排除合理的短暂Pending和SchedulingGated |
| 运行治理 | 建立标签字典、策略校验、容量基线和Pending Runbook | 明确节点池所有者与例外审批 |
应急恢复不等于根因确认。回滚后Pod能够调度,只能说明变更与故障相关;还需要配置差异、事件和单变量验证共同支撑具体根因。
十三、可直接使用的只读Pending现场采集脚本
下面脚本只读取对象,不删除Pod、不修改标签、不增加Toleration、不扩缩容,也不创建调试容器。
#!/usr/bin/env bash
set -u
set -o pipefail
NS=${1:?用法: $0 <namespace> <pod-name>}
POD=${2:?用法: $0 <namespace> <pod-name>}
STAMP=$(date +%Y%m%d-%H%M%S)
OUT="pending-evidence-${NS}-${POD}-${STAMP}"
mkdir -p "$OUT"
umask 077
capture() {
local file=$1
shift
printf '采集 %s\n' "$file"
if ! "$@" >"$OUT/$file" 2>"$OUT/$file.err"; then
printf '失败: %s,查看 %s.err\n' "$file" "$file" >&2
fi
}
capture context.txt kubectl config current-context
capture version.txt kubectl version
capture pod.yaml kubectl get pod "$POD" -n "$NS" -o yaml
capture pod-wide.txt kubectl get pod "$POD" -n "$NS" -o wide
capture pod-describe.txt kubectl describe pod "$POD" -n "$NS"
capture pod-events.txt kubectl get events -n "$NS" \
--field-selector "involvedObject.kind=Pod,involvedObject.name=$POD" \
--sort-by=.metadata.creationTimestamp
capture namespace-pods.txt kubectl get pods -n "$NS" -o wide
capture controllers.txt kubectl get deploy,statefulset,daemonset,job,replicaset \
-n "$NS" -o wide
capture quota-limitrange.yaml kubectl get resourcequota,limitrange \
-n "$NS" -o yaml
capture nodes.txt kubectl get nodes -o wide
capture node-scheduling.txt kubectl get nodes \
-o custom-columns='NAME:.metadata.name,UNSCHEDULABLE:.spec.unschedulable,TAINTS:.spec.taints,CPU:.status.allocatable.cpu,MEMORY:.status.allocatable.memory,PODS:.status.allocatable.pods'
capture storageclasses.yaml kubectl get storageclass -o yaml
capture priorityclasses.yaml kubectl get priorityclass -o yaml
capture runtimeclasses.yaml kubectl get runtimeclass -o yaml
PVC_FILE="$OUT/pvc-names.txt"
if kubectl get pod "$POD" -n "$NS" \
-o jsonpath='{range .spec.volumes[?(@.persistentVolumeClaim)]}{.persistentVolumeClaim.claimName}{"\n"}{end}' \
>"$PVC_FILE" 2>"$PVC_FILE.err"; then
while IFS= read -r pvc; do
[[ -n "$pvc" ]] || continue
safe_pvc=${pvc//\//_}
capture "pvc-${safe_pvc}.yaml" kubectl get pvc "$pvc" -n "$NS" -o yaml
capture "pvc-${safe_pvc}-describe.txt" kubectl describe pvc "$pvc" -n "$NS"
done <"$PVC_FILE"
fi
printf '采集完成: %s\n' "$OUT"
printf '分享前请检查内部地址、镜像、标签、配置引用和事件信息\n'
使用方式:
bash collect-pending-evidence.sh '<namespace>' '<pod-name>'
脚本边界:
- 集群级StorageClass、PriorityClass和RuntimeClass可能因RBAC失败,失败本身应记录为证据缺口
- 脚本不会自动计算每个Node的有效requests,需结合事件缩小候选节点后进一步审计
- Pod YAML不直接展开Secret值,但仍可能暴露内部命名、镜像和配置引用
- 如果Pod被控制器快速替换,应同时按控制器和时间窗口保存Events
- 采集结果不能替代根因判断,只用于保存首轮现场
十四、监控、准入和运行治理
14.1 监控什么
推荐至少覆盖:
- 按Namespace、工作负载和原因统计长时间Pending Pod
PodScheduled=False和Unschedulable持续时间- FailedScheduling事件与事件速率
- 调度器pending queue,包括active、backoff、unschedulable和gated等队列,具体标签以版本为准
- Node Allocatable与requests承诺比例
- Node Pool扩容失败、云资源配额和实例供应失败
- PVC Pending、CSI制备错误和存储容量
告警不能只写phase="Pending"。镜像拉取、Init Container、短暂滚动发布和SchedulingGated都可能处于Pending,需要结合持续时间、PodScheduled Condition和业务类型降噪。
14.2 发布前校验
可在CI或Admission Policy中检查:
- required NodeAffinity引用的标签和值是否存在于目标节点池
- Toleration是否符合节点池准入规则
- requests是否缺失、异常升高或超过单节点规格
- GPU等扩展资源是否有对应Node Pool
- topologySpreadConstraints的topologyKey是否有标签基线
- PVC StorageClass与工作负载拓扑是否匹配
- schedulerName、PriorityClass、RuntimeClass是否存在
- RollingUpdate峰值容量是否可承载
校验器不应简单禁止所有高级调度字段,而应把组织允许的标签、节点池和例外流程编码成策略。
14.3 团队Runbook的停止条件
每个分支都要有停止条件:
- Pod不存在:停止查Pod日志,转向控制器和准入
- nodeName已有值:停止查调度过滤,转向节点启动链路
- 事件与对象都不支持当前假设:回到阶段判断,不继续堆命令
- 需要修改污点、标签、PVC或优先级:停止只读取证,进入变更审批
- 证据不足:记录缺失的调度器日志、配置或云平台事件,不强行下结论

十五、常见误区
误区1:看到Pending就直接扩容
调度Gate、标签拼写、污点和PVC拓扑问题不会因为普通节点数量增加而自动解决。
误区2:用kubectl top判断调度器应该把Pod放进去
调度器主要按requests与Allocatable做资源过滤,不按实时usage替代调度账本。
误区3:把ResourceQuota写成已有Pod Pending的直接原因
配额违规通常在创建时被准入拒绝,常见表现是ReplicaSet FailedCreate。先确认Pod对象是否存在。
误区4:只处理FailedScheduling中的第一类原因
事件可能表示不同Node被不同条件过滤。修掉一个条件后,其余条件仍可能阻塞调度。
误区5:为了验证直接删除Node污点
这会改变整个节点池的准入边界,可能让大量Pod同时调度进来。
误区6:认为Toleration会把Pod吸引到节点
Toleration只是允许进入,不等于选择该节点。定向调度还需要标签和亲和性等约束。
误区7:看到nominatedNodeName就认为马上一定成功
它只是抢占过程中的提名,不是最终绑定保证。
误区8:直接删除Pending PVC重建
这可能触发存储回收并造成数据风险,必须先确认ReclaimPolicy、数据与备份。
误区9:同时修改requests、Affinity和Toleration
即使Pod恢复,也无法判断哪个变量起作用,还可能引入新的容量与隔离风险。
十六、面试怎么说
60秒版本
Pod Pending我会先区分三个阶段。第一,Pod对象是否存在,如果Deployment副本不足但没有Pod,就查ReplicaSet的FailedCreate、ResourceQuota、LimitRange和准入策略。第二,Pod存在时看spec.nodeName和PodScheduled Condition,nodeName已有值说明已经调度,应转查镜像、Sandbox、Init Container和挂载。第三,尚未调度时先排除schedulingGates和错误的schedulerName,再根据FailedScheduling事件检查资源requests、污点与容忍、NodeAffinity、Pod Anti-Affinity、拓扑分布和PVC绑定。资源判断使用Allocatable与requests账本,不用当前top数据代替。所有变更前先保存Events和配置,并尽量一次只验证一个变量。
3分钟场景版本
假设Deployment期望3个副本但新Pod Pending,我不会先扩容。先确认Pod对象存在、nodeName为空且PodScheduled=False,证明问题在调度前,而不是镜像或kubelet。然后看FailedScheduling事件。假如事件显示六个节点中两个有不可容忍的专用污点,四个不匹配NodeAffinity,我会把节点按过滤原因分组。接着对比正常旧Pod和异常新Pod的模板,核对Node标签。若发现新版本required NodeAffinity值拼写错误,而普通节点使用正确标签,专用节点本来就不该承载该业务,那么主假设是亲和性配置错误,而不是CPU不足。我会只修正Git或Helm中的标签值,在受控发布中创建新Pod,不同时增加Toleration或修改requests。发布前检查副本、PDB、maxSurge和回退版本;调度成功后继续观察,而不是把一次恢复直接写成根因证明。
十七、延伸问答
1. Pod为Pending,为什么没有FailedScheduling事件
可能已经绑定Node、处于SchedulingGated、指定了未运行的自定义调度器、事件已过期,或者当前身份无权读取Events。先检查nodeName、Conditions、gates、schedulerName和RBAC。
2. 节点CPU使用率很低,为什么提示Insufficient cpu
调度器看的是CPU requests承诺与Allocatable,不是当前实际利用率。应审计目标节点上所有Pod requests。
3. preferred亲和性不满足会让Pod Pending吗
单独的软偏好通常只影响打分,不应阻止调度。还要检查是否同时存在required约束、污点、资源或其他Filter条件。
4. 为什么修改Node标签要谨慎
标签可能同时被多个工作负载的Affinity、拓扑、存储和运维自动化使用。修改会影响整个节点的候选关系,应先查引用、评估范围并准备恢复原值。
5. Cluster Autoscaler能解决所有资源不足吗
不能。只有当新增节点能够消除调度约束,并且Node Group、云配额和扩容策略允许时才有帮助。标签错误、污点不匹配、PVC拓扑冲突或Pod大于最大节点规格都可能无法通过扩容解决。
6. 为什么抢占不能解决NodeAffinity问题
抢占只能通过移除较低优先级Pod释放资源,不能改变Node标签、污点、存储拓扑或硬亲和性。
7. PVC是Bound,为什么Pod仍可能Pending
已绑定PV可能带有NodeAffinity,Pod的候选Node与卷拓扑冲突时仍无法调度。还要检查AccessModes和其他Pod是否占用ReadWriteOncePod卷。
8. 调度器插件需要逐个排查吗
不应一开始就查所有插件。先根据Events和对象字段定位资源、污点、亲和性、拓扑或VolumeBinding分支;只有默认字段无法解释、多个Pod异常或使用自定义Profile时,再检查调度器配置和日志。
9. 可以直接给Pending Pod指定nodeName吗
不建议把它当成排障捷径。直接指定会绕过正常调度决策,可能忽略部分调度约束,但kubelet和存储等后续环节仍可能拒绝或失败,也会破坏调度治理。应修复控制器模板和真实约束。
10. 什么时候可以确认根因
至少需要配置或容量差异、事件证据、合理机制解释和受控验证相互支持。一次删除、重建或扩容后恢复,只能作为相关线索。
小结
- Pending包含未调度和已调度但容器未启动,先看
spec.nodeName与PodScheduled Condition。 - 副本不足但Pod不存在时,应查控制器FailedCreate、配额和准入,而不是查一个不存在的Pod。
- Scheduling Gates和错误的schedulerName会让正常调度尚未开始。
- 调度器的资源判断主要基于requests、Pod有效需求与Node Allocatable,不以实时top代替。
- 污点、亲和性、拓扑分布和存储绑定都是候选节点过滤条件,必须组合分析。
- 一条FailedScheduling事件可能包含多组Node失败原因,不能只处理第一项。
- 抢占只能释放资源,不能修复标签、污点和存储拓扑冲突。
- 变更前先保存现场,验证时尽量只改变一个主变量,并准备回退。
- 长期治理应覆盖标签字典、容量基线、准入校验、调度监控和Pending Runbook。
下一篇预告
下一篇进入CrashLoopBackOff系统排查。重点分析当前与上一次容器状态、退出码、logs --previous、探针、配置、依赖和启动顺序,并整理一份容器反复重启的证据清单。
参考资料
- Kubernetes官方文档:Pod Lifecycle
- Kubernetes官方文档:Debug Pods
- Kubernetes官方文档:Kubernetes Scheduler
- Kubernetes官方文档:Resource Management for Pods and Containers
- Kubernetes官方文档:Pod Overhead
- Kubernetes官方文档:Taints and Tolerations
- Kubernetes官方文档:Assigning Pods to Nodes
- Kubernetes官方文档:Pod Topology Spread Constraints
- Kubernetes官方文档:Storage Classes
- Kubernetes官方文档:Persistent Volumes
- Kubernetes官方文档:Resource Quotas
- Kubernetes官方文档:Limit Ranges
- Kubernetes官方文档:Pod Scheduling Readiness
- Kubernetes官方文档:Configure Multiple Schedulers
- Kubernetes官方文档:Scheduling Configuration
- Kubernetes官方文档:Pod Priority and Preemption
更多推荐
所有评论(0)