K8s 持久化存储:PV/PVC 与 StorageClass

在 Kubernetes 中,Pod 本身是易逝的——节点重启、调度漂移、容器崩溃都会让容器内的文件系统灰飞烟灭。要把数据库、日志、用户上传文件这类有状态数据真正留存下来,就必须依赖 K8s 持久化存储体系:PV(PersistentVolume)描述底层存储资源,PVC(PersistentVolumeClaim)是应用对存储的请求,而 StorageClass 则定义了动态供给的模板。本文从三者关系讲起,带你把无状态集群改造成能安全承载有状态负载的生产环境。

一、为什么需要持久化存储:Pod 的 ephemeral 本质

Kubernetes 的设计哲学是无状态优先——这与 K8s 入门:Pod、Deployment、Service 里讲的无状态编排一脉相承。默认情况下,容器写入自身可写层的任何数据,在 Pod 被删除或重新调度后都会永久丢失。唯一随 Pod 生命周期存在的是 emptyDir 卷,它适合缓存、临时计算中间结果,却无法跨 Pod 存活。

更严格地说,Kubernetes 为有状态负载准备了专门的 StatefulSet 控制器:它给每个副本分配固定的网络标识与各自独立的持久卷,副本重建后卷与标识一一对应,这正是 MySQL 主从、Elasticsearch 集群能稳定运行的底座;与之相对,Deployment 更适合无状态的 Web 服务。无论用哪种控制器,真正把数据落到磁盘的,始终是下面要讲的 PV/PVC 机制。

凡是具备以下特征的工作负载,都必须挂载持久卷:

• 数据库(如 MySQL 主从复制与读写分离MongoDB 复制集与分片集群):数据文件是真相来源,丢失即事故;
• 消息队列与搜索引擎(Kafka、Elasticsearch):分段与索引不能随 Pod 蒸发;
• 用户上传、日志归档、模型权重:需要跨重启保留。

apiVersion: v1
kind: Pod
metadata:
  name: ephemeral-demo
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: cache
      mountPath: /tmp/cache
  volumes:
  - name: cache
    emptyDir: {}

上面这个 emptyDir 在 Pod 运行期间可用,但 Pod 一旦重建,/tmp/cache 里的内容就归零——这就是必须引入 PV/PVC 的根本原因。

二、PV 与 PVC:供给模型的两层抽象

2.1 PV:集群级的存储资源

PersistentVolume 是集群管理员预先置备的存储资源,生命周期独立于任何 Pod。它屏蔽了底层 NFS、云盘、Ceph 等实现的差异,向上提供统一的capacity、accessModes、reclaimPolicy 等语义。

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs-01
spec:
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: nfs-csi
  nfs:
    server: 10.0.0.20
    path: /data/k8s

其中 persistentVolumeReclaimPolicy 决定 PVC 被删除后 PV 的归宿:Retain 保留数据等待人工回收(生产推荐),Delete 则连同后端存储一起清掉;storageClassName 为空表示这是一个“不被动态供给绑定”的静态卷。

2.2 PVC:开发者的存储请求

开发者不需要关心存储背后是 NFS 还是云盘,只需用 PVC 声明“我要多大、什么访问模式、哪类存储”。Kubernetes 的绑定控制器会按 storageClassName 与 accessModes 找到最匹配的可用 PV 并一对一绑定。

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: nfs-csi
  resources:
    requests:
      storage: 20Gi

把 PVC 挂进 Pod 同样是标准写法:在 volumes 里引用 persistentVolumeClaim.claimName,再在容器 volumeMounts 指定挂载路径。

apiVersion: v1
kind: Pod
metadata:
  name: mysql
spec:
  containers:
  - name: mysql
    image: mysql:8.0
    volumeMounts:
    - name: data
      mountPath: /var/lib/mysql
  volumes:
  - name: data
    persistentVolumeClaim:
      claimName: mysql-data

绑定完成后,Pod 写入 /var/lib/mysql 的数据就落到了 NFS 的 /data/k8s,Pod 无论怎么重建都不会丢。

三、StorageClass:动态供给的引擎

如果集群里只有静态 PV,每来一个 PVC 管理员就要手动建一块 PV,规模一大就不可持续。StorageClass 的价值在于:它定义了“供给模板”和对应的 CSI provisioner,当 PVC 指定了某个 StorageClass 且集群存在该驱动时,Kubernetes 会自动 Provision 出一块 PV 并完成绑定——这就是动态供给。

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
  server: 10.0.0.20
