Kubernetes 持久化存储实战——PV/PVC/StorageClass 的完整配置指南
Kubernetes 持久化存储实战——PV/PVC/StorageClass 的完整配置指南
适读人群:需要在 K8s 上跑有状态服务(数据库、中间件)的工程师 | 阅读时长:约 15 分钟 | 核心价值:搞清 PV/PVC/StorageClass 三层关系,避开数据丢失的关键陷阱
我见过的 K8s 生产事故里,和存储相关的占了大约 30%。要么是数据意外被删,要么是存储 IO 成为瓶颈,要么是 Pod 迁移后数据丢了。
这些事故大部分都可以通过正确的存储配置来避免。
PV/PVC/StorageClass 三者关系
很多人在这里绕不清楚,我先用一个类比理清楚:
- StorageClass:存储的"类型规范",相当于"可以购买哪类存储"的目录(SSD、HDD、网络存储)
- PV(PersistentVolume):实际的存储资源,相当于"已经买好的一块硬盘",由运维或云平台创建
- PVC(PersistentVolumeClaim):应用的存储声明,相当于"我要用一块至少 50GB SSD 的硬盘"
Pod 引用 PVC,PVC 绑定到 PV,PV 对应实际存储。
Pod → PVC → PV → 实际存储(云盘/NFS/本地盘)StorageClass 的作用是让 PVC 可以动态申请(Dynamic Provisioning)PV,不需要运维提前手动创建。
静态 PV 配置(手动管理)
适合没有云环境、自建存储的场景:
# 1. 创建 PV(运维手动创建,对应实际存储)
apiVersion: v1
kind: PersistentVolume
metadata:
name: postgres-pv-01
labels:
type: local
app: postgres
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteOnce # 一次只能被一个节点读写(适合数据库)
persistentVolumeReclaimPolicy: Retain # 删除 PVC 后,PV 数据保留(不自动删)
storageClassName: manual
hostPath:
path: /data/postgres # 宿主机路径(仅适合单节点测试)
type: DirectoryOrCreate
---
# 2. 创建 PVC(开发者创建,申请存储)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgres-pvc
namespace: production
spec:
accessModes:
- ReadWriteOnce
storageClassName: manual
resources:
requests:
storage: 100Gi
selector:
matchLabels:
app: postgres # 精确匹配特定 PVAccessModes 说明
| 模式 | 缩写 | 含义 | 常见场景 |
|---|---|---|---|
| ReadWriteOnce | RWO | 单节点读写 | 数据库、单实例应用 |
| ReadOnlyMany | ROX | 多节点只读 | 共享静态文件 |
| ReadWriteMany | RWX | 多节点读写 | 共享文件系统(NFS) |
| ReadWriteOncePod | RWOP | 单 Pod 读写(K8s 1.22+) | 严格隔离场景 |
StorageClass 和动态 PV(推荐用法)
云环境下,推荐用 StorageClass 动态创建 PV,省去手动管理的工作:
# StorageClass(通常云平台已经预置,也可以自定义)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd
annotations:
storageclass.kubernetes.io/is-default-class: "false"
provisioner: disk.csi.aliyuncs.com # 阿里云云盘 CSI
parameters:
type: cloud_ssd # SSD 类型
fsType: ext4
reclaimPolicy: Retain # 重要!PVC 删了,云盘不要自动删
volumeBindingMode: WaitForFirstConsumer # 等 Pod 调度后再创建云盘(保证同 AZ)
allowVolumeExpansion: true # 允许在线扩容reclaimPolicy 有三个值:
Retain:删 PVC 后,PV 和底层存储保留(生产推荐)Delete:删 PVC 后,PV 和底层存储一并删除(谨慎!会丢数据)Recycle:已废弃
reclaimPolicy: Retain 是生产环境最重要的配置之一,一定要设置。
对应的 PVC(只需要声明 StorageClass 名称,PV 会自动创建):
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: mysql-data-pvc
namespace: production
spec:
accessModes:
- ReadWriteOnce
storageClassName: fast-ssd
resources:
requests:
storage: 200GiStatefulSet 的存储配置
StatefulSet 是 K8s 为有状态服务设计的控制器,它的 volumeClaimTemplates 会自动为每个 Pod 创建独立的 PVC:
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: mysql-cluster
namespace: production
spec:
serviceName: mysql-headless
replicas: 3
selector:
matchLabels:
app: mysql
template:
metadata:
labels:
app: mysql
spec:
containers:
- name: mysql
image: mysql:8.0
env:
- name: MYSQL_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: mysql-secret
key: root-password
ports:
- containerPort: 3306
volumeMounts:
- name: mysql-data
mountPath: /var/lib/mysql
- name: mysql-config
mountPath: /etc/mysql/conf.d
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
memory: "4Gi"
volumes:
- name: mysql-config
configMap:
name: mysql-config
# 为每个 Pod 自动创建独立 PVC
volumeClaimTemplates:
- metadata:
name: mysql-data
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd
resources:
requests:
storage: 200Gi
---
# Headless Service(StatefulSet 需要)
apiVersion: v1
kind: Service
metadata:
name: mysql-headless
namespace: production
spec:
clusterIP: None # Headless:不分配 ClusterIP
selector:
app: mysql
ports:
- port: 3306StatefulSet 的 Pod 名是固定的(mysql-cluster-0, mysql-cluster-1, mysql-cluster-2),PVC 也是固定的(mysql-data-mysql-cluster-0, ...),Pod 重启后仍然挂载同一个 PVC。
踩坑实录一:删掉 StatefulSet 后,PVC 被 cascade delete 了
现象:执行 kubectl delete statefulset mysql-cluster,StatefulSet 和 Pod 都删了,过了一分钟,对应的 PVC 也消失了,云盘里的数据丢了。
原因:默认情况下,删除 StatefulSet 时不会级联删除 PVC,但如果使用了 kubectl delete statefulset mysql-cluster --cascade=foreground 或者某些 GitOps 工具自动清理资源,PVC 会被一起删除。
解法:
- 手动删除时,先删 StatefulSet,确认 PVC 还在,再处理后续:
kubectl delete statefulset mysql-cluster --cascade=orphan # 只删 sts,不删 Pod 和 PVC
kubectl get pvc -n production # 确认 PVC 还在- StatefulSet 1.27+ 支持
persistentVolumeClaimRetentionPolicy:
spec:
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain # 删除 StatefulSet 时,PVC 保留
whenScaled: Retain # 缩容时,多出来的 PVC 保留踩坑实录二:PVC 动态扩容后 Pod 不生效
现象:PVC 存储不够了,执行了 PVC 扩容(edit 改了 storage 大小),PV 扩容了,但 Pod 内部的文件系统还是旧的大小。
原因:文件系统扩容(resize2fs 等操作)是在 Pod 挂载 PVC 时触发的,需要 Pod 重启才能触发文件系统扩容。
解法:
# 1. 修改 PVC 存储大小
kubectl patch pvc mysql-data-mysql-cluster-0 -n production \
-p '{"spec":{"resources":{"requests":{"storage":"500Gi"}}}}'
# 2. 等待 PV 扩容完成(观察 PVC 状态)
kubectl get pvc mysql-data-mysql-cluster-0 -n production -w
# 状态会经历:Bound → FileSystemResizePending → Bound
# 3. 重启 Pod 触发文件系统扩容
kubectl delete pod mysql-cluster-0 -n production
# StatefulSet 会自动重建,重建后文件系统扩容触发踩坑实录三:NFS 存储性能导致数据库超时
现象:把 MySQL 数据目录挂载到 NFS,写入性能极差,随机 IO 延迟高达 30ms+,数据库操作频繁超时。
原因:NFS 是网络文件系统,不适合数据库这类随机 IO 密集型的工作负载。数据库对 fsync 操作有严格要求,NFS 的网络延迟会严重拖慢。
解法:数据库存储必须使用本地 SSD 或云盘(云厂商的 SSD 云盘 IO 延迟一般在 0.5~1ms)。NFS 只适合:
- 共享文件存储(图片、文档等)
- 配置文件
- 日志文件(顺序写,对随机 IO 不敏感)
存储资源管理
# 查看所有 PV 及其状态
kubectl get pv -o wide
# 查看 PVC 绑定情况
kubectl get pvc -n production
# PV 状态说明:
# Available - 未绑定,等待 PVC
# Bound - 已绑定到 PVC
# Released - PVC 已删除,但 PV 数据还在(Retain 策略)
# Failed - 自动回收失败
# 手动释放 Released 状态的 PV(Retain 策略后需要手动清理)
kubectl patch pv my-pv -p '{"spec":{"claimRef":null}}'
# 查看存储使用情况
kubectl exec -it mysql-cluster-0 -n production -- df -h持久化存储是 K8s 有状态服务最关键的部分,比无状态服务复杂得多。一定要在上生产之前把 Retain 策略、备份机制、扩容流程都测试一遍,不要等到真出问题才发现配置有问题。
