系统学习 故障排查学习路径:从方法论到系统、网络、数据库与 K8s

incident-pvc-zone

新增可用区节点池后 PVC 卡在 Pending,看起来像 CSI 故障,实际是存储拓扑与调度约束互相锁死。

现象

某业务集群新增一个可用区节点池,为降低成本和缩短网络路径,团队把一批 StatefulSet 的 Pod 通过 nodeSelector 固定到新池。滚动更新开始后,新副本的 PVC 一直 Pending,Pod 也 Pending,StatefulSet 卡在更新过程中,后续发布全部阻塞。

kubectl describe pvc 的事件只有一行:

waiting for first consumer to be created before binding

没有 provision 失败、没有挂载错误,CSI controller 日志也很干净,存储后端容量充足。老副本运行正常,只有新副本起不来。由于上一次类似告警是 CSI 挂载失败,团队第一反应仍然是存储插件出了问题。

真正有信息量的事件在 Pod 上:

0/12 nodes are available: 6 node(s) didn't find available persistent volumes to bind,
6 node(s) didn't match Pod's node affinity/selector.

PVC 没有 volumeName,说明它还没有绑定到任何 PV;问题发生在调度和供应阶段,而不是挂载阶段。

时间线

时间 事件
T+0 修改 StatefulSet 的 nodeSelector,指向新可用区节点池,开始滚动更新
T+6m 新 Pod、PVC 同时 Pending,发布告警触发
T+12m 先查 CSI controller 和存储后端,均正常;StorageClass 参数看起来也没问题,误判为供应失败
T+20m 查看 PVC 事件,只看到 waiting for first consumer,误以为只是调度慢
T+28m 查看 Pod 事件,发现 didn't find available persistent volumes to bind
T+34m 对比 SC 的 allowedTopologies、节点可用区标签和 Pod 的 nodeSelector,确认拓扑交集为空
T+42m 回滚 nodeSelector,Pod 重新调度到旧可用区,PVC 开始绑定
T+50m PVC Bound,Pod Running,StatefulSet 恢复滚动
T+65m 完成数据一致性与副本健康检查
T+2h 补充新可用区上线检查清单和 PVC Pending 告警

影响面

新副本无法启动,StatefulSet 的滚动更新卡住,后续所有变更都被阻塞。对于有状态服务,副本数不足会降低读容量和故障域覆盖;如果当时发生节点故障,剩余副本的压力会明显上升。写入仍由老副本承担,没有出现数据丢失,但容量余量被压缩。

新可用区的资源无法投入使用,成本优化目标落空。PVC 处于未绑定状态,理论上删除重建不会丢数据,但现场如果误删已绑定的 PVC 或 PV,风险会完全不同。事故期间没有执行这类操作,恢复后做了完整的卷和副本校验。

误导线索

最初的判断是 CSI 供应失败,理由有三个:PVC Pending 是最直观的信号;上一次类似故障是 csi-mount-failed;StorageClass 的 provisioner 看起来配置正确。团队检查了 CSI controller 日志、存储后端健康状态和账号权限,都没有发现异常,方向因此卡住。

推翻这个判断的证据有三条:

  1. waiting for first consumer 是 WaitForFirstConsumer 的正常状态,不是错误。 在这种模式下,PVC 本来就要等 Pod 被调度后才绑定或供应。只盯着 PVC 事件,会把它当成“再等等就好”,从而错过真正原因。
  2. Pod 事件明确写了 didn't find available persistent volumes to bind。 这说明调度器在过滤节点时,发现节点的存储拓扑与 Pod 或卷的约束冲突,根本没有进入供应阶段。
  3. 三个对象的交集为空。 StorageClass 的 allowedTopologies 只包含旧可用区 A,新节点池的节点标签是可用区 B,Pod 的 nodeSelector 又强制要求 B。调度器找不到同时满足“Pod 节点约束”和“存储拓扑”的节点,PVC 就永远等不到 consumer。

节点 CPU、内存都有余量,所以“资源不足”也被排除。把 PVC 事件、Pod 事件、StorageClass 和节点标签放在一起看,因果链才完整。

根因

直接原因是 StorageClass 的 allowedTopologies 在创建时被写死为旧可用区 A,而新节点池位于可用区 B。StatefulSet 的 nodeSelector 又把新副本固定到 B,两个约束没有交集。

