从一次Pod调度失败讲起:手把手排查K8s + Ceph RBD存储的'多挂载'故障

那天凌晨三点,报警短信把我们从睡梦中拽醒——生产环境的一个关键服务Pod卡在ContainerCreating状态超过15分钟。监控面板上刺眼的红色警告显示:Multi-Attach error for volume "ceph-pv"。这个看似简单的错误背后,隐藏着Kubernetes存储子系统与Ceph RBD的深度交互机制。让我们用一次真实的故障复盘,带你穿透表象理解本质。

1. 故障现象与初步诊断

当kubectl describe pod显示Multi-Attach error时,大多数工程师的第一反应是检查PV/PVC绑定状态。但在我们的案例中,所有资源显示都完全正常:

$ kubectl get pvc
NAME        STATUS   VOLUME    CAPACITY   ACCESS MODES
ceph-pvc    Bound    ceph-pv   1Gi        RWO

$ kubectl get pv
NAME      CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS
ceph-pv   1Gi        RWO            Recycle          Bound

真正关键的线索藏在Pod事件详情里:

Events:
  Type     Reason              Age   From                     Message
  ----     ------              ----  ----                     -------
  Warning  FailedAttachVolume  2m    attachdetach-controller  Multi-Attach error for volume "ceph-pv"
  Normal   Scheduled           5m    default-scheduler        Successfully assigned to node-2

此时需要立即检查kubelet日志。在故障节点执行:

journalctl -u kubelet --since "1 hour ago" | grep -i rbd

日志中会出现类似这样的关键错误:

rbd: image k8stest/rbda is locked by other nodes

2. Ceph RBD的挂载机制深度解析

Ceph RBD的块设备特性决定了其挂载行为的三层限制:

挂载场景ReadWriteOnce支持典型错误表现
同节点跨Pod✅ 是无报错
同Pod多容器✅ 是无报错
跨节点挂载❌ 否Multi-Attach error

这种限制源于RBD底层机制:

  1. 块设备通过内核模块映射到主机
  2. Ceph通过独占锁机制保证数据一致性
  3. Kubernetes调度器无法感知存储层限制

验证方法:直接在Ceph集群查看设备锁状态

rbd status k8stest/rbda

输出示例:

Watchers:
  watcher=192.168.1.10:0/123456789 client.12345 cookie=123

3. Deployment更新策略的陷阱

当Deployment进行滚动更新时,其默认策略会与RBD特性产生冲突:

  1. 先启动新Pod(可能调度到新节点)
  2. 等待新Pod进入Ready状态
  3. 终止旧Pod

这个看似合理的流程在RBD场景下会导致:

时序图:
旧Pod(node-1) --持有RBD锁--> 新Pod(node-2)尝试挂载 --冲突--> 触发Multi-Attach错误

通过调整Deployment策略参数可以暂时缓解:

spec:
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0

但这不是根本解决方案,只是将问题从更新时转移到了节点故障时。

4. 终极解决方案选型

根据业务需求,我们有以下几种解决方案:

方案A:改用支持多节点挂载的存储

- rbd:
+ cephfs:
    monitors:
    - 192.168.0.5:6789
    path: /k8s_volumes/webapp
    user: admin
    secretRef:
      name: ceph-secret

优点

  • 彻底解决跨节点挂载问题
  • 支持真正的读写共享

缺点

  • 需要重建存储架构
  • 性能较RBD有所下降

方案B:拓扑感知调度 + Pod反亲和

affinity:
  podAntiAffinity:
    requiredDuringSchedulingIgnoredDuringExecution:
    - labelSelector:
        matchExpressions:
        - key: app
          operator: In
          values: [nginx]
      topologyKey: kubernetes.io/hostname

适用场景

  • 必须使用RBD的场景
  • 可以接受单节点故障导致服务中断

方案C:动态Provisioner配置

对于新建集群,建议直接配置StorageClass:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ceph-rbd
provisioner: rbd.csi.ceph.com
parameters:
  clusterID: ceph-cluster
  pool: k8stest
  imageFeatures: layering
  csi.storage.k8s.io/provisioner-secret-name: ceph-secret
  csi.storage.k8s.io/node-stage-secret-name: ceph-secret
reclaimPolicy: Delete
allowVolumeExpansion: true
mountOptions:
  - discard

5. 故障预防体系建设

建立三层防御机制:

  1. 事前检查清单

    • [ ] 验证存储类型与访问模式匹配
    • [ ] 部署前测试跨节点挂载场景
    • [ ] 配置适当的Pod反亲和规则
  2. 事中监控指标

    # RBD挂载失败告警
    kubelet_volume_stats_available_bytes{persistentvolumeclaim="ceph-pvc"} == 0
    
  3. 事后应急预案

    # 强制释放RBD锁(慎用)
    rbd lock remove k8stest/rbda client.12345
    

那次故障最终让我们意识到,在云原生存储领域,表面简单的配置背后需要深入理解各组件间的交互机制。现在我们的运维手册里多了一条铁律:使用RBD时,必须同时考虑调度策略和存储特性的匹配度。

更多推荐