标签: VolumeAttachment

  • 深入 K8S VolumeAttachment 死锁排查:Node 宕机引发的 Multi-Attach 挂载冲突与 Non-Graceful 驱逐实战

    某次处理生产环境高可用数据库集群的容灾演练故障,现象极具代表性:物理节点发生硬宕机(模拟拔电),该节点上的 StatefulSet Pod 被重新调度到新节点后,长时间卡在 ContainerCreating 状态。最终结论:在未进行 STONITH(Shoot The Other Node In The Head)确认前,直接对挂载了块存储的 Pod 执行 --force 删除是极度危险的低级操作。这会彻底打乱 K8S AD 控制器(Attach/Detach Controller)与 CSI 驱动的协同状态。正确的解法是利用 K8S 的 Non-Graceful Node Shutdown(NGNS)特性,通过 out-of-service 污点触发底层合法的 Volume 卸载。

    遇到 Pod 驱逐卡住,第一反应就是敲 kubectl delete pod xxx --force --grace-period=0,这种肌肉记忆在跑 Web 服务的无状态场景下无所谓,但在 StatefulSet + RWO(ReadWriteOnce)块存储场景下,就是在人为制造存储脑裂。

    案发现场与暴力操作的代价

    监控大盘显示某核心服务的 P99 延迟突增至超时阈值,对应的底层 Node 因为内核 Panic 处于 NotReady 状态。 排查新调度的 Pod 状态,发现报出经典的 CSI 挂载冲突错误:

    Warning  FailedAttachVolume  3m2s (x12 over 15m)  attachdetach-controller
    Multi-Attach error for volume "pvc-8f9a3b2c" Volume is already exclusively attached to one node and can't be attached to another
    

    排查过程中发现,之前的处理人员看 Pod 一直处于 Terminating,反手就是一个 --force 强删。 表面上看,Pod 从 APIServer 的 etcd 记录里消失了,并且顺利在另一台 Node 上生成了处于 Pending/ContainerCreating 的新 Pod,看似调度成功。但实际上,底层存储的控制面完全是乱套的。

    通过查看当前的 VolumeAttachment 对象,真相一目了然:

    # kubectl get volumeattachment -l "kubernetes.io/pv-name=pvc-8f9a3b2c" -o yaml
    ...
    spec:
      attacher: ebs.csi.aws.com
      nodeName: dead-node-01   <-- 依然绑定在旧的死亡节点上
      source:
        persistentVolumeName: pvc-8f9a3b2c
    status:
      attached: true           <-- CSI 认为还没有卸载
    

    为什么 K8S 会死锁?谈谈防御性编程的底线

    这个“死锁”不是 Bug,而是 K8S 存储架构在设计上的底线防御机制(Fencing)

    在 CSI(Container Storage Interface)的生命周期语义中,一个 RWO 的云盘(如 AWS EBS、阿里云 ESSD)要挂载到新节点,必须确保在旧节点上已经完全脱离。 当 Node 宕机处于 NotReady 时,K8S 的 kube-controller-manager 无法和该节点上的 kubelet 通信。AD 控制器为了防止数据损坏,会一直等待 kubelet 汇报 UnpublishVolume(卸载文件系统)完成。

    如果不强制等待会怎样? 假设 Node 并没有死,只是网络发生脑裂(Split-Brain),Node 上的业务进程仍在疯狂将 Page Cache 刷入磁盘(ext4/xfs)。如果此时 AD 控制器直接调用云厂商 API 将云盘强行卸载并挂载到新 Node 上,新旧两个 Kernel 同时对同一个 Block Device 的 Superblock 和 Journal 区域进行写操作,文件系统会在几秒钟内被彻底击穿,导致不可逆的数据损坏。

    强删 Pod (--force) 仅仅是删除了逻辑对象,并没有改变 VolumeAttachment 的物理挂载状态。CSI external-attacher 看到旧的挂载关系未解除,自然拒绝向底层 IaaS 发起新的 AttachVolume API 请求。

    破局之道:Non-Graceful Node Shutdown

    在 K8S 1.26+ 时代(或开启了相关 Feature Gate 的早期版本),处理这种硬宕机有了标准的官方姿势。

    不要去动 Pod,也不要试图手动去 kubectl edit volumeattachment 删 finalizers(这会导致 APIServer 状态与云厂商 IaaS 状态彻底脱节,后续挂载永久失败)。

    第一步:确认物理节点死亡。 通过云控制台、IPMI 或底层带外管理,确保该 Node 已经处于 Power Off 状态,或者至少其网络和存储 HBA 卡已被彻底隔离。这是所有操作的前提。

    第二步:打上 out-of-service 污点。 向 K8S 宣告该节点已物理死亡,允许绕过 kubelet 的优雅等待:

    kubectl taint nodes dead-node-01 node.kubernetes.io/out-of-service=nodeshutdown:NoExecute
    

    这个操作会触发连锁反应:

    1. Taint Controller 检测到 out-of-service 污点。

    2. 触发 Pod 的强制驱逐逻辑(无视 grace-period)。

    3. 最关键的一步:Attach/Detach Controller 捕获到该污点后,判定无需等待死亡 kubelet 的回应,直接调用 CSI 驱动的 ControllerUnpublishVolume 接口。

    4. CSI 驱动调用云厂商 IaaS API,在云底座层面强行将云盘与死机 Node 解绑。

    5. 旧的 VolumeAttachment 被清理,新 Node 上的 Pod 顺利触发 AttachVolume,业务恢复。

    待故障节点修好重新加回集群前,记得移除污点:

    kubectl taint nodes dead-node-01 node.kubernetes.io/out-of-service-
    

    排查清单:同类 Volume 挂载异常速查

    针对 StatefulSet + CSI 存储卡 ContainerCreating 的场景,请严格按照以下顺序排查:

    1. 查明 Pod 阻塞源头: 使用 kubectl describe pod 检查 Events。如果是 Multi-Attach error,说明被旧节点锁死;如果是 volume node affinity conflict,说明 StorageClass 拓扑感知(Topology)不匹配,Pod 被调度到了 PV 所在的可用区之外。

    2. 审查 VolumeAttachment 状态: 执行 kubectl get volumeattachment | grep ,查看 Attached 列的状态和绑定的 NodeName。若绑定在 NotReady 的节点上,立刻停止任何针对 Pod 的 --force 操作。

    3. 隔离与污点注入(STONITH 机制): 确认底层服务器无 IO 活动后,执行 kubectl taint nodes node.kubernetes.io/out-of-service=nodeshutdown:NoExecute 触发合法强制卸载。

    4. 校验底座 IaaS 状态(终极手段): 如果 K8S 侧显示 Attached: false 但新节点依然挂载失败,说明 CSI 控制面出现了数据不一致。需直接登录云厂商控制台(或使用 awscli/aliyun-cli),强制从 IaaS 层面 Detach 云盘,随后重建当前卡死的 VolumeAttachment 对象。

  • 深入 K8S CSI 挂载雪崩排查:Node 假死引发的 Multi-Attach 锁死与 VolumeAttachment 强制清理实战

    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 了文件系统,因此它绝对不会将 VolumeAttachmentattached 状态改为 false,挂载死锁由此产生。

    破局与自愈:如何安全介入清理死锁?

    方案一:手动暴力介入(适用于 K8s < 1.26)

    当明确知道旧节点已经物理宕机被彻底隔离(例如已经在云控制台强制关机),我们需要手动帮助 ADC 越过这道安全红线。

    1. 强制删除旧 Pod(如果它还处于 Terminating 状态): bash kubectl delete pod --grace-period=0 --force
    2. 强制删除旧节点残留的 VolumeAttachment: 找到对应 PV 的 VolumeAttachment 记录,直接干掉: bash kubectl delete volumeattachment csi-24a9e4...
    3. 此时,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
    

    这个污点是内置控制器的“免死金牌”。一旦加上:

    1. Taint Manager 会立刻驱逐节点上的所有 Pod,无视普通的 finalizers。

    2. 最核心的是: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)。