WaitForFirstConsumer 的语义是“先调度 Pod,再按 Pod 所在拓扑绑定或创建卷”,但它并不会绕过 StorageClass 的拓扑限制。调度器必须同时满足 Pod 的节点选择器、亲和性、污点容忍和存储拓扑;当这些条件互斥时,Pod 无法调度,PVC 也就不会触发动态供应,形成“Pod 等 PVC、PVC 等 Pod”的死锁。这不是 CSI 故障,而是调度约束与存储拓扑的配置冲突。

促成条件包括:

修复

先判断是“未绑定”还是“已绑定但调度冲突”,两者的处理方式完全不同:

kubectl -n app get pvc,pod -o wide

kubectl -n app describe pvc data-app-2
kubectl -n app describe pod app-2

kubectl -n app get events --sort-by=.lastTimestamp | tail -40

# PVC 是否已经绑定
kubectl -n app get pvc data-app-2 \
  -o jsonpath='{.spec.volumeName}{"\n"}{.status.phase}{"\n"}'

核对拓扑相关对象:

kubectl get sc fast-ssd -o yaml
kubectl get nodes -L topology.kubernetes.io/zone,node-pool
kubectl -n app get pod app-2 -o jsonpath='{.spec.nodeSelector}{"\n"}'
kubectl get csinodes -o wide

如果 PVC 未绑定,最快的恢复动作是撤销与新可用区冲突的 nodeSelector,让 Pod 回到有存储能力的旧可用区。StatefulSet 的 Pod 模板可以更新,修改后会触发新的滚动:

kubectl -n app patch statefulset app --type=json \
  -p='[{"op":"remove","path":"/spec/template/spec/nodeSelector/node-pool"}]'

如果业务确实要落到新可用区,需要同时满足三个条件:StorageClass 的 allowedTopologies 包含新可用区、CSI 节点插件在新可用区有实例、后端在该可用区有容量。确认后可以更新 StorageClass:

kubectl patch sc fast-ssd --type=merge -p \
  '{"allowedTopologies":[{"matchLabelExpressions":[
      {"key":"topology.kubernetes.io/zone","values":["zone-a","zone-b"]}
    ]}]}'

更新后调度器会重新尝试 Pending 的 Pod,PVC 有机会在可用区 B 完成供应;如果事件没有触发重调度,可以重建 Pending 的 Pod(不要动已绑定 PVC)。不要为了让 PVC 绑定而删除 StatefulSet 的已有 PVC;如果 PVC 已经 Bound 到可用区 A 的 PV,而 Pod 被改到 B,编辑 nodeSelector 不会迁移数据,正确做法是回到 A,或者用快照/备份把数据迁移到 B 的新卷。删除 Bound PVC 是否会连带删除 PV 取决于 reclaimPolicy,操作前必须确认。

如果必须更换存储类,由于 volumeClaimTemplates 不可变,通常需要创建新的 StatefulSet 或采用迁移方案,不能直接原地改字段。恢复后要检查 PVC 的 volumeName、PV 的 nodeAffinity、Pod 实际所在可用区和数据校验结果,确认不是“Pod Running 但卷在错误拓扑”。

预防

相关命令

参考链接

读完自测

合上页面,先回答,再展开对照。

  1. 为什么 WaitForFirstConsumer 下 PVC Pending 本身不是错误?什么情况下会变成死锁?
  2. Pod 事件里的 didn't find available persistent volumes to bind 说明了什么?应该同时核对哪三类对象?
  3. 如果 PVC 已经 Bound 到可用区 A 的 PV,而 Pod 被改到可用区 B,直接删 PVC 重建有什么风险?正确做法是什么?
答案
  1. WaitForFirstConsumer 会延迟绑定和供应,直到使用该 PVC 的 Pod 被调度,所以 PVC 在 Pod 调度前保持 Pending 是正常行为。当 Pod 的节点约束与 StorageClass 的 allowedTopologies 或 PV 的节点亲和没有交集时,Pod 无法调度,PVC 也就永远等不到 consumer,形成死锁。
  2. 说明调度器过滤节点时发现存储拓扑与 Pod/卷的约束冲突,Pod 无法落到满足卷拓扑的节点上。应同时核对 StorageClass 的 allowedTopologies、节点可用区标签、Pod 的 nodeSelector/亲和性;如果 PVC 已绑定,还要核对 PV 的 nodeAffinity。
  3. 删除 Bound PVC 可能触发 PV 回收,导致数据丢失,具体取决于 reclaimPolicy,而且 Pod 也不会因此获得可用区 B 的数据。正确做法是让 Pod 回到可用区 A,或用快照/备份把数据迁移到 B 的新 PV,再切换工作负载。
微信小程序码:扫码打开命令速查小程序

小程序版

微信扫码,随时查命令