K8S存储体系全解-从PV-PVC-SC到StatefulSet持久化实战

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 会根据 accessModesstorage 请求自动匹配最合适的 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 存储的三个层次:

  1. 基础层:理解 PV/PVC/SC 的关系和绑定机制
  2. 进阶层:掌握动态存储供给和默认存储类配置
  3. 实战层:StatefulSet + Headless Service + VolumeClaimTemplate 构建完整的有状态服务存储方案
相关推荐
古城小栈2 小时前
K8s 认证、授权 系统
云原生·容器·kubernetes
姚不倒3 小时前
Go语言实战:多态文件存储系统(接口、错误处理、panic/recover)
云原生·golang
木雷坞3 小时前
csdn-enterpriseGitLab Runner docker pull 慢:并行流水线镜像拉取排查
运维·docker·容器·gitlab
sbjdhjd3 小时前
02 下 | Kubernetes Pod 实战实验完全解析
linux·运维·云原生·kubernetes·podman·kubelet·kubeless
切糕师学AI3 小时前
Envoy 详解:云原生时代的高性能网络代理
网络·云原生·istio·网络代理·envoy·sidecar·网格服务
酷道4 小时前
获取Docker阿里云专属镜像加速地址
阿里云·docker·容器·云计算
古城小栈4 小时前
K8s 存储组件 通俗精讲
云原生·容器·kubernetes
千匠网络4 小时前
千匠网络制造行业渠道分销B2B解决方案:AI驱动,重构产业分销模式
网络·云原生·架构·制造业·b2b·电商解决方案