KKubepath
存储运维工程师 路线10 / 15 · 把后端存储接进集群退出路线
原理预计 35 分钟

PV、PVC、StorageClass 与 CSI

一个 PVC 从 Pending 到挂进容器,中间经过四个组件。知道是哪四个,排障就有了方向。

学完这节你能做到

  • 讲清 PV / PVC / StorageClass 三者的职责边界与动态供应流程
  • 按业务场景选对 accessModes 与 reclaimPolicy
  • 区分「卷创建不出来」和「卷挂不上」,分别去看哪个组件的日志

容器里的文件是临时的:容器崩溃重启,容器内创建和修改的文件就没了。K8s 用 Volume 解决这件事,并把它分成两大类。

Volume
├─ Ephemeral Volume 临时卷(与 Pod 同生命周期)
│   ├─ emptyDir            用宿主机系统盘或内存
│   └─ configMap / secret  把配置挂进容器
│
└─ Persistent Volume 持久卷(超出 Pod 生命周期)
    ├─ PV            集群里的一块存储
    ├─ PVC           用户对存储的申请
    └─ StorageClass  动态创建 PV 的模板

这一节把持久卷这条线讲透 —— 因为集群里几乎所有的存储故障都在这条线上。

PV / PVC / SC:谁创建、谁使用

理解这三者最好的方式是看谁创建它、它代表什么

对象谁创建代表什么
PV管理员,或由 SC 自动创建真实存在的一块存储后端
PVC应用开发者「我要一块 20G 的存储,用 xxx 类型」
StorageClass管理员创建 PV 的模板:用哪个驱动、什么参数

手工维护成百上千个 PV 是不现实的,所以有了动态供应(Dynamic Provisioning):管理员只定义 StorageClass,用户创建 PVC 时由驱动自动创建 PV 并绑定。

管理员视角                        用户视角
─────────                        ────────
部署 CSI 驱动         →
创建 StorageClass     →         创建 PVC,指定 storageClassName
                                     ↓(自动发生)
                                K8s 通过 SC 调用 CSI 创建 PV 并绑定
                                     ↓
                                Pod 的 volumes 引用 PVC,
                                通过 volumeMounts 挂到容器路径
i这条链路和控制面是同一个模式

注意这里又是 reconcile:PVC 是期望态,PV 是实际态,中间有个控制器(external-provisioner)在不停地把两者拉平。

所以排查思路和 L1 讲的一样 —— 看对象状态、看事件、看那个负责 reconcile 的控制器的日志。存储没有例外。

accessModes:最容易配错的字段

模式缩写含义谁支持
ReadWriteOnceRWO单节点读写块存储(RBD、云盘、local)
ReadOnlyManyROX多节点只读多数后端
ReadWriteManyRWX多节点读写文件存储(CephFS、GPFS、NFS)
ReadWriteOncePodRWOP单 Pod 读写较新特性,比 RWO 更严格
×RWO 不是「只能一个 Pod 用」

RWO 的准确含义是单节点读写。同一节点上的多个 Pod 是可以共享一个 RWO 卷的 —— 因为挂载点在节点上。

这会导致一类很难查的问题:本地测试时两个 Pod 恰好调度到同一节点,共享 RWO 卷工作正常;上生产后 Pod 分散到不同节点,开始报 Multi-Attach error,或者更糟 —— 两个节点同时挂载同一个块设备,把文件系统写坏。

要严格限制单 Pod 使用,用 ReadWriteOncePod。要真正多节点共享,必须用支持 RWX 的文件存储。

回收策略与绑定模式

这两个字段决定了「删 PVC 会发生什么」和「PV 什么时候创建」:

reclaimPolicy: Delete        # PVC 删除后,PV 和后端存储一起删除
# reclaimPolicy: Retain      # PVC 删除后保留 PV 与数据,需手工清理

volumeBindingMode: Immediate              # PVC 创建后立刻创建并绑定 PV
# volumeBindingMode: WaitForFirstConsumer # 等到有 Pod 使用时再创建
!生产环境的重要数据用 Retain

Delete 是默认值,也最省事 —— 但它意味着误删一个 PVC 就直接销毁了后端数据,没有回收站。

重要数据的 StorageClass 应该配 Retain:PVC 删除后 PV 进入 Released 状态,数据还在,确认无误再手工删。多一步手工操作,换一次误删事故的兜底。

WaitForFirstConsumer 则用于本地存储或有拓扑约束的后端:如果在知道 Pod 落在哪台节点之前就把 PV 创建出来,很可能建在错误的可用区或节点上,导致 Pod 根本调度不上去。

CSI 架构:两侧,两类故障

CSI(Container Storage Interface)把第三方存储和 K8s 解耦。一个 CSI 驱动通常部署成两部分:

controller(Deployment,通常 2 副本)
  ├─ external-provisioner   创建 / 删除卷
  ├─ external-attacher      attach / detach
  ├─ external-resizer       扩容
  ├─ external-snapshotter   快照
  └─ CSI driver 本体

node(DaemonSet,每节点一个)
  ├─ node-driver-registrar  向 kubelet 注册
  └─ CSI driver 本体        执行实际的 mount / umount

这个架构直接给出了排障的分界线:创建卷的问题看 controller,挂载卷的问题看 node。

# 卷创建不出来(PVC Pending)→ 看 provisioner
kubectl -n <ns> logs deploy/<csi-controller> -c csi-provisioner

# 卷创建了但 Pod 挂不上(Pod ContainerCreating)→ 看那台节点上的 node plugin
kubectl -n <ns> logs <csi-node-pod-on-that-node> -c csi-<driver>

按现象选排查入口

把上面所有内容压缩成一张值班时能直接用的表:

现象卡在哪先看什么
PVC Pending卷还没创建出来describe pvc 的 Events → provisioner 日志
PVC Bound 但 Pod ContainerCreating卷创建了但挂不上目标节点的 node plugin 日志
Pod 报 Multi-Attach erroraccessMode 与实际用法冲突PVC 的 accessMode 与 Pod 分布
PVC 删了但 PV 还在 ReleasedreclaimPolicy: Retain 生效了这是预期行为,确认后手工删
PV 卡在 Terminatingfinalizer 没被摘掉后端是否已删除、CSI 是否正常
扩容不生效SC 未开 allowVolumeExpansionSC 配置 + resizer 日志
检查点单选

两个 Pod 要共享同一份数据。PVC 用的是 ceph-csi-rbd 的 StorageClass,accessMode 写了 ReadWriteMany。会发生什么?

检查点单选

一个 PVC 状态是 Bound,但使用它的 Pod 一直卡在 ContainerCreating。应该去看哪里?

这节课的落点

  • PV 是实物、PVC 是申请、SC 是模板;动态供应把管理员从手工建 PV 里解放出来
  • RWO 是单节点而不是单 Pod,跨节点共享必须用支持 RWX 的文件存储
  • 重要数据的 SC 配 reclaimPolicy: Retain
  • CSI 分 controller(创建卷)和 node(挂载卷)两侧,PVC Pending 看前者,Pod ContainerCreating 看后者
  • 存储也是 reconcile 模式,排查套路和其它对象完全一致

延伸资料