2. 容器持久化存储基础

容器天生是“无状态”的:镜像层只读,容器可写层随容器生命周期生灭。当 Pod 被删除、重建或调度到另一节点时,写在容器可写层里的数据会一并消失。要让数据库、消息队列、日志归档、AI 训练检查点这类有状态负载在 Kubernetes 上稳定运行,就必须把数据落到容器生命周期之外的持久化后端上。本节围绕 Kubernetes 的存储抽象模型展开,讲清 PV、PVC、StorageClass 三者的职责边界,以及 CSI 驱动如何把一次 PVC 声明变成后端存储上真实可用的卷。

一、Kubernetes 存储模型的三层抽象

Kubernetes 把“存储怎么用”和“存储从哪来”拆成两层,中间用声明式对象解耦。理解这三层,是理解后续所有持久化操作的基础。

  • PersistentVolume(PV):集群级别的存储资源对象,代表一块已经存在或可被供给的真实存储,例如一块 SAN LUN、一个 NFS 导出目录、一个云盘。PV 由管理员静态创建,或由供给器动态创建,它描述的是“供给方视角”的容量、访问模式、回收策略和后端标识。
  • PersistentVolumeClaim(PVC):命名空间级别的用户请求对象。用户不关心后端是哪个阵列、走什么协议,只声明“我要 100Gi、ReadWriteOnce、用 fast 这个类”。PVC 描述的是“消费方视角”的需求。
  • StorageClass(SC):把 PVC 的抽象需求翻译成具体后端供给动作的模板。它指定 provisioner(用哪个 CSI 驱动)、parameters(后端池、协议、QoS 等)、reclaimPolicy 和 volumeBindingMode。

三者关系可以概括为:PVC 是“订单”,StorageClass 是“生产规范”,PV 是“成品”,而 CSI 驱动是“生产线”。用户只下订单,系统按规范生产出成品并交付给 Pod 使用。

二、访问模式与回收策略:两个最容易踩坑的参数

PV/PVC 的 accessModes 常被误解为“权限控制”,实际上它描述的是挂载语义,而非读写权限:

访问模式含义典型后端
ReadWriteOnce (RWO)可读写,但同一时刻只能被单个节点挂载块存储(iSCSI/FC LUN、云盘)
ReadOnlyMany (ROX)只读,可被多个节点同时挂载快照克隆、只读共享卷
ReadWriteMany (RWX)可读写,可被多个节点同时挂载NFS、CephFS、部分文件存储
ReadWriteOncePod (RWOP)可读写,且只能被单个 Pod 挂载块存储(K8s 1.22+)

注意 RWO 的约束单位是“节点”而非“Pod”:同一节点上的多个 Pod 可以共享同一个 RWO 卷,但跨节点就会挂载失败。如果业务需要多副本同时写同一份数据,必须选 RWX 后端,否则会出现第二个 Pod 卡在 ContainerCreating 的状态。

回收策略 reclaimPolicy 决定 PVC 删除后 PV 的命运:Delete 会连带删除后端存储卷,适合动态供给的临时数据;Retain 保留 PV 和后端数据,PV 进入 Released 状态,需要管理员手工清理后才能复用,适合有合规留存要求的数据。生产环境对数据库类负载通常显式设置 Retain,避免误删 PVC 导致数据不可恢复。

三、CSI 驱动机制:从 in-tree 到 out-of-tree

早期 Kubernetes 把存储插件编译进核心代码(in-tree),升级驱动必须升级集群,耦合严重。CSI(Container Storage Interface)把驱动标准化为独立进程,通过 gRPC 与 kubelet 和控制器通信,实现“存储厂商发版不依赖 K8s 发版”。

一个 CSI 驱动通常由两部分组成:

  • Controller 组件:以 Deployment/StatefulSet 形式运行,包含 external-provisioner、external-attacher、external-resizer、external-snapshotter 等 sidecar,负责卷的创建、删除、挂载到节点、扩容、快照等集群级操作。
  • Node 组件:以 DaemonSet 形式运行在每个节点上,实现 NodeStageVolume、NodePublishVolume 等接口,负责把卷真正挂到节点并 bind mount 进 Pod 的目录。

CSI 定义了三组核心 RPC:Identity(驱动身份与能力上报)、Controller(卷生命周期)、Node(节点侧挂载)。Kubernetes 通过 CSIDriver、CSINode、VolumeAttachment 等对象记录驱动能力和挂载状态,调度器据此判断某个节点是否具备该驱动的挂载能力。

四、动态供给的完整链路

