K8s 存储核心:一文读懂 PV 和 PVC 的设计逻辑与实践
在 Kubernetes 中,Pod 是 “临时的”—— 重启、重建后数据会丢失;而数据库、日志这类应用需要 “持久化存储”(数据保存在磁盘,不受 Pod 生命周期影响)。但直接让 Pod 对接物理磁盘或云存储,会导致 “存储与应用强耦合”(比如换个云厂商,存储配置全要改)。
PV(Persistent Volume,持久卷)和 PVC(Persistent Volume Claim,持久卷声明)就是为解决这个问题而生的 —— 它们通过 “解耦存储与应用”,让开发者不用关心底层存储细节,运维人员统一管理存储资源。下面从 “是什么→怎么协作→怎么用” 三个维度,彻底搞懂 PV 和 PVC。
一、先搞懂:PV 和 PVC 分别是什么?
PV 和 PVC 的关系,类似 “商品” 和 “订单”—— 运维人员提前准备好 “商品”(PV,存储资源),开发者提交 “订单”(PVC,存储需求),K8s 负责 “匹配订单与商品”。
1. PV:集群级的 “存储资源池”
- 定义:PV 是 Kubernetes 集群中的 “持久化存储资源”,由运维人员创建,对应底层实际的存储设备(如本地磁盘、云硬盘 EBS、NFS 共享存储)。
- 核心属性:
- 存储容量(capacity.storage):如 10Gi、100Gi;
- 存储类型(storageClassName):给 PV 打 “标签”,方便 PVC 按类型匹配(如 “fast” 对应 SSD,“slow” 对应 HDD);
- 访问模式(accessModes):定义 PV 能被如何使用,常见三种:
访问模式 | 含义 | 适用场景 |
ReadWriteOnce (RWO) | 仅允许一个节点以 “读写” 方式挂载 | 数据库(如 MySQL,避免多节点同时写导致数据混乱) |
ReadOnlyMany (ROX) | 允许多个节点以 “只读” 方式挂载 | 静态资源共享(如 Nginx 挂载静态网页目录) |
ReadWriteMany (RWX) | 允许多个节点以 “读写” 方式挂载 | 分布式应用(如 GlusterFS、Ceph 共享存储) |
- 回收策略(persistentVolumeReclaimPolicy):PV 释放后如何处理数据,常见三种:
- Retain(保留):数据保留,需运维人员手动清理(适合重要数据,避免误删);
- Delete(删除):PV 被释放后,自动删除底层存储(如云硬盘 EBS 会被删除,适合测试环境);
- Recycle(回收):清空数据后 PV 重新进入资源池(已废弃,推荐用 Retain 或 Delete)。
- 示例 PV 配置(NFS 类型存储):
apiVersion: v1
kind: PersistentVolume
metadata:
name: nfs-pv-10gi # PV 名称
spec:
capacity:
storage: 10Gi # 容量 10Gi
accessModes:
- ReadWriteMany # 支持多节点读写
storageClassName: nfs-storage # 存储类型标签
nfs: # 底层存储为 NFS
server: 192.168.1.100 # NFS 服务器 IP
path: /nfs/share/data # NFS 共享目录
persistentVolumeReclaimPolicy: Retain # 释放后保留数据
2. PVC:Pod 的 “存储需求订单”
- 定义:PVC 是 Pod 对存储资源的 “需求声明”,由开发者创建,描述 Pod 需要的存储容量、类型、访问模式等,不关心底层是 NFS 还是云硬盘。
- 核心逻辑:PVC 不直接对接存储设备,而是 “请求匹配” 集群中符合条件的 PV—— 匹配成功后,PVC 与 PV 绑定,Pod 再通过 PVC 挂载存储。
- 示例 PVC 配置(请求 10Gi NFS 存储):
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-pvc-10gi # PVC 名称
spec:
accessModes:
- ReadWriteMany # 需与 PV 的访问模式匹配
resources:
requests:
storage: 10Gi # 请求 10Gi 容量(需 ≤ PV 容量)
storageClassName: nfs-storage # 需与 PV 的存储类型匹配(精确匹配)
二、核心关系:PV 与 PVC 的 “绑定逻辑”
PV 和 PVC 不是 “手动指定绑定”,而是由 Kubernetes 自动匹配,核心遵循 “3 个匹配条件”,缺一不可:
- 存储类型匹配:PVC 的 storageClassName 必须与 PV 的 storageClassName 完全一致(若都为 "",表示 “无类型”,可互相匹配);
- 访问模式兼容:PVC 请求的 accessModes 必须是 PV 支持的 accessModes 子集(如 PV 支持 RWO+RWX,PVC 请求 RWX 是兼容的);
- 容量满足:PVC 请求的 storage 必须 ≤ PV 的 capacity.storage(如 PV 是 10Gi,PVC 请求 8Gi 可行,请求 12Gi 不可行)。
绑定后的特点:
- 一对一绑定:一个 PV 只能绑定一个 PVC,一个 PVC 也只能绑定一个 PV(绑定后 PV 状态变为 Bound,不再接受其他 PVC 请求);
- 生命周期独立:PV 是集群级资源(不属于任何命名空间),PVC 是命名空间级资源(仅在所属命名空间内可见)—— 这意味着,不同命名空间的 PVC 不能绑定同一个 PV。
三、生命周期:从 “创建” 到 “释放” 的完整流程
PV 和 PVC 的生命周期分为 5 个阶段,清晰反映了资源的流转过程:
- Provisioning(供给):运维人员创建 PV(静态供给),或通过 StorageClass 自动创建 PV(动态供给,后面补充);
- Binding(绑定):开发者创建 PVC,K8s 匹配符合条件的 PV,绑定后两者状态均变为 Bound;
- Using(使用):Pod 通过 volumes 字段引用 PVC,挂载 PV 对应的存储,开始读写数据(Pod 运行期间,PVC 与 PV 持续绑定,不可解绑);
- Releasing(释放):删除 PVC(或 Pod 被删除且 PVC 无其他引用),PVC 状态变为 Released,PV 状态也变为 Released(此时 PV 中的数据按回收策略处理);
- Reclaiming(回收):根据 PV 的 persistentVolumeReclaimPolicy 处理:
- Retain:PV 状态变为 Available,数据保留,需运维人员手动清理数据后,PV 才能重新被绑定;
- Delete:PV 自动删除,底层存储(如 EBS)也会被删除,资源彻底释放;
- Recycle(废弃):自动清空数据,PV 状态变回 Available,可重新绑定。
生命周期状态流转图(简化版):
PV:Provisioning(创建)→ Available(可用)→ Bound(绑定)→ Released(释放)→ Reclaimed(回收)
PVC:Pending(等待匹配)→ Bound(绑定)→ Released(释放)→ Terminating(删除中)
四、进阶:动态供给(StorageClass)—— 自动创建 PV
手动创建 PV 存在 “效率低”“资源浪费” 的问题(比如开发者需要 5Gi 存储,运维人员得提前创建 5Gi PV,否则 PVC 会一直 Pending)。
StorageClass(存储类) 解决了这个问题 —— 它允许 K8s 根据 PVC 的需求,自动创建 PV,无需运维人员手动干预。
核心逻辑:
- 运维人员创建 StorageClass,定义 “如何动态生成 PV”(如底层用云硬盘 EBS、NFS,以及 PV 的回收策略、访问模式等);
- 开发者创建 PVC 时,指定 storageClassName 为该 StorageClass 名称;
- K8s 检测到 PVC 请求后,调用 StorageClass 对应的 “存储插件”(如云厂商提供的插件),自动创建 PV 并与 PVC 绑定。
示例:用 StorageClass 动态创建 NFS 类型 PV
- 创建 StorageClass(需提前部署 NFS 存储插件):
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: dynamic-nfs-sc # StorageClass 名称
provisioner: k8s-sigs.io/nfs-subdir-external-provisioner # NFS 存储插件
parameters:
server: 192.168.1.100 # NFS 服务器 IP
path: /nfs/dynamic # NFS 动态存储根目录
onDelete: retain # 删除 PVC 时,保留 NFS 目录(避免数据丢失)
reclaimPolicy: Retain # 动态创建的 PV 回收策略
allowVolumeExpansion: true # 允许 PVC 扩容(需底层存储支持)
- 创建 PVC 触发动态 PV:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: dynamic-pvc-5gi
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 5Gi # 请求 5Gi 容量
storageClassName: dynamic-nfs-sc # 指定 StorageClass,触发动态创建 PV
- 验证结果:
执行 kubectl get pv 会发现,K8s 已自动创建一个名为 pvc-xxxxxx 的 PV(名称包含 PVC ID),且状态为 Bound,无需运维人员手动创建 PV。
五、实践:Pod 如何使用 PVC 挂载存储?
最终目的是让 Pod 用上存储,只需在 Pod 配置中通过 volumes 引用 PVC,再通过 volumeMounts 挂载到容器内的目录。
示例:MySQL Pod 使用 PVC 持久化数据
apiVersion: v1
kind: Pod
metadata:
name: mysql-pod
spec:
containers:
- name: mysql
image: mysql:5.7
env:
- name: MYSQL_ROOT_PASSWORD
value: "123456"
volumeMounts:
- name: mysql-data # 与下面 volumes 名称对应
mountPath: /var/lib/mysql # 容器内挂载目录(MySQL 数据存储目录)
volumes:
- name: mysql-data
persistentVolumeClaim:
claimName: app-pvc-10gi # 引用已绑定的 PVC 名称
关键验证:
- 执行 kubectl apply -f mysql-pod.yaml 创建 Pod;
- 进入容器写入数据:kubectl exec -it mysql-pod -- mysql -uroot -p123456 -e "create database testdb;";
- 删除 Pod 并重建:kubectl delete pod mysql-pod,再重新创建 Pod;
- 再次进入容器查看:kubectl exec -it mysql-pod -- mysql -uroot -p123456 -e "show databases;",会发现 testdb 仍存在 —— 说明数据通过 PV 持久化,不受 Pod 重建影响。
六、常见问题与避坑指南
- PVC 一直 Pending,无法绑定 PV?
- 检查 storageClassName 是否匹配(大小写敏感);
- 检查 PV 的 accessModes 是否包含 PVC 请求的模式;
- 检查 PV 容量是否 ≥ PVC 请求容量;
- 检查 PV 是否已被其他 PVC 绑定(kubectl get pv 看状态是否为 Available)。
- Pod 挂载 PVC 失败,提示 “permission denied”?
securityContext:
fsGroup: 1000 # 容器内用户组 ID,与存储目录权限匹配
- 底层存储目录权限问题(如 NFS 共享目录的权限不是 777,需执行 chmod 777 /nfs/share/data);
- 容器内用户无读写权限,可在 Pod 中添加 securityContext 配置:
- 如何扩容 PVC?
- 前提:StorageClass 开启 allowVolumeExpansion: true,且底层存储支持扩容(如 EBS、NFS);
- 直接修改 PVC 的 storage 字段(如从 10Gi 改为 20Gi),K8s 会自动扩容 PV 和底层存储。
总结:PV/PVC 的核心价值
PV 和 PVC 的设计,本质是 **“解耦存储管理与应用使用”**:
- 对运维人员:统一管理存储资源(创建 PV/StorageClass),不用关心哪个 Pod 在用;
- 对开发者:只需声明存储需求(创建 PVC),不用关心底层是 NFS 还是云硬盘;
- 对集群:存储资源可复用(PV 释放后可重新绑定),且支持动态扩缩容,适配不同场景。
掌握 PV 和 PVC,是 K8s 持久化存储的基础 —— 无论是数据库、日志系统,还是需要保存配置的应用,都离不开这套机制。建议从 “静态 PV+PVC” 入手实践,熟悉后再尝试动态供给(StorageClass),逐步掌握 K8s 存储的核心能力。
