Kubernetes 存储鬼故事:当 3 个 Pod 抢一块硬盘时发生了什么?

云计算 云原生
此次 PV 劫持事故暴露了云原生技术栈中“配置即代码”的双刃剑特性:灵活性的背后,是严谨性的绝对要求。通过本文的深度解析,希望读者不仅能够规避类似问题,此次 PV 劫持事故暴露了云原生技术栈中“配置即代码”的双刃剑特性:灵活性的背后,是严谨性的绝对要求。通过本文的深度解析,希望读者不仅能够规避类似问题,更能在团队内建立起存储配置的“免疫体系”,让 Kubernetes 真正成为业务创新的坚实底座。

引言

对于这种案例,你们的处理思路是怎么样的呢,是否真正的处理过,如果遇到,你们应该怎么处理。

我想大多数人都没有遇到过。

开始

引言:云原生时代的“存储鬼故事”

在 Kubernetes 集群中,存储管理是许多团队的“暗礁区”。一个看似普通的 StatefulSet 配置错误,竟导致分布式数据库的多节点同时写入同一块磁盘,最终引发数据覆盖、服务崩溃的连环灾难。本文将深入拆解这一经典案例,揭示存储配置背后的技术陷阱,并给出可复用的解决方案。

第一部分:灾难现场还原

1.1 现象:混乱的数据库与崩溃的集群

某金融科技团队在 Kubernetes 上部署了一个 MongoDB 分片集群(使用 StatefulSet 管理),上线后频繁出现以下诡异现象:

• 数据“幽灵覆盖”:用户订单数据随机丢失,A 节点写入的记录被 B 节点覆盖。

• Pod 自杀式重启:日志中频繁出现 MongoDB failed to lock file: /data/db/mongod.lock 错误,Pod 因文件锁冲突陷入 CrashLoopBackOff

• 存储监控告警:Prometheus 检测到单个 PVC(data-pvc-0)被 3 个 Pod 同时挂载,磁盘 IOPS 飙升至 10,000 以上。

团队最初误以为是“分布式系统的正常波动”,直到某次数据错乱导致 10 万级订单金额异常,才意识到问题严重性。

1.2 初步排查:令人困惑的配置

基础设施环境

• Kubernetes 集群:v1.24(AWS EKS)

• 存储后端:AWS EBS(gp3 卷)

• 关键配置:

# StatefulSet 片段
volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteMany" ]  # 错误配置!
      storageClassName: "aws-ebs-ssd"
      resources:
        requests:
          storage: 100Gi
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

矛盾点分析

1. StatefulSet 的设计逻辑:每个 Pod(如 mongo-0mongo-1)应通过 volumeClaimTemplates 自动创建独立的 PVC/PV,为何多个 Pod 共享同一个 PVC?

2. AWS EBS 的物理限制:EBS 卷仅支持 ReadWriteOnce(单节点读写),为何 PVC 中声明 ReadWriteMany 未被拒绝?

第二部分:根因深度拆解

2.1 致命错误 1:StorageClass 的 volumeBindingMode 陷阱

问题配置

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: aws-ebs-ssd
provisioner: ebs.csi.aws.com
volumeBindingMode: Immediate  # 灾难源头!
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

技术原理

• Immediate 模式:PVC 创建时立即绑定 PV,无视 Pod 调度位置

• WaitForFirstConsumer 模式(正确选择):延迟 PV 绑定,直到 Pod 被调度到某节点,确保 PV 与节点拓扑匹配。

灾难连锁反应

1. StatefulSet 创建时,一次性生成所有 PVC(如 data-pvc-0data-pvc-1)。

2. 由于 volumeBindingMode: Immediate,所有 PVC 立即绑定到随机 EBS 卷。

3. AWS EBS 的区域限制:若集群跨多个可用区(AZ),部分 PVC 可能因 AZ 不匹配而绑定失败,转而“劫持”已有 PV。

4. 最终,多个 Pod 的 PVC 指向同一个 EBS 卷(RWX 模式未被过滤,见下文)。

2.2 致命错误 2:滥用 ReadWriteMany 访问模式

开发误区

• 误解声明式 API:认为 PVC 中声明的 accessModes 是“需求”而非“强制约束”,期望 Kubernetes 自动降级处理。

• 现实打脸:AWS EBS 的 CSI 驱动不会验证 accessModes,即使后端存储不支持 RWX,PVC 仍能成功绑定!