动态供给(Dynamic Provisioning)是当前主流方式:用户只创建 PVC,不预先创建 PV。其链路如下:

  1. 用户提交 PVC,指定 storageClassName 和容量、访问模式。
  2. PersistentVolumeController 发现 PVC 处于 Pending,且其 StorageClass 的 provisioner 不是 kubernetes.io/no-provisioner。
  3. external-provisioner 监听到该 PVC,调用 CSI 驱动的 CreateVolume,把 SC 中的 parameters 透传给后端。
  4. 后端存储完成卷创建,返回卷 ID、容量等信息。
  5. provisioner 据此创建 PV 对象,并把它与 PVC 双向绑定(claimRef / volumeName)。
  6. Pod 调度到某节点后,kubelet 调用 CSI Node 组件完成 stage 与 publish,卷出现在容器内。

如果 SC 的 volumeBindingMode 设为 WaitForFirstConsumer,则第 3 步会推迟到有 Pod 使用该 PVC 时才触发。这对拓扑受限的存储(如只能挂到特定可用区的云盘、只能访问特定控制器的 LUN)非常关键:先调度 Pod 确定节点,再在对应拓扑域创建卷,避免“卷建好了但 Pod 调度不过去”的死锁。

Kubernetes 持久化存储:声明 → 供给 → 挂载链路 用户 / 工作负载 Pod 声明 volume 引用 PVC 名称 PersistentVolumeClaim storageClassName: fast 100Gi · ReadWriteOnce StorageClass provisioner: csi.alletra WaitForFirstConsumer 匹配 控制平面 PV Controller 发现 Pending PVC external-provisioner 调用 CreateVolume external-attacher ControllerPublish Scheduler 拓扑感知调度 CSI 驱动(Controller 侧) Identity / Controller RPC CreateVolume · DeleteVolume ControllerPublish / Unpublish CreateSnapshot · ExpandVolume HPE Alletra 存储后端 卷 / LUN / 文件系统 容量池 · QoS · 快照 iSCSI / FC / NFS 协议 返回 volumeId 与拓扑信息 gRPC API 节点侧:kubelet → CSI Node 组件(NodeStageVolume / NodePublishVolume)→ bind mount 进 Pod 目录

五、一个可对照的配置示例

下面给出一个基于 CSI 驱动的 StorageClass 与 PVC 示例,参数为教学示例值,实际使用时需替换为环境中真实的驱动名与后端池名称。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: alletra-fast
provisioner: csi.alletra.com
parameters:
  pool: "SSD_Pool_01"
  protocol: "iscsi"
  fsType: "xfs"
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data
  namespace: prod
spec:
  storageClassName: alletra-fast
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi

提交后可用以下命令观察链路状态:

kubectl get pvc mysql-data -n prod -w
# 期望:Pending → Bound

kubectl get pv
# 期望:出现一个由 csi.alletra.com 动态创建的 PV,
#       CAPACITY=100Gi,CLAIM 指向 prod/mysql-data

kubectl describe pvc mysql-data -n prod
# 关注 Events 中 Provisioning / ProvisioningSucceeded 记录

判断供给是否成功的依据是:PVC 进入 Bound、PV 的 claimRef 指向该 PVC、后端存储上出现对应卷。若 PVC 长时间停留在 Pending,常见原因有三类:StorageClass 名称拼写不一致、provisioner 未运行或未注册、后端容量池无可用空间。此时应优先查看 external-provisioner 的日志,而不是反复重建 PVC。

扩容方面,只有 StorageClass 开启 allowVolumeExpansion: true 且 CSI 驱动实现了 ControllerExpandVolume 时,才能通过编辑 PVC 的 spec.resources.requests.storage 在线扩容。文件系统扩容通常需要重启 Pod 才能生效,块设备扩容则可在挂载状态下完成。
RWO 卷跨节点挂载会失败,多副本 Deployment 若共享同一 RWO PVC,第二个副本会卡在 ContainerCreating。需要多副本共享写入时,应改用 RWX 后端,或为每个副本使用独立的 PVC(StatefulSet 的 volumeClaimTemplates 正是为此设计)。

把上述对象关系串起来看:PVC 表达需求,StorageClass 定义供给规范,CSI 驱动执行真实的后端操作,PV 是绑定后的成品,kubelet 与 CSI Node 组件完成最后一公里的挂载。掌握这条链路,后续对接 HPE Alletra 的 CSI 驱动、配置动态供给与快照保护时,就能准确定位每一步的输入、输出与排障入口。