reclaimPolicy: Delete
allowVolumeExpansion: true

关键字段有三点:provisioner 指向具体的 CSI 驱动;reclaimPolicy 规定动态卷被释放后的默认行为;allowVolumeExpansion: true 则允许后续在线扩容。有了它,开发者只需提交 PVC,PV 会自动从天而降。

维度静态供给动态供给(StorageClass)
运维动作管理员手动创建 PVCSI 驱动自动 Provision
扩展方式需重建 PV 重新绑定改 PVC 大小即可在线扩容
适用场景小集群、存量存储多租户、大规模、云原生

四、访问模式与生产选型

accessModes 决定了卷能被多少个节点以什么权限挂载,选错会直接导致 PVC 长期处于 Pending。三个模式对应不同后端能力:

访问模式缩写含义典型后端
ReadWriteOnceRWO单节点读写云盘、本地盘、NFS
ReadOnlyManyROX多节点只读NFS、对象存储
ReadWriteManyRWX多节点读写NFS、CephFS

一个经典坑:把 RWX 需求的共享目录挂到只支持 RWO 的云盘上,PVC 会一直 Pending 且事件里只提示“no volume plugin matched”。需要多副本共享同一份数据时(如训练集、静态资源),务必选 NFS/CephFS 这类支持 RWX 的后端。

五、动态扩容、快照与回收实战

5.1 在线扩容

只要 StorageClass 开了 allowVolumeExpansion,扩容就是改 PVC 的 requests.storage 再 apply,文件系统随后由 CSI 驱动在线扩展,业务无感知。

kubectl patch pvc mysql-data -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'

5.2 快照备份

CSI 快照需要集群安装了 external-snapshot 控制器。通过 VolumeSnapshot 可以基于某个 PVC 打点,再由其克隆出新卷,是做数据库小时级备份的轻量手段。

apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: mysql-snap-01
spec:
  volumeSnapshotClassName: csi-nfs-snapclass
  source:
    persistentVolumeClaimName: mysql-data

六、生产排错清单(踩坑向)

持久化存储的故障大多集中在“绑不上、挂不稳、删错盘”三类,按下面顺序排查效率最高:

kubectl get pvc -o wide
kubectl describe pvc mysql-data
kubectl get events --sort-by=.lastTimestamp

• PVC 一直 Pending:先 describe 看 Events,常见原因是没有匹配 storageClassName 的 provisioner,或底层存储配额耗尽;
• 节点宕机后 PV 无法重新挂载:RWO 卷被原节点占用,需等原节点 NotReady 超时或手动删除引用;
• 误删 PVC 导致数据丢失:若 StorageClass 的 reclaimPolicy 是 Delete,后端数据会一并清除,重要数据务必改用 Retain 或在 SC 层覆盖。

一个实用的经验是:给关键 PVC 打上 team、app 等标签,并配合监控对 PV 使用率做阈值告警,避免“磁盘悄悄写满后 Pod 起不来”的凌晨事故。同时把 reclaimPolicy 的选择写进团队的存储规范,让开发在申请 PVC 时就明确“这份数据是否允许随 PVC 一起被删”。

七、权限与安全

挂载卷后常遇到“容器内进程无权限写目录”,根因多是宿主机与容器 UID 不一致。通过 Pod 的 GitHub Actions 实战 做镜像构建与推送时,也应把有状态配置与密钥分离;而 Helm 实战 能把这些存储模板打包成可复用 Chart。 统一归属组即可,避免随意 chmod 777。另外,数据库密码、密钥等敏感信息务必走 Secret 挂载,而不是写进 ConfigMap 或镜像。

八、总结

回顾三者关系:PV 是“被提供的存储”,PVC 是“对存储的请求”,StorageClass 是“自动提供的规则”。掌握这套抽象,你就能在 Kubernetes 上安全地运行 MySQL、Redis、Elasticsearch 等有状态负载,并通过动态供给、在线扩容、快照把运维成本压到最低。建议新集群默认配置好 CSI 驱动与 StorageClass,让有状态应用的上线像无状态 Deployment 一样顺滑。

上一篇 Web Components 实战:从零封装可复用组件