Node 假死时,StatefulSet 发生驱逐漂移,但底层块存储因旧节点未释放导致新节点挂载失败,陷入持续的 Multi-Attach error 死锁。本文直接给出破局方案:通过清理 VolumeAttachment 僵尸对象强制解除挂载锁,并基于 K8s 1.26+ 的 out-of-service 污点实现 Non-Graceful Node Shutdown 自愈,同时剖析 CSI external-attacher 的防脑裂流转机制。
故障现场:Pod 永远停留在 ContainerCreating
某次处理基础架构告警,某可用区交换机故障导致部分 K8s Worker 节点失联(状态变为 NotReady)。按照系统默认配置,大约 5 分钟后(pod-eviction-timeout),运行在故障节点上的 StatefulSet 实例被驱逐并在健康的 Node 上重新调度。
但是,新创建的 Pod 一直卡在 ContainerCreating,通过 kubectl describe pod 查看 Events,满屏全是同一种报错:
Warning FailedAttachVolume 2m45s (x12 over 15m) attachdetach-controller
Multi-Attach error for volume "pvc-c93a8...": Volume is already exclusively attached to one node and can't be attached to another
同时,底层存储 CSI Driver(以 Ceph RBD 为例,AWS EBS/阿里云云盘同理)的日志中疯狂输出:
rpc error: code = FailedPrecondition desc = volume is published to another node
很明显,新节点无法将云盘 attach 过来,因为 K8s 认为这块盘还挂载在那个“已经死掉”的旧节点上。
为什么 CSI 驱动不会自动强制 Detach 假死节点的 Volume?
这是排查此类问题时最常产生的疑问:既然节点已经 NotReady 且 Pod 被驱逐了,为什么 K8s 负责管理挂载的 AttachDetachController (ADC) 不直接把旧节点上的盘强制卸载(Force Detach)?
答案是:为了绝对的数据安全(防脑裂)。
在块存储(ReadWriteOnce 模式,通常格式化为 ext4/xfs)的场景下,如果旧节点只是网络断开(假死),而 CPU、内存和磁盘 IO 还在正常运行。如果此时 K8s 强制在 IaaS 层将这块云盘摘除并挂载给新节点,新节点的 Pod 开始写入数据,一旦旧节点网络恢复,其内核缓存中未刷盘的脏数据(Dirty Pages)会继续向磁盘 flush,立刻导致文件系统元数据损坏(Filesystem Corruption)。
为了防御这种脑裂,CSI 引入了极其严谨的状态机。K8s 侧通过 VolumeAttachment CRD 来记录挂载状态,而非直接依赖底层云 API:
# 查看集群中的挂载记录
$ kubectl get volumeattachment -l "node.name=old-dead-node"
NAME ATTACHER PV NODE ATTACHED AGE
csi-24a9e4... diskplugin.csi.alibabacloud.com pvc-c93a... old-dead-node true 120d
查看这个僵尸 VolumeAttachment 的详情:
status:
attached: true # 这里一日不变成 false,新节点的 attach 就一日不能发起
attachmentMetadata:
csi.storage.k8s.io/node-name: old-dead-node
在旧节点恢复通信(或者被彻底销毁)之前,external-attacher Sidecar 无法确认原节点的 kubelet 是否已经安全 unmount 了文件系统,因此它绝对不会将 VolumeAttachment 的 attached 状态改为 false,挂载死锁由此产生。
破局与自愈:如何安全介入清理死锁?
方案一:手动暴力介入(适用于 K8s < 1.26)
当明确知道旧节点已经物理宕机或被彻底隔离(例如已经在云控制台强制关机),我们需要手动帮助 ADC 越过这道安全红线。
- 强制删除旧 Pod(如果它还处于 Terminating 状态):
bash kubectl delete pod--grace-period=0 --force - 强制删除旧节点残留的 VolumeAttachment:
找到对应 PV 的
VolumeAttachment记录,直接干掉:bash kubectl delete volumeattachment csi-24a9e4... - 此时,
external-attacher会监听到旧 Attachment 消失,ADC 终于允许为新节点创建新的VolumeAttachment,挂载流程恢复,Pod 启动。
方案二:Non-Graceful Node Shutdown (NGNS) 自动化(K8s 1.26+ 标准解法)
手动干预违背了自动化的运维信条。K8s 1.26 正式 GA 了 Non-Graceful Node Shutdown 特性。
当节点失联且你确认它无法恢复时(可以通过外部监控脚本或 Node 自动运维系统判定),不要去删 Pod,而是直接给这个死亡节点打上一个特定的污点:
kubectl taint nodes old-dead-node node.kubernetes.io/out-of-service=nodeshutdown:NoExecute
这个污点是内置控制器的“免死金牌”。一旦加上:
-
Taint Manager 会立刻驱逐节点上的所有 Pod,无视普通的 finalizers。
-
最核心的是:AttachDetachController 看到这个污点后,会认为系统管理员已经做出了背书(节点已死),它将直接绕过 CSI 正常的优雅 Detach 流程,强制删除
VolumeAttachment并通知云厂商底层解绑。
存储拓扑感知(Topology Awareness)的隐藏陷阱
在多可用区(Multi-AZ)架构下排查时,就算解决了 Multi-Attach,Pod 仍有可能一直处于 Pending,报错变成:
1 node(s) had volume node affinity conflict.
这是因为 K8s 原生集成了存储拓扑感知。CSI 驱动(例如 AWS EBS CSI Driver)在创建 PV 时,会在 PV 的 nodeAffinity 中注入可用区标签:
# PV 的拓扑信息片段
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: topology.ebs.csi.aws.com/zone
operator: In
values:
- ap-southeast-1a
如果旧节点在 AZ-A,而 K8s 调度器将 Pod 驱逐到了 AZ-B 的新节点上,此时跨可用区是无法挂载单 AZ 云盘的。
防范机制:StorageClass 延迟绑定
必须确保 StorageClass 启用了 volumeBindingMode: WaitForFirstConsumer。
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ebs-sc
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
allowedTopologies:
- matchLabelExpressions:
- key: topology.ebs.csi.aws.com/zone
values:
- ap-southeast-1a
- ap-southeast-1b
但对于已经创建且绑定到特定 AZ 的现存 PV,如果整个 AZ 挂了,K8s 层面无能为力。这就要求核心有状态服务(如 DB、消息队列)必须在应用层做高可用(如 Raft、多副本同步),不要指望底层块存储跨 AZ 漂移。
常见问题
Q1:如果不确认节点死透,直接强制删除 VolumeAttachment,具体会发生什么样的底层损坏?
A:如果旧节点只是管理网断开,业务网和存储网还在工作。强删 VolumeAttachment 导致盘被挂载给新节点,此时旧节点的 ext4 的日志(Journaling)仍在向设备写入,新节点也在写入。这会造成 inode 树严重破坏,通常在 5 分钟内整个文件系统就会变成只读(Read-only file system),甚至引发内核 Panic。操作前必须确保 IaaS 层原节点已下电。
Q2:为什么 NFS 或者 CephFS 这种网络文件系统不会发生 Multi-Attach 报错?
A:NFS/CephFS 提供的是文件级访问,其 AccessMode 通常是 ReadWriteMany(RWX)。K8s 和底层存储本身就允许多个节点同时挂载(Mount)同一个共享目录,没有独占锁(Exclusive Lock)的概念,因此不受 external-attacher 单点绑定的限制。
Q3:Local PV(本地盘)在节点宕机时,调度行为和 CSI 有什么不同? A:Local PV 与节点是强绑定的(通过严格的 NodeAffinity)。一旦本地磁盘所在的 Node 宕机,使用该 PV 的 Pod 无论如何都不会漂移到其他节点上,它会永远处于 Pending,直到原节点恢复。所以 Local PV 只能用于自身具备数据冗余复制能力的应用(如 Elasticsearch、TiKV)。