新增可用区节点池后 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 日志、存储后端健康状态和账号权限,都没有发现异常,方向因此卡住。
推翻这个判断的证据有三条:
waiting for first consumer 是 WaitForFirstConsumer 的正常状态,不是错误。 在这种模式下,PVC 本来就要等 Pod 被调度后才绑定或供应。只盯着 PVC 事件,会把它当成“再等等就好”,从而错过真正原因。didn't find available persistent volumes to bind。 这说明调度器在过滤节点时,发现节点的存储拓扑与 Pod 或卷的约束冲突,根本没有进入供应阶段。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 故障,而是调度约束与存储拓扑的配置冲突。
促成条件包括:
allowedTopologies 没有随节点池扩展。nodeSelector 固定可用区,而不是用 topologySpreadConstraints 做软约束;一旦存储侧没有对应拓扑,Pod 完全没有回退空间。volumeClaimTemplates 不可变,现场不能简单地“改一下存储类”;这也让处理动作更谨慎,延长了恢复时间。先判断是“未绑定”还是“已绑定但调度冲突”,两者的处理方式完全不同:
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 但卷在错误拓扑”。
allowedTopologies、CSI 节点插件是否已部署到新可用区、后端容量、快照能力、跨可用区延迟。topologySpreadConstraints 做均衡,而不是硬 nodeSelector 锁定单一可用区;必须锁定时,先确认该可用区有对应的存储类和 PV 能力。didn't find available persistent volumes to bind 等存储拓扑相关调度失败时,直接告警到存储和调度 owner,而不是只报“PVC Pending”。WaitForFirstConsumer 的语义写进 runbook:PVC Pending 时先看 Pod 事件,再看 StorageClass 和节点标签,最后才怀疑 CSI。nodeSelector 修改,必须同时评估卷拓扑、复制和恢复方案。合上页面,先回答,再展开对照。
WaitForFirstConsumer 下 PVC Pending 本身不是错误?什么情况下会变成死锁?didn't find available persistent volumes to bind 说明了什么?应该同时核对哪三类对象?WaitForFirstConsumer 会延迟绑定和供应,直到使用该 PVC 的 Pod 被调度,所以 PVC 在 Pod 调度前保持 Pending 是正常行为。当 Pod 的节点约束与 StorageClass 的 allowedTopologies 或 PV 的节点亲和没有交集时,Pod 无法调度,PVC 也就永远等不到 consumer,形成死锁。allowedTopologies、节点可用区标签、Pod 的 nodeSelector/亲和性;如果 PVC 已绑定,还要核对 PV 的 nodeAffinity。reclaimPolicy,而且 Pod 也不会因此获得可用区 B 的数据。正确做法是让 Pod 回到可用区 A,或用快照/备份把数据迁移到 B 的新 PV,再切换工作负载。
小程序版
微信扫码,随时查命令