技术真相

• Kubernetes 的松散耦合设计:PVC 的 accessModes 仅是用户“期望”,存储驱动可自由决定是否遵守。

• AWS EBS 的“沉默妥协”:当 PVC 声明 ReadWriteMany 时,EBS 驱动会“默认”以 ReadWriteOnce 模式挂载,但允许多个 Pod 强制挂载同一卷

• 后果:多个 Pod 绕过 Kubernetes 调度,直接通过存储后端(EBS)挂载同一块磁盘,引发文件系统竞态。

2.3 文件系统层:为什么多写必然崩溃?

以 MongoDB 为例,其数据目录需要独占访问权

1. 锁文件冲突mongod.lock 文件用于保证单进程独占数据目录,多 Pod 同时挂载时,锁机制失效。

2. 日志文件撕裂:多个实例的 WiredTiger 日志(Journal)交叉写入,导致数据无法恢复。

3. 磁盘结构损坏:Ext4/XFS 等文件系统并非为多节点并发设计,元数据(inode、superblock)可能被破坏。

# 查看 EBS 卷挂载情况(SSH 到 Node)
$ lsblk
nvme1n1   259:4    0  100G  0 disk /var/lib/kubelet/pods/xxxx/volumes/kubernetes.io~csi/aws-ebs-vol1
# 发现同一卷被挂载到多个 Pod 目录!
  • 1.
  • 2.
  • 3.
  • 4.

第三部分:系统性修复方案

3.1 紧急止血:如何抢救数据?

1. 暂停 StatefulSet

kubectl scale statefulset mongo --replicas=0
  • 1.

2. 备份数据卷

• 通过 AWS 控制台为问题 EBS 卷创建快照。

• 切勿直接操作在线卷,避免进一步损坏。

3. 挂载到临时 Pod 恢复数据

# 临时恢复 Pod
apiVersion: v1
kind: Pod
metadata:
  name: data-recovery
spec:
  containers:
  - name: recovery-tool
    image: alpine
    command: ["sleep", "infinity"]
    volumeMounts:
    - name: data
      mountPath: /data
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: data-pvc-0  # 指定问题 PVC
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

• 使用 fsck 检查文件系统,提取未损坏数据。

3.2 配置修复:根治存储劫持

3.2.1 修正 StorageClass 绑定策略
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: aws-ebs-ssd
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer  # 关键修复!
parameters:
  type: gp3
  encrypted: "true"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

效果验证

# 描述 PVC,观察事件
kubectl describe pvc data-pvc-0
  • 1.
  • 2.

• 期望输出

Events:
Type    Reason                Age   From                         Message
----    ------                ----  ----                         -------
Normal  WaitForFirstConsumer  5s    persistentvolume-controller  waiting for first consumer to be created before binding
  • 1.
  • 2.
  • 3.
  • 4.
3.2.2 强制使用 ReadWriteOnce

在 StatefulSet 中修正 PVC 模板:

volumeClaimTemplates:
- metadata:
    name: data
  spec:
    accessModes: [ "ReadWriteOnce" ]  # 严格限制为 RWO
    storageClassName: "aws-ebs-ssd"
    resources:
      requests:
        storage: 100Gi
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

3.3 重建 StatefulSet:安全操作手册

1. 彻底清理旧资源

# 删除 StatefulSet(保留 Pod 用于数据迁移)
kubectl delete statefulset mongo --cascade=orphan

# 删除所有关联 PVC(谨慎操作!)
kubectl delete pvc data-pvc-0 data-pvc-1 data-pvc-2

# 确认 PV 状态变为 "Released"
kubectl get pv
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

2. 从备份恢复数据

• 基于快照创建新 EBS 卷,挂载到每个 Pod 的独立 PVC。

  1. 3. 滚动重启
kubectl apply -f fixed-statefulset.yaml
kubectl rollout status statefulset mongo
  • 1.
  • 2.

第四部分:防御体系构建 —— 从亡羊补牢到未雨绸缪

4.1 技术管控:代码未动,策略先行

• 策略 1:通过 OPA/Gatekeeper 禁止危险配置

# 策略:禁止创建 RWX 模式的 PVC
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sPSPVolumeTypes
metadata:
  name: deny-rwx-pvc
