Kubernetes持久化存储怎么配置?PV、PVC与StorageClass教程

容器的文件系统是「用完即弃」的:Pod 重建,写在容器里的数据就没了。这对无状态服务没问题,但数据库、消息队列、文件服务这类有状态应用,数据必须活得比 Pod 久。Kubernetes 为此设计了一套存储抽象——PV、PVC、StorageClass,把「存储资源」和「使用存储的声明」解耦开。

初学这套模型最容易懵:既然有 PVC,为什么还要 PV?StorageClass 又是干什么的?动态供给和静态供给到底差在哪?为什么 PVC 一直 Pending?这些问题不搞清楚,配持久化存储就是靠试错。

这篇文章会从「为什么需要抽象」讲起,把 PV、PVC、StorageClass 三者的关系和生命周期讲透,给出静态供给和动态供给两套完整可用的 YAML,覆盖访问模式(AccessModes)、回收策略(ReclaimPolicy)、卷扩容等关键配置,最后附上常见故障排查和选型建议。

一、为什么需要这层抽象

直接想一下:如果 Pod 里直接写「挂载 /dev/sdb1」,会发生什么?
- 应用和具体存储设备强绑定,换集群、换云就要改配置;
- 存储的分配、回收、权限全靠人管,无法自动化;
- 多个 Pod、多个团队争抢存储时没有统一的分配机制。

Kubernetes 的解法是引入两层间接:
- PV(PersistentVolume):集群层面的存储资源,代表「一块真实的存储」,由管理员或 provisioner 创建,独立于任何 Pod;
- PVC(PersistentVolumeClaim):用户(开发者)层面的存储请求,声明「我要多大、什么访问模式」,不关心底层是哪块盘;
- StorageClass:存储的「模板/类别」,定义用哪个 provisioner、什么参数,让 PVC 可以按需自动创建 PV。

用一句话概括关系:PVC 是「需求」,PV 是「供给」,StorageClass 是「按需求自动供给的规则」。Pod 只引用 PVC,PVC 通过绑定(Bind)关联到某个 PV。

二、PV 与 PVC 的关键字段

一个静态创建的 PV 长这样:

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

对应的 PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pvc-app
  namespace: default
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: manual
  resources:
    requests:
      storage: 10Gi

绑定规则:PVC 会去寻找「容量 ≥ 请求容量、accessModes 匹配、storageClassName 相同」的 PV。匹配成功后,两者状态都变成 Bound。

几个必须搞懂的字段:

AccessModes(访问模式):

模式 缩写 含义 适用存储
ReadWriteOnce RWO 单节点读写挂载 块存储(云盘)
ReadOnlyMany ROX 多节点只读 共享文件存储
ReadWriteMany RWX 多节点读写 NFS、CephFS、对象存储挂载
ReadWriteOncePod RWOP 单 Pod 读写 较新特性,块存储

注意 RWO 是「单节点」而非「单 Pod」,同一节点上多个 Pod 可以共用同一个 RWO 卷。

ReclaimPolicy(回收策略):
- Retain:PVC 删除后保留 PV 和数据,需人工清理,生产数据库强烈建议用这个;
- Delete:PVC 删除后自动删除 PV 及底层存储(动态供给默认),小心数据丢失;
- Recycle:已废弃,不要用。

storageClassName:留空表示不绑定任何 StorageClass;写 "" 表示显式要求静态供给;写具体名字则走该类的动态供给。

三、动态供给:生产推荐方式

动态供给(Dynamic Provisioning)让 PVC 一提交,StorageClass 对应的 provisioner 就自动创建 PV,无需管理员手工建卷。

1. 创建 StorageClass

以云盘为例(参数名因云厂商而异,以下为示意,具体以官方文档为准):

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: diskplugin.csi.example.com
parameters:
  type: ssd
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

关键选项:
- reclaimPolicy: Retain:删除 PVC 后保留数据,避免误删;
- allowVolumeExpansion: true:允许后续扩容 PVC;
- volumeBindingMode: WaitForFirstConsumer:延迟绑定,等 Pod 调度后再创建卷,避免可用区不匹配导致卷和节点不在同一区——多可用区集群务必用这个。

2. 创建 PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: fast-ssd
  resources:
    requests:
      storage: 50Gi

3. Pod 中挂载

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 临时分配一个生命周期跟随 Pod 的卷,用 volumeClaimTemplates(StatefulSet 特有):

volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast-ssd
      resources:
        requests:
          storage: 50Gi

StatefulSet 会为每个副本生成独立的 PVC(如 data-mysql-0、data-mysql-1),这一点对有状态应用至关重要。

四、静态供给 vs 动态供给

维度 静态供给 动态供给
PV 创建方式 管理员手工创建 provisioner 自动创建
灵活性 低,需预先规划 高,按需分配
运维成本 高 低
适用场景 特殊存储、外部已有卷 绝大多数生产场景
典型存储 NFS 手工挂载、已有云盘 云盘 CSI、Ceph RBD

现代集群基本都用动态供给。静态供给主要出现在两类场景:接入已有的 NFS/共享存储,或使用没有 CSI 驱动支持的老设备。

五、扩容与迁移

PVC 扩容需要满足:StorageClass 开启了 allowVolumeExpansion,且底层存储支持在线扩容。修改 PVC 的 requests.storage 即可:

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

风险提示:扩容只能增大不能缩小,且部分存储需要重启 Pod 才能生效(文件系统扩展)。操作前务必确认存储后端支持,并备份数据。回滚方式有限——已扩的容量通常无法缩回,只能新建较小的卷迁移数据。

跨存储迁移常用思路是「新 PVC + 一次性拷贝 Pod」:

# 用临时 Pod 挂载新旧两个卷,rsync 迁移
kubectl run migrate --restart=Never --image=alpine \
  --overrides='{"spec":{"containers":[{"name":"m","image":"alpine","command":["sh","-c","sleep 3600"],"volumeMounts":[{"name":"old","mountPath":"/old"},{"name":"new","mountPath":"/new"}]}],"volumes":[{"name":"old","persistentVolumeClaim":{"claimName":"mysql-data"}},{"name":"new","persistentVolumeClaim":{"claimName":"mysql-data-new"}}]}}'

风险提示:迁移前先把应用停下(缩到 0 副本),确保数据静止,否则会丢增量数据。迁移完成并校验无误后再删旧卷。

六、常见故障排查

PVC 一直 Pending。逐项检查:

kubectl describe pvc mysql-data
kubectl get storageclass
kubectl get pv
  • 静态供给:没有匹配的 PV(容量/模式/类名不符);
  • 动态供给:StorageClass 不存在或 provisioner 没运行(看 kubectl get pods -A | grep -i csi);
  • WaitForFirstConsumer 模式下,PVC 在没有 Pod 使用时本就该是 Pending,属正常。

Pod 挂载失败。kubectl describe pod 里看到 FailedMount,常见原因是可用区不匹配(卷在一区、Pod 调度到二区)、访问模式冲突(RWO 卷想被两节点挂载)、或存储后端网络不通。

volume 删除后数据还在。这是 Retain 策略的正常表现。清理步骤:

kubectl delete pvc mysql-data
# 数据仍在底层,PV 处于 Released 状态,需人工处理
kubectl get pv | grep Released

要恢复使用时,需手动清理 PV 的 claimRef 再重新绑定,或直接删 PV(视底层存储而定)。

风险提示:删除 PV 或底层卷是不可逆操作,务必确认数据已备份。回滚几乎不可能,操作前用 kubectl get pv -o yaml 留档。

查看所有卷占用:

kubectl get pvc -A
kubectl get pv
kubectl get pod -A -o json | grep -i persistentVolumeClaim

七、选型与容量规划建议

按存储类型给一些经验方向:

  • 云上块存储(云盘):适合数据库等 RWO 场景,延迟低,性能稳定;容量按数据增长预留 30% 到 50% 余量。
  • 共享文件存储(NFS/CephFS):适合多 Pod 共享读写的 RWX 场景,如共享上传目录、日志汇集;性能受网络影响较大。
  • 本地盘(Local PV):极低延迟,适合缓存、搜索索引,但不具备高可用,节点故障数据不可访问,需应用层做副本。
  • 对象存储:适合海量非结构化数据;通常通过 CSI 或应用 SDK 访问,不适合做主数据库。

容量估算经验值:数据库类卷建议初始按「未来一年预估数据的 1.5 到 2 倍」规划;日志类按日增量的 7 到 30 倍规划;不确定时优先用支持在线扩容的 StorageClass。

高可用方面:单副本存储一旦后端故障,数据可能短暂不可用。数据库应通过主从复制、备份策略来保障,不要只依赖存储本身。

还要强调一点被反复忽视的实践:持久化存储最怕的不是性能,而是「以为数据安全」。很多事故的根因是团队默认「卷是持久的、不会丢」,于是没有做备份、没有演练恢复,直到某次误删 PVC、误删命名空间或者存储后端故障时才发现数据取不回来。稳妥的做法是把「备份 + 恢复演练」纳入存储上线的必选动作:明确哪些卷必须备份、备份到哪里、保留多久、多久恢复演练一次。备份目标不要放在同一个存储后端,否则后端整体故障时备份和源数据一起丢。恢复演练至少要覆盖「从备份重建 PVC 并让应用重新挂载」这条完整路径,只在文档里写下步骤而从不实操,等于没有备份。

常见问题

Q1:PVC 的 storageClassName 留空会怎样?
A:会使用集群默认的 StorageClass(若设置了默认类);如果集群没有默认类,且没有匹配的静态 PV,PVC 会一直 Pending。

Q2:RWO 的卷能被多个 Pod 用吗?
A:若这些 Pod 在同一节点上可以;跨节点不行。跨节点共享要用 RWX 类型的存储(如 NFS、CephFS)。

Q3:删了 PVC,数据会丢吗?
A:取决于 PV 的 reclaimPolicy。动态供给默认是 Delete,会连底层存储一起删;生产建议改成 Retain,删 PVC 后数据保留,手动确认后再清理。

Q4:StatefulSet 的 PVC 会随 Pod 删除吗?
A:不会。StatefulSet 用 volumeClaimTemplates 创建的 PVC 在删除 Pod(甚至删除 StatefulSet)后依然保留,需要手工删除,这也是为了数据安全。

Q5:扩容 PVC 后 Pod 里的文件系统没变大。
A:部分存储需要重启 Pod 触发文件系统扩展。用 kubectl describe pvc 看是否有 FileSystemResizePending 事件,必要时 kubectl delete pod 让 Pod 重建。

总结

  1. 记住三者定位:PV 是存储资源,PVC 是使用声明,StorageClass 是自动供给的模板;Pod 只引用 PVC。
  2. 生产优先用动态供给,一个 StorageClass 配好 provisioner、Retain 回收策略和 WaitForFirstConsumer 绑定模式,后续按需申请。
  3. AccessModes 要选对:数据库等单节点场景用 RWO,多 Pod 共享用 RWX;RWO 是「单节点」不是「单 Pod」。
  4. 有状态应用用 StatefulSet + volumeClaimTemplates,每个副本独立卷,扩容与迁移前先停应用、先备份。
  5. 删除 PVC/PV、缩容、迁移都是不可逆或高风险操作,务必留好 Retain 策略和备份,操作前 kubectl get pv -o yaml 留档。