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名称,应回答三个问题:

  1. 是哪个控制器或准入Webhook添加的?
  2. 什么条件满足后应该移除?
  3. 负责移除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
labelSelectormatchLabelKeys哪些Pod参与计数,具体能力受版本影响
maxSkew允许的分布偏斜上限,计算与合格拓扑域有关
whenUnsatisfiable: DoNotSchedule硬约束,无法满足时不调度
whenUnsatisfiable: ScheduleAnyway软约束,倾向减少偏斜但不阻止调度
minDomains参与计算所需的最小合格域数量,适用条件受版本约束

排查步骤:

  1. 列出所有Node的topologyKey标签。
  2. 先应用NodeAffinity、NodeSelector和污点等条件,确认哪些拓扑域实际合格。
  3. labelSelector确认哪些已有Pod参与计数。
  4. 根据目标版本规则计算新增Pod后是否超过maxSkew
  5. 检查缺失拓扑标签的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是否正常
  • volumeBindingModeImmediate还是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/memoryrequests账本放不下Pod有效requests、Node Allocatable、已承诺requests不能用当前top证明节点绝对没资源
Too many podsNode Pod容量达到上限allocatable pods、CNI地址和节点Pod数量不一定是CPU或内存不足
untolerated taintPod不被目标节点池接纳Node taints、Pod tolerations、节点池用途不应直接删除污点
didn't match node affinity/selector硬标签约束不满足Pod约束、Node标签、调度器addedAffinity不一定是节点标签单方面错误
didn't match pod anti-affinity rulesPod间反亲和性无法满足参照Pod标签、Namespace、topologyKey不应直接删除反亲和性
topology spread constraints相关提示新Pod会超过硬性偏斜限制合格域、参与计数Pod、maxSkew不能只数可用区总数
unbound immediate PersistentVolumeClaimsPVC绑定尚未满足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: 0maxSurge: 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+08FailedScheduling指向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.

注意,原因中的节点计数可能重叠,不能把26相加后认为集群有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、maxUnavailablemaxSurge
  • 上一版镜像与配置是否仍可用
  • 数据库或消息格式是否向后兼容
  • 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=TrueReadiness检查当前通过长时间运行稳定、无性能问题
Scheduled Event调度器已选择Node原因只能是Affinity修正
启动日志进程完成当前启动路径所有接口、下游依赖和业务数据均正常

根因闭环还应满足:

  1. 修正版与失败版只有亲和性值这一项主要差异。
  2. 修正后新Pod能够绑定带workload-tier=online的普通Node。
  3. 不需要删除污点,也不需要扩容Node。
  4. FailedScheduling不再重复出现同一Affinity原因。
  5. 后续发布观察窗口内没有再次发生同类阻塞。

如果修正后仍然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. 什么时候可以确认根因

至少需要配置或容量差异、事件证据、合理机制解释和受控验证相互支持。一次删除、重建或扩容后恢复,只能作为相关线索。


小结

  1. Pending包含未调度和已调度但容器未启动,先看spec.nodeName与PodScheduled Condition。
  2. 副本不足但Pod不存在时,应查控制器FailedCreate、配额和准入,而不是查一个不存在的Pod。
  3. Scheduling Gates和错误的schedulerName会让正常调度尚未开始。
  4. 调度器的资源判断主要基于requests、Pod有效需求与Node Allocatable,不以实时top代替。
  5. 污点、亲和性、拓扑分布和存储绑定都是候选节点过滤条件,必须组合分析。
  6. 一条FailedScheduling事件可能包含多组Node失败原因,不能只处理第一项。
  7. 抢占只能释放资源,不能修复标签、污点和存储拓扑冲突。
  8. 变更前先保存现场,验证时尽量只改变一个主变量,并准备回退。
  9. 长期治理应覆盖标签字典、容量基线、准入校验、调度监控和Pending Runbook。

下一篇预告

下一篇进入CrashLoopBackOff系统排查。重点分析当前与上一次容器状态、退出码、logs --previous、探针、配置、依赖和启动顺序,并整理一份容器反复重启的证据清单。

参考资料

  1. Kubernetes官方文档:Pod Lifecycle
  2. Kubernetes官方文档:Debug Pods
  3. Kubernetes官方文档:Kubernetes Scheduler
  4. Kubernetes官方文档:Resource Management for Pods and Containers
  5. Kubernetes官方文档:Pod Overhead
  6. Kubernetes官方文档:Taints and Tolerations
  7. Kubernetes官方文档:Assigning Pods to Nodes
  8. Kubernetes官方文档:Pod Topology Spread Constraints
  9. Kubernetes官方文档:Storage Classes
  10. Kubernetes官方文档:Persistent Volumes
  11. Kubernetes官方文档:Resource Quotas
  12. Kubernetes官方文档:Limit Ranges
  13. Kubernetes官方文档:Pod Scheduling Readiness
  14. Kubernetes官方文档:Configure Multiple Schedulers
  15. Kubernetes官方文档:Scheduling Configuration
  16. Kubernetes官方文档:Pod Priority and Preemption

更多推荐