spec:
  match:
    kinds:
    - apiGroups: [""]
      kinds: ["PersistentVolumeClaim"]
  parameters:
    # 允许的访问模式列表
    allowedAccessModes: ["ReadWriteOnce", "ReadOnlyMany"]
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

• 策略 2:CI/CD 流水线集成检查在 Helm/Kustomize 渲染后,添加如下检查:

# 使用 pluto 检测废弃 API 和危险配置
pluto detect-files --target-versions k8s=v1.25 ./manifests/
  • 1.
  • 2.

4.2 架构优化:存储层的最佳实践

• 方案 1:专供 StatefulSet 的 StorageClass

# 专用 StorageClass,限制为 RWO + WaitForFirstConsumer
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: statefulset-ebs
  labels:
    usage: statefulset
provisioner: ebs.csi.aws.com
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
parameters:
  type: gp3
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

• 方案 2:Operator 自动化管理使用类似 MongoDB Kubernetes Operator 的方案,让 Operator 自动处理 PVC 模板、备份、扩缩容等复杂逻辑。

4.3 监控告警:实时捕获存储异常

• 指标 1:PVC 挂载冲突检测通过 Prometheus 监控 kubelet_volume_stats_* 系列指标,设置如下告警规则:

- alert:MultiplePodsMountSamePVC
expr:countby(persistentvolumeclaim)(kube_pod_spec_volumes_persistentvolumeclaims_info{})>1
for:5m
labels:
    severity:critical
annotations:
    summary: "Multiple Pods mounting the same PVC {{ $labels.persistentvolumeclaim }}"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • • 指标 2:存储后端健康度集成 AWS CloudWatch 的 EBS 卷 IOPS、延迟监控,确保存储性能达标。

第五部分:从案例中提炼的云原生存储哲学

5.1 Kubernetes 存储的“三大纪律”

1. StatefulSet 必须配 volumeClaimTemplates:手动管理 PVC 是万恶之源,务必让每个 Pod 自动获得独立存储。

2. 假设存储不支持任何高级特性:除非文档明确声明,否则默认存储仅支持 RWO,且不能跨节点挂载。

3. 永远测试存储行为:在预发布环境中模拟 Pod 故障、扩缩容场景,验证存储的真实表现。

5.2 文化启示:打破开发与运维的认知墙

• 开发人员须知

理解 PVC/PV 的物理含义,accessModes 不是“愿望清单”,而是“物理约束”。

分布式系统的数据一致性需在应用层设计,不能依赖存储黑魔法。

• 运维人员须知

• 提供“安全默认值”(Safe Defaults),例如预配置合规的 StorageClass。

• 通过策略守卫(Policy Guardrails)防止危险配置落地。

结语:让存储成为应用的地基,而非软肋

此次 PV 劫持事故暴露了云原生技术栈中“配置即代码”的双刃剑特性:灵活性的背后,是严谨性的绝对要求。通过本文的深度解析,希望读者不仅能够规避类似问题,更能在团队内建立起存储配置的“免疫体系”,让 Kubernetes 真正成为业务创新的坚实底座。

“在 Kubernetes 中,存储配置的每一个字符,都应是经过验证的真理。”—— 某事故复盘后的团队箴言

责任编辑:武晓燕 来源: 云原生运维圈
相关推荐

2019-11-12 14:41:41

Redis程序员Linux

2019-08-26 09:35:25

命令ping抓包

2021-01-18 08:23:23

内存时底层CPU

2017-12-15 10:35:48

2020-08-20 11:50:31

语言类型转换代码

2021-11-23 23:31:43

C语言数据类型系统

2014-08-15 16:50:34

玻璃

2021-06-30 06:02:38

MySQL SQL 语句数据库

2019-11-04 11:13:31

Python硬盘Windows

2015-11-19 00:11:12

2017-04-07 15:57:20

人工智能放射科诊断

2023-03-31 08:12:30

操作系统nanosleep信号

2015-07-03 09:27:43

网络闰秒

2017-04-05 09:50:50

人工智能医生

2018-02-26 08:42:53

2023-06-05 07:39:47

场景械硬盘安全

2024-12-30 07:15:00

OpenAIChatGPT人工智能

2022-09-15 07:54:59

awaitPromise

2020-08-17 12:47:07

Mozilla裁员浏览器

2022-05-31 13:58:09

MySQL查询语句
点赞
收藏

51CTO技术栈公众号