K8s 存储模型:PV、PVC、SC 与 CSI
容器时代的存储接口层,运维和开发在这里分工。
学完这节你能做到
- 说清 PV / PVC / StorageClass 三者的职责边界
- 解释 CSI driver 的组件构成与调用链
- 排查 PVC 一直 Pending 的常见原因
容器时代的存储接口层
容器里的文件是临时的:容器崩溃或重启,容器内创建和修改的文件就消失了。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 时 K8s 自动调用驱动创建对应的 PV 并绑定。
管理员视角 用户视角
───────── ────────
部署 CSI 驱动 →
创建 StorageClass → 创建 PVC,指定 storageClassName
↓(自动发生)
K8s 通过 SC 调用 CSI 创建 PV 并绑定
↓
Pod 的 volumes 引用 PVC,
通过 volumeMounts 挂到容器路径
accessModes:最容易配错的字段
| 模式 | 缩写 | 含义 | 谁支持 |
|---|---|---|---|
| ReadWriteOnce | RWO | 单节点读写 | 块存储(RBD、云盘、local) |
| ReadOnlyMany | ROX | 多节点只读 | 多数 |
| ReadWriteMany | RWX | 多节点读写 | 文件存储(CephFS、GPFS、NFS) |
| ReadWriteOncePod | RWOP | 单 Pod 读写 | 较新特性,比 RWO 更严格 |
RWO 的准确含义是单节点读写。同一个节点上的多个 Pod 是可以共享一个 RWO 卷的(因为挂载点在节点上)。
这经常导致一类困惑:本地测试时两个 Pod 恰好调度到同一节点,共享 RWO 卷工作正常;上生产后 Pod 分散到不同节点,就开始报 Multi-Attach error。
要严格限制单 Pod 使用,用 ReadWriteOncePod。要真正多节点共享,必须用支持 RWX 的文件存储。
回收策略与卷绑定模式
reclaimPolicy: Delete # PVC 删除后,PV 和后端存储一起删除
# reclaimPolicy: Retain # PVC 删除后保留 PV 和数据,需手工清理
volumeBindingMode: Immediate # PVC 创建后立刻创建并绑定 PV
# volumeBindingMode: WaitForFirstConsumer # 等到有 Pod 使用时再创建
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
- 1.确认 PVC 的状态和它请求的 StorageClass
- 2.看 Events,让 K8s 直接告诉你卡在哪
- 3.确认 StorageClass 引用的 secret 与集群参数
- 4.确认 secret 里的用户名
- 5.在 Ceph 侧核对该用户的权限范围
模拟终端:一个 PVC 创建了 10 分钟仍是 Pending,业务 Pod 起不来。 输入 goals 看目标,hint 要提示。
结论:client.kubernetes 的 OSD 权限被限定在 pool=kube,而 StorageClass 里指定的池是 rbd。修复方式是二者对齐:
# 方式一:放开用户权限到正确的池
ceph auth caps client.kubernetes \
mon 'profile rbd' \
osd 'profile rbd pool=rbd' \
mgr 'profile rbd pool=rbd'
# 方式二:把 StorageClass 的 pool 改成 kube
PVC Pending 排查清单
按顺序走,绝大多数问题在前三步就能定位:
| 检查项 | 命令 | 常见原因 |
|---|---|---|
| 1. PVC 事件 | kubectl describe pvc | 信息最直接,先看这里 |
| 2. StorageClass 存在吗 | kubectl get sc | 名字写错、SC 不存在 |
| 3. provisioner 日志 | kubectl logs <csi-controller> | 权限、网络、参数错误 |
| 4. secret 是否正确 | kubectl get secret -o yaml | key 名字错、namespace 错、内容过期 |
| 5. 后端存储健康 | ceph -s / mmhealth | 存储侧真的有问题 |
| 6. accessMode 是否支持 | 对比 SC 与 PVC | 用 RBD 要求 RWX |
| 7. 容量与配额 | ceph df / mmlsquota | 后端没空间了 |
| 8. 拓扑约束 | describe 里的调度信息 | WaitForFirstConsumer + 无可用节点 |
两个 Pod 要共享同一份数据。PVC 用了 ceph-csi-rbd 的 StorageClass,accessMode 写 ReadWriteMany。会发生什么?
PVC Pending,describe 显示 provisioner 报 Permission denied。最该往哪个方向查?
关于 reclaimPolicy,生产环境重要数据应该怎么配?
这节课的落点
- PV 是实物、PVC 是申请、SC 是模板;动态供应把管理员从手工建 PV 中解放出来
- RWO 是单节点而非单 Pod,跨节点共享必须用支持 RWX 的文件存储
- 重要数据用
reclaimPolicy: Retain - CSI 分 controller(创建卷)和 node(挂载卷)两部分,按症状选择看哪边的日志
- PVC Pending 的排查顺序:describe 事件 → SC → provisioner 日志 → secret → 后端
Permission denied通常是 caps 限定的池与 SC 的 pool 不一致
延伸资料
- ·k8s-in-action
storage/README.md - ·k8s-in-action
storage/local-storage/