K8s 的存储体系有三个核心概念:
- PV 是什么?为什么要手动创建?
- PVC 又是什么?和 PV 什么关系?
- StorageClass 怎么做到自动创建 PV?
很多人分不清它们谁是谁、怎么配合。这篇文章一次讲透。
一、先看懂三个角色
K8s 把存储抽象成了一套「供需匹配」模型,类比租房市场特别好理解:
| 角色 | K8s 概念 | 租房类比 | 谁创建的 |
|---|---|---|---|
| 房源 | PV(PersistentVolume) | 可出租的房子 | 管理员 / StorageClass |
| 求租 | PVC(PersistentVolumeClaim) | 我要一套两室一厅 | 开发者(写在 Pod 配置里) |
| 中介 | StorageClass | 房产中介,按需建房 | 管理员 |
关键设计理念:开发人员不需要知道底层存储是什么。你只要声明"我要 10Gi 存储",剩下的 K8s 自动匹配。
yaml
[root@master-1 ~]# cat pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim # 资源类型为持久卷声明(PVC)
metadata:
name: nginx-pvc # PVC 对象名称,需在命名空间内唯一
spec:
storageClassName: "" # 空字符串表示禁用动态存储,只匹配静态 PV
accessModes:
- ReadWriteMany
resources: # 声明存储资源需求
limits:
storage: 20G # 存储资源上限(不可超过 PV 容量)
requests:
storage: 10Gi # 实际请求的存储空间(PV 需满足此值)
至于这 10Gi 来自 NFS、Ceph、还是云盘,开发者不用关心。
二、PV 和 PVC 是怎么绑定的
1. 静态供给:管理员先建好 PV
yaml
[root@master-1 ~]# cat pv.yaml
# 管理员提前建好一个 20Gi 的 NFS PV
apiVersion: v1
kind: PersistentVolume #资源类型为持久卷(PV)
metadata:
name: nginx-pv
spec:
accessModes:
- ReadWriteMany #适合 NFS 共享存储
# 声明存储卷的类型为nfs
nfs:
path: /ifs/kubernetes/data/pv001
server: 192.168.91.19
persistentVolumeReclaimPolicy: Retain
# 声明存储的容量
capacity:
storage: 20Gi
当 PVC 请求 10Gi ReadWriteMany 时,K8s 会在所有 PV 里找一个「容量够 + 访问模式匹配」的,绑上去。
绑定的规则很死:
- 容量:PV 容量 ≥ PVC 请求(20Gi 的 PV 可以给 10Gi 的 PVC,反之不行)
- 访问模式:必须兼容(RWO → RWO,RWX → RWX,RWO 不能给 ROX)
- 一对一:一个 PV 只能绑一个 PVC,绑完其他 PVC 就看不到了
bash
[root@master-1 ~]# kubectl apply -f pv.yaml
persistentvolume/nginx-pv created
[root@master-1 ~]# kubectl apply -f pvc.yaml
persistentvolumeclaim/nginx-pvc created
# 查看绑定状态
[root@master-1 ~]# kubectl get pv,pvc

2. 动态供给:StorageClass 自动化建 PV
静态供给的问题是:管理员得提前把所有规格 PV 建好,太累了。
StorageClass 解决的就是这个:当 PVC 来了,自动帮你建 PV。
yaml
# 定义一个 StorageClass
apiVersion: storage.k8s.io/v1
# 指定使用的 Kubernetes API 版本,storage.k8s.io/v1 是存储相关资源的稳定版
kind: StorageClass
metadata:
name: managed-nfs-storage # StorageClass 的名称,PVC 可以通过指定这个名字来使用该存储类
annotations:
storageclass.kubernetes.io/is-default-class: "true"
# 关键注解:将当前 StorageClass 设置为集群的**默认存储类**。
# 当 PVC 未指定 storageClassName 时,会自动使用这个默认类来动态创建 PV。
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner
# 指定存储供应商(provisioner)的名称。
# 这里使用的是 NFS 子目录外部供应器,它会根据 PVC 请求在 NFS 服务器上自动创建子目录并绑定。
parameters:
archiveOnDelete: "false"
# 参数:当 PVC 被删除时,是否自动归档(保留)对应的后端存储数据。
# false 表示直接删除 NFS 上对应的子目录及数据;true 表示会重命名(归档)而不删除。
reclaimPolicy: Delete
# PV 的回收策略。Delete 表示当 PVC 被释放后,动态创建的 PV 和其后端存储(如 NFS 子目录)会被一起删除。
# 其他可选值有 Retain(保留数据供手动处理)。
volumeBindingMode: Immediate
# 卷绑定模式。Immediate 表示 PVC 创建时立即进行绑定,不考虑 Pod 调度约束。
# 另一个常见值是 WaitForFirstConsumer,会等待第一个使用该 PVC 的 Pod 被调度后再绑定,以感知节点可用区等。
然后在 PVC 里引用它:
yaml
[root@master-1 ~]# cat pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim # 资源类型为持久卷声明(PVC)
metadata:
name: nginx-pvc # PVC 对象名称,需在命名空间内唯一
spec:
storageClassName: managed-nfs-storage # 指定用哪个 StorageClass
accessModes:
- ReadWriteMany
resources: # 声明存储资源需求
limits:
storage: 20G # 存储资源上限(不可超过 PV 容量)
requests:
storage: 10Gi # 实际请求的存储空间(PV 需满足此值)
[root@master-1 ~]# kubectl apply -f pvc.yaml
K8s 收到这个 PVC,自动创建一个 PV 并绑定。全程管理员零介入。

三、一张图看清完整链路
[Pod] → 引用 PVC → PVC 找 PV → PV 对接实际存储(NFS/Ceph/云盘)
↑
StorageClass 自动创建 PV(动态供给)

写成配置就是三层调用:
yaml
# Layer 1: Pod 引用 PVC
spec:
volumes:
- name: data
persistentVolumeClaim:
claimName: my-pvc # 指向 PVC
# Layer 2: PVC 引用 StorageClass
spec:
storageClassName: fast-ssd
resources:
requests:
storage: 20Gi
# Layer 3: StorageClass 定义后端供给
provisioner: disk.csi.azure.com # 或 NFS / Ceph / 云盘 CSI
四、三个必须知道的关键参数
1. accessModes(访问模式)
| 模式 | 含义 | 典型场景 |
|---|---|---|
| ReadWriteOnce (RWO) | 单节点读写 | 数据库(MySQL、PostgreSQL) |
| ReadOnlyMany (ROX) | 多节点只读 | 静态资源、配置文件分发 |
| ReadWriteMany (RWX) | 多节点读写 | 共享文件存储、日志采集 |
注意:RWO 不等于单 Pod。同一 Node 上多个 Pod 都可以读写同一个 RWO 卷。
2. reclaimPolicy(回收策略)
yaml
reclaimPolicy: Retain # 推荐:删除 PVC 后 PV 保留,手动清理
reclaimPolicy: Delete # 危险:删除 PVC 后自动删底层存储
reclaimPolicy: Recycle # 已废弃:rm -rf 后重新可用
生产环境建议一律用 Retain。Delete 策略在测试环境省事,但在生产上一个误删 PVC 就可能导致数据永久丢失。
3. volumeBindingMode(卷绑定模式)
yaml
volumeBindingMode: Immediate # PVC 一创建就绑(可能跨可用区)
volumeBindingMode: WaitForFirstConsumer # 等 Pod 调度后再绑(推荐)
WaitForFirstConsumer 的好处:PV 创建的云盘和 Pod 在同一个可用区,避免跨 AZ 延迟。云环境下强烈推荐。
五、排坑实战:最常见的 3 个问题
1. PVC 一直 Pending
bash
[root@master-1 ~]# kubectl get pvc nginx-pvc
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
nginx-pvc Pending managed-nfs-storage1 53s
排查三步走:
bash
# 1. 看 PVC 详情,最下面会告诉你原因
kubectl describe pvc nginx-pvc
# 2. 常见报错:
# "no persistent volumes available" → 没有匹配的 PV 或 StorageClass 没配
# "storageclass.storage.k8s.io "standard" not found" → StorageClass 名称写错了
# 3. 看 StorageClass 是否存在
kubectl get storageclass

2. Pod 挂载超时,一直 ContainerCreating
bash
kubectl describe pod nginx
# Events:
# Warning FailedMount Unable to attach or mount volumes
通常是两个原因:
- 云盘跨可用区挂载 :Pod 在 Zone A,云盘在 Zone B → 用
WaitForFirstConsumer - NFS 服务器不可达:检查 NFS Server IP 和防火墙
3. 删了 PVC,数据还在吗?
取决于 reclaimPolicy:
| 策略 | 删 PVC | 删 PV | 云盘/实际存储 |
|---|---|---|---|
| Retain | PV 变 Released | 需手动删 | 保留 ✅ |
| Delete | PV 被删 | 自动删 | 一起删 ⚠️ |
所以在 Delete 策略下,谁手滑删了 PVC 谁就可能背锅。
六、总结
一句话:PV 是管理员准备的存量房,PVC 是开发者的租房请求,StorageClass 是自动建房的中介。
落地清单:
| # | 动作 | 为什么 |
|---|---|---|
| 1 | 生产环境配 StorageClass,不要手动建 PV | 减少人工操作,避免规格不匹配 |
| 2 | reclaimPolicy 用 Retain | 防止误删 PVC 导致数据丢失 |
| 3 | volumeBindingMode 用 WaitForFirstConsumer | 避免云盘跨可用区挂载 |
| 4 | PVC 命名规范:{app}-{环境}-data |
一眼认出是谁的卷 |
| 5 | 定期 `kubectl get pv | grep Released` |
K8s 存储说难不难,三个概念搞清楚了,剩下的就是选好 CSI 驱动、配好回收策略。出问题的时候,对着 PV → PVC → StorageClass 这条链逐层排查,基本都能定位。
你是手动建 PV 还是用 StorageClass 动态供给?评论区聊聊你的生产实践。