K8S存储体系全解:从PV/PVC/SC到StatefulSet持久化实战
导读 :容器是"阅后即焚"的------Pod 重启后内部数据全部丢失。对于数据库、文件服务等有状态应用,数据持久化是不可逾越的门槛。K8S 通过 PV(持久卷)→ PVC(持久卷声明)→ SC(存储类) 三层抽象,以及 StatefulSet + VolumeClaimTemplate 机制,构建了一套优雅的存储管理体系。本文将通过 NFS 存储后端,从手动创建 PV 到动态供给 SC,从 Deployment 共享存储到 StatefulSet 独享存储,带你彻底搞懂 K8S 的存储体系。
一、为什么需要持久化存储?
容器的文件系统是临时的------容器停止或重建后数据即丢失。但在生产环境中,很多应用需要数据持久化:
| 场景 | 数据类型 | 丢失后果 |
|---|---|---|
| MySQL / Redis | 数据文件 | 业务数据全部丢失 |
| 文件服务 | 用户上传的文件 | 用户内容丢失 |
| 日志采集 | 采集的日志数据 | 日志断档 |
| 配置中心 | 持久化配置 | 服务配置丢失 |
K8S 存储抽象层的核心价值:
┌─────────────────────────────────────────────────────────┐
│ K8S 存储抽象体系 │
│ │
│ Pod ──→ PVC(声明需要多少存储)──→ PV/SC(提供存储) │
│ │
│ 开发者只需关心:"我要 5Gi 存储"(PVC) │
│ 运维者只需关心:"后端存储是什么"(PV/SC) │
│ 两者通过标签和存储类解耦,互不干扰 │
└─────────────────────────────────────────────────────────┘
二、存储卷类型全景
K8S 支持的存储卷类型多达 30+ 种,按层次可以分为三类:
┌──────────────────────────────────────────────────────────┐
│ K8S 存储卷分类 │
├──────────────┬──────────────┬────────────────────────────┤
│ 临时存储 │ 本地存储 │ 持久存储(网络存储) │
├──────────────┼──────────────┼────────────────────────────┤
│ emptyDir │ hostPath │ NFS │
│ (Pod内临时) │ (节点目录) │ Ceph RBD / CephFS │
│ │ │ iSCSI │
│ │ │ AWS EBS │
│ │ │ 阿里云 OSS / NAS │
│ │ │ GlusterFS │
└──────────────┴──────────────┴────────────────────────────┘
| 类型 | 生命周期 | 数据持久性 | 适用场景 |
|---|---|---|---|
| emptyDir | 与 Pod 同生共死 | 不持久化 | Pod 内容器间共享临时数据 |
| hostPath | 与节点同生共死 | 节点级持久化 | 开发测试、DaemonSet |
| NFS | 独立于 Pod 和节点 | 持久化 | 文件共享、日志存储 |
| Ceph | 独立于 Pod 和节点 | 持久化 | 高可用块存储、对象存储 |
| PV/PVC | 管理员控制 | 持久化 | 生产环境标准方式 |
三、PV/PVC/SC 核心概念
3.1 三个核心资源的关系
┌─────────────────────────────────────────────────────────┐
│ Pod ←──引用── PVC ←──绑定── PV ←──对接── 后端存储 │
│ ↑ ↑ │
│ │ 或由 SC 动态创建 │
│ │ ↑ │
│ └── 指定 SC ──→ SC │
└─────────────────────────────────────────────────────────┘
| 资源 | 全称 | 角色 | 由谁管理 |
|---|---|---|---|
| PV | PersistentVolume | 一块具体的存储资源 | 集群管理员 |
| PVC | PersistentVolumeClaim | 对存储的"申请单" | 开发者/用户 |
| SC | StorageClass | 存储的"类别模板",支持动态创建 PV | 集群管理员 |
类比理解:PV 是"房子",PVC 是"租房合同",SC 是"房产中介"。开发者(租客)只需要签合同(PVC),不用关心房子在哪(PV),房产中介(SC)会自动帮忙找房子。
3.2 PV 的三种状态
Available(可用)──→ Bound(绑定)──→ Released(已释放)──→ Failed(失败)
↑ │ │
└──── 回收后 ←──────┘ │
│ │
└── 手动清理后 ←────────┘
| 状态 | 说明 |
|---|---|
| Available | PV 已创建,等待 PVC 绑定 |
| Bound | PV 已被 PVC 绑定,正在使用 |
| Released | PVC 被删除,PV 已释放但数据保留(取决于回收策略) |
| Failed | 自动回收失败 |
3.3 PV 访问模式
| 访问模式 | 简称 | 说明 |
|---|---|---|
| ReadWriteOnce | RWO | 单节点读写(最常用) |
| ReadOnlyMany | ROX | 多节点只读 |
| ReadWriteMany | RWX | 多节点读写(NFS/CephFS 支持) |
| ReadWriteOncePod | RWOP | 单 Pod 读写(K8S 1.22+,仅 CSI 卷) |
3.4 PV 回收策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
| Retain | 保留 PV 和数据,需手动清理后重新使用 | 生产环境(最安全) |
| Delete | 删除 PV 和对应的后端存储数据 | 测试环境、动态供给 |
| Recycle | 执行 rm -rf 清理后重新可用(已废弃) |
旧版兼容 |
四、实战一:手动创建 PV + PVC + Pod
4.1 NFS 后端准备
在 NFS 服务器上创建存储目录:
bash
# 在 NFS 服务器(10.0.0.250)上创建 PV 对应的目录
mkdir -pv /data/nfs-server/pv/linux/pv00{1,2,3}
# 确认 NFS 共享配置
exportfs
# /data/nfs-server <world>
4.2 创建 PV
yaml
# manual-pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv01
spec:
accessModes:
- ReadWriteMany # NFS 支持多节点读写
nfs:
path: /data/nfs-server/pv/linux/pv001
server: 10.0.0.250
persistentVolumeReclaimPolicy: Retain # 保留回收策略
capacity:
storage: 2Gi
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv02
spec:
accessModes:
- ReadWriteMany
nfs:
path: /data/nfs-server/pv/linux/pv002
server: 10.0.0.250
persistentVolumeReclaimPolicy: Retain
capacity:
storage: 5Gi
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv03
spec:
accessModes:
- ReadWriteMany
nfs:
path: /data/nfs-server/pv/linux/pv003
server: 10.0.0.250
persistentVolumeReclaimPolicy: Retain
capacity:
storage: 10Gi
bash
kubectl apply -f manual-pv.yaml
# 查看所有 PV(状态为 Available,等待绑定)
kubectl get pv
# NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS AGE
# pv01 2Gi RWX Retain Available 4s
# pv02 5Gi RWX Retain Available 4s
# pv03 10Gi RWX Retain Available 4s
4.3 创建 PVC
PVC 会根据 accessModes 和 storage 请求自动匹配最合适的 PV:
yaml
# manual-pvc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc001
spec:
accessModes:
- ReadWriteMany
resources:
limits:
storage: 4Gi # 最大不超过 4Gi
requests:
storage: 3Gi # 至少需要 3Gi
bash
kubectl apply -f manual-pvc.yaml
# PVC 自动绑定了 pv02(5Gi >= 3Gi 且 <= 4Gi)
kubectl get pvc,pv
# NAME STATUS VOLUME CAPACITY ACCESS MODES
# persistentvolumeclaim/pvc001 Bound pv02 5Gi RWX
#
# NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM
# pv01 2Gi RWX Retain Available
# pv02 5Gi RWX Retain Bound default/pvc001 ← 已绑定
# pv03 10Gi RWX Retain Available
PV 绑定规则 :PVC 按
accessModes过滤匹配的 PV,再按storage请求大小筛选(PV 容量 >= PVC 请求),最后选择容量最小的 PV 进行绑定。
4.4 Pod 引用 PVC
yaml
# deploy-pvc.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: deploy-pvc
spec:
replicas: 3
selector:
matchLabels:
apps: v1
template:
metadata:
labels:
apps: v1
spec:
volumes:
- name: data
persistentVolumeClaim:
claimName: pvc001 # 引用 PVC
containers:
- name: c1
image: nginx:1.20
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html # 挂载到 Nginx 目录
bash
kubectl apply -f deploy-pvc.yaml
# 多个 Pod 跨节点共享同一块 NFS 存储
kubectl get pods -o wide
# NAME READY IP NODE
# deploy-pvc-xxx-f2b69 1/1 10.100.2.106 worker233
# deploy-pvc-xxx-q56hp 1/1 10.100.2.107 worker233
# deploy-pvc-xxx-sjct4 1/1 10.100.1.228 worker232
# 跨节点访问验证(所有 Pod 返回相同数据)
curl 10.100.2.106 # 同一份数据
curl 10.100.1.228 # 同一份数据
五、实战二:StorageClass 动态存储
手动创建 PV 的问题很明显:每块存储都需要管理员手动创建和维护 。当 Pod 数量很多时,管理成本极高。StorageClass(SC)通过动态供给解决了这个问题。
5.1 手动 vs 动态对比
手动模式(PV 预创建):
管理员创建 PV(1Gi) + PV(2Gi) + PV(5Gi) + PV(10Gi) + ...
PVC 按 size 匹配可用的 PV
问题:PV 大小不一定精确匹配,可能浪费存储
动态模式(SC 自动创建):
管理员创建一个 StorageClass(关联 NFS 后端)
PVC 只需指定 storageClassName + 请求大小
SC 自动创建精确大小的 PV
优势:按需分配,零浪费
5.2 部署 NFS CSI Driver
bash
# 克隆 NFS CSI Driver
git clone https://github.com/kubernetes-csi/csi-driver-nfs.git
cd csi-driver-nfs-4.9.0
# 安装 NFS CSI Driver(所有节点需导入镜像)
./deploy/install-driver.sh v4.9.0 local
# 验证组件部署
kubectl -n kube-system get pod -o wide -l app
# csi-nfs-controller-xxx 4/4 Running worker232 (控制器,1个)
# csi-nfs-node-xxx 3/3 Running master231 (节点插件,每个节点1个)
# csi-nfs-node-xxx 3/3 Running worker232
# csi-nfs-node-xxx 3/3 Running worker233
5.3 创建 StorageClass
yaml
# storageclass.yaml(来自 csi-driver-nfs-4.9.0/deploy/v4.9.0/)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
server: 10.0.0.250
share: /data/nfs-server/sc/
reclaimPolicy: Delete
volumeBindingMode: Immediate
bash
kubectl apply -f storageclass.yaml
kubectl get sc
# NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE
# nfs-csi nfs.csi.k8s.io Delete Immediate
5.4 PVC 使用动态存储类
yaml
# pvc-sc.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: pvc-sc-dynamic
spec:
storageClassName: nfs-csi # 指定存储类
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi # 自动创建精确 1Gi 的 PV
bash
kubectl apply -f pvc-sc.yaml
# PV 自动创建,名称为 UUID 格式(由 SC 动态生成)
kubectl get pvc,sc,pv
# NAME STATUS VOLUME CAPACITY STORAGECLASS
# pvc-sc-dynamic Bound pvc-91a29925-xxxx 1Mi nfs-csi
#
# NAME PROVISIONER RECLAIMPOLICY
# nfs-csi nfs.csi.k8s.io Delete
#
# NAME CAPACITY RECLAIM POLICY STATUS
# pvc-91a29925-26ae-448f-9748-28d0e54fe97c 1Mi Delete Bound default/pvc-sc-dynamic nfs-csi
动态供给的关键 :PVC 创建时指定
storageClassName,SC 会自动创建一个容量精确匹配的 PV 并完成绑定。PVC 删除时,如果回收策略是 Delete,PV 和后端数据也会自动清理。
5.5 配置默认存储类
K8S 支持设置一个默认存储类,PVC 不指定 storageClassName 时自动使用:
bash
# 设置默认存储类
kubectl patch sc nfs-csi -p \
'{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'
# 验证(出现 default 标记)
kubectl get sc
# NAME PROVISIONER RECLAIMPOLICY DEFAULT
# nfs-csi (default) nfs.csi.k8s.io Delete ✓
# 取消默认
kubectl patch sc nfs-csi -p \
'{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'
声明式定义多个存储类:
yaml
# sc-multiple.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: sc-ssd
annotations:
storageclass.kubernetes.io/is-default-class: "false"
provisioner: nfs.csi.k8s.io
parameters:
server: 10.0.0.250
share: /data/nfs-server/sc-ssd/
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
- nfsvers=4.1
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: sc-hdd
annotations:
storageclass.kubernetes.io/is-default-class: "true"
provisioner: nfs.csi.k8s.io
parameters:
server: 10.0.0.250
share: /data/nfs-server/sc-hdd/
生产建议 :不同性能的存储(SSD/HDD)定义不同的 SC,PVC 按需选择,实现存储分级管理。
六、实战三:StatefulSet 独享存储
6.1 有状态服务 vs 无状态服务
无状态服务(Nginx、Tomcat):
- 所有 Pod 完全等价
- Pod 重启后无需恢复状态
- Deployment + PVC 即可(多 Pod 共享存储)
有状态服务(MySQL、Redis、Kafka):
- Pod 之间有主从/分片关系
- 每个 Pod 需要独立的存储
- Pod 需要稳定的网络标识(名称/IP)
- 启动/停止有严格的顺序要求
StatefulSet 解决的三大难题:
| 难题 | StatefulSet 的解决方案 |
|---|---|
| 启动/停止顺序 | 按序号 0→N 启动,N→0 终止 |
| 独立存储 | VolumeClaimTemplate 为每个 Pod 创建独立 PVC |
| 稳定网络标识 | Headless Service + 固定编号(sts-name-0, sts-name-1...) |
6.2 Headless Service:稳定网络标识
Headless Service(无头服务)的核心特征是 clusterIP: None------不分配虚拟 IP,DNS 直接解析到 Pod IP:
yaml
# Headless Service
apiVersion: v1
kind: Service
metadata:
name: svc-headless
spec:
ports:
- port: 80
name: web
clusterIP: None # 关键:设为 None 即为 Headless Service
selector:
app: nginx
yaml
# StatefulSet
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts
spec:
selector:
matchLabels:
app: nginx
serviceName: svc-headless # 关键:关联 Headless Service
replicas: 3
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.20
bash
kubectl apply -f sts-headless.yaml
# Pod 按序号创建:0 → 1 → 2
kubectl get sts,svc,pods -o wide
# NAME READY AGE
# statefulset.apps/sts 3/3 15s
#
# NAME TYPE CLUSTER-IP PORT(S)
# service/svc-headless ClusterIP None 80/TCP ← 无头服务
#
# NAME READY IP NODE
# pod/sts-0 1/1 10.100.2.118 worker233 ← 按序创建
# pod/sts-1 1/1 10.100.1.235 worker232
# pod/sts-2 1/1 10.100.2.119 worker233
DNS 解析验证:
bash
# 每个 Pod 都有独立的 DNS 记录
dig @10.200.0.10 sts-0.svc-headless.default.svc.oldboyedu.com +short
# 10.100.2.151
dig @10.200.0.10 sts-1.svc-headless.default.svc.oldboyedu.com +short
# 10.100.1.9
# Pod 内部可以通过名称互相访问
kubectl exec -it sts-0 -- sh
/ # ping sts-1.svc-headless -c 3
# PING sts-1.svc-headless (10.100.1.10): 56 data bytes
# 3 packets transmitted, 3 packets received, 0% packet loss
6.3 Pod 删除后网络标识保持稳定
bash
# 删除所有 Pod
kubectl delete pods -l app=nginx
# pod "sts-0" deleted
# pod "sts-1" deleted
# pod "sts-2" deleted
# StatefulSet 自动重建,名称不变、DNS 不变
kubectl get pods -o wide
# NAME READY IP NODE AGE
# pod/sts-0 1/1 10.100.2.153 worker233 5s ← 名称不变
# pod/sts-1 1/1 10.100.1.10 worker232 3s ← 名称不变
# pod/sts-2 1/1 10.100.2.154 worker233 2s ← 名称不变
# DNS 仍然有效(IP 可能变化但 DNS 名称不变)
kubectl exec -it sts-0 -- ping sts-1.svc-headless -c 3
# PING sts-1.svc-headless (10.100.1.10): 56 data bytes
# 0% packet loss
与 Deployment 的关键区别 :Deployment 的 Pod 名称为随机哈希,重建后名称和 DNS 完全改变;StatefulSet 的 Pod 名称固定为
sts-{0,1,2,...},重建后名称和 DNS 保持不变。
6.4 VolumeClaimTemplate:独享存储
StatefulSet 通过 volumeClaimTemplates 为每个 Pod 自动创建独立的 PVC:
yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: sts
spec:
selector:
matchLabels:
app: nginx
serviceName: svc-headless
replicas: 3
volumeClaimTemplates: # 关键:存储卷申请模板
- metadata:
name: data
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: "nfs-csi" # 使用动态存储类
resources:
requests:
storage: 2Gi # 每个 Pod 独享 2Gi
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.20
volumeMounts:
- name: data
mountPath: /usr/share/nginx/html
bash
kubectl apply -f sts-volumeclaimtemplate.yaml
# 每个 Pod 自动创建独立的 PVC
kubectl get pvc
# NAME STATUS VOLUME CAPACITY
# data-sts-0 Bound pvc-xxx-0 2Gi nfs-csi
# data-sts-1 Bound pvc-xxx-1 2Gi nfs-csi
# data-sts-2 Bound pvc-xxx-2 2Gi nfs-csi
StatefulSet 存储模型:
Pod sts-0 ←→ PVC data-sts-0 ←→ PV pvc-xxx-0 ←→ NFS 目录 sc/sts-0/
Pod sts-1 ←→ PVC data-sts-1 ←→ PV pvc-xxx-1 ←→ NFS 目录 sc/sts-1/
Pod sts-2 ←→ PVC data-sts-2 ←→ PV pvc-xxx-2 ←→ NFS 目录 sc/sts-2/
每个 Pod 拥有独立的存储空间,Pod 重建后自动绑定回原来的 PVC,数据不丢失。
七、CSI、CRI、CNI 三大接口区分
在面试和实际工作中,经常需要区分 K8S 的三大插件接口:
| 接口 | 全称 | 职责 | 典型插件 |
|---|---|---|---|
| CSI | Container Storage Interface | 存储管理(创建/挂载/删除卷) | NFS CSI、Ceph CSI、AWS EBS CSI |
| CRI | Container Runtime Interface | 容器运行时管理 | Docker(已废弃)、containerd、CRI-O |
| CNI | Container Network Interface | 网络管理(Pod 间通信、网络策略) | Flannel、Calico、Cilium |
一句话记忆:CSI 管"存"、CRI 管"跑"、CNI 管"通"。
八、生产环境最佳实践
8.1 存储选型建议
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 开发/测试 | NFS + SC | 部署简单、成本低 |
| 高可用 Web 服务 | Ceph RBD + SC | 性能高、支持快照 |
| 文件共享服务 | CephFS / NFS | 支持 RWX 多节点读写 |
| 日志存储 | CephFS + PVC | 容量大、可扩展 |
| 数据库 | Ceph RBD + StatefulSet | 低延迟、块设备级性能 |
8.2 生产检查清单
markdown
- [ ] PVC 设置了合理的 storage requests(避免浪费)
- [ ] 生产环境 PV 回收策略使用 Retain(防止误删数据)
- [ ] StatefulSet 使用 Headless Service 确保稳定网络标识
- [ ] 动态存储类已配置默认 SC(简化 PVC 创建)
- [ ] NFS/Ceph 后端已做高可用和备份
- [ ] PVC 设置 storage limits 防止无限使用
- [ ] 定期检查 Released 状态的 PV 并清理
8.3 PV/PVC 不绑定的排查思路
bash
# 1. 检查 PVC 状态
kubectl get pvc
# Pending → 未绑定成功
# 2. 查看 PVC 事件
kubectl describe pvc <pvc-name>
# 常见原因:
# - 没有匹配的 PV(手动模式)
# - 没有匹配的 SC(动态模式)
# - accessModes 不匹配
# - storage 大小超过所有可用 PV
# 3. 检查 SC 是否存在
kubectl get sc
# 4. 检查可用 PV
kubectl get pv
十、总结
K8S 存储体系的核心知识链路:
emptyDir/hostPath(临时/本地存储)
↓
PV(手动创建持久卷)← PVC(声明式申请)→ Pod
↓
SC(动态供给存储类)← PVC → Pod(自动创建 PV)
↓
StatefulSet + VolumeClaimTemplate(每个 Pod 独享存储)
↓
CSI(标准存储接口)→ Ceph / NFS / 云存储(后端存储)
掌握 K8S 存储的三个层次:
- 基础层:理解 PV/PVC/SC 的关系和绑定机制
- 进阶层:掌握动态存储供给和默认存储类配置
- 实战层:StatefulSet + Headless Service + VolumeClaimTemplate 构建完整的有状态服务存储方案