前言

容器生来无常,Pod 一删数据就没了。K8s 存储体系要回答:怎么给易逝的 Pod 一个持久的家。这篇讲透 PV/PVC/StorageClass 三层抽象和实战用法。

一、问题与分层

开发者视角: "我要 10Gi 读写快的存储"        ← PVC(声明)
    │ 谁来满足?
集群管理员视角: "我有一堆 NFS/云盘供给"      ← PV(资源) + StorageClass(供给方式)
    │ 挂给谁?
Pod 视角: volumeMounts 挂载点               ← Volume(使用)

为什么隔一层 PVC? 开发者不应该关心"存储在哪台 NFS/哪种云盘"——就像申请内存不需要指定物理内存条编号。解耦需求与供给。

二、临时与半永久卷

# emptyDir: Pod 的"临时便签"(Pod 删除即消失, 崩溃重启仍在)
volumes:
- name: scratch
  emptyDir: {}
# 用途: 缓存、中间产物、多容器共享文件

# hostPath: 用宿主机目录(危险但实用)
volumes:
- name: node-log
  hostPath: {path: /var/log, type: Directory}
# 用途: DaemonSet 采集节点日志(node-exporter/promtail)
# ⚠️ Pod 会漂移 + 数据跟节点走, 业务数据绝不用它

三、PV 与 PVC:静态供给

管理员预创建 PV(以 NFS 为例):

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs-data
spec:
  capacity: {storage: 50Gi}
  accessModes: ["ReadWriteMany"]        # RWX: 多节点读写
  persistentVolumeReclaimPolicy: Retain  # PVC 删除后 PV 保留(防误删数据!)
  nfs:
    server: 10.0.2.100
    path: /export/data

开发者提需求:

apiVersion: v1
kind: PersistentClaim
metadata:
  name: app-data
spec:
  accessModes: ["ReadWriteMany"]
  resources:
    requests: {storage: 50Gi}
  storageClassName: ""                  # 绑定静态 PV

Pod 使用:

spec:
  containers:
  - name: app
    volumeMounts: [{name: data, mountPath: /data}]
  volumes:
  - name: data
    persistentVolumeClaim: {claimName: app-data}

accessModes:

模式 缩写 含义
ReadWriteOnce RWO 单节点读写(云盘类)
ReadOnlyMany ROX 多节点只读
ReadWriteMany RWX 多节点读写(NFS/CephFS/文件存储)
ReadWriteOncePod RWOP 单 Pod 读写(更严格)

回收策略:Retain(保留,需手动清)/ Recycle(已废弃)/ Delete(动态供给默认,删 PVC 连底层盘一起删——慎用于重要数据)。

四、StorageClass:动态供给(主流玩法)

不用手动建 PV,PVC 一提,存储插件自动创建底层资源:

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
provisioner: diskplugin.csi.tencent.com        # CSI 插件(云厂商内置)
parameters:
  type: CLOUD_SSD
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer        # 等首个 Pod 调度后再建盘(同可用区!)
---
apiVersion: v1
kind: PersistentClaim
metadata:
  name: db-data
spec:
  storageClassName: fast-ssd
  accessModes: ["ReadWriteOnce"]
  resources:
    requests: {storage: 100Gi}
kubectl get pvc
# NAME      STATUS   VOLUME                                     CAPACITY   STORAGECLASS
# db-data   Bound    pvc-4f2a...                                100Gi      fast-ssd
kubectl get pv | head        # 自动创建的 PV

volumeBindingMode 的坑:Immediate 会立刻建盘——若 Pod 与盘不在同可用区就挂不上;用 WaitForFirstConsumer 让存储跟着 Pod 走。

五、扩容与快照

# 在线扩容(存储类需 allowVolumeExpansion: true)
kubectl patch pvc db-data -p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'

# 快照(需安装 CSI Snapshotter)
kubectl apply -f snapshot.yaml
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: db-snap-20250802
spec:
  volumeSnapshotClassName: csi-snapclass
  source:
    persistentVolumeClaimName: db-data

六、StatefulSet:有状态的正解

Deployment 的 Pod 是无面人(名字随机、存储共享);StatefulSet 给每个副本稳定的身份与独立存储:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql          # 配套 headless service
  replicas: 3
  selector: {matchLabels: {app: mysql}}
  template:
    metadata: {labels: {app: mysql}}
    spec:
      containers:
      - name: mysql
        image: mysql:8.0
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql
  volumeClaimTemplates:        # ★ 每个副本自动生成独立 PVC
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: fast-ssd
      resources: {requests: {storage: 100Gi}}
kubectl get pvc
# data-mysql-0   Bound  ...   # mysql-0 专属盘
# data-mysql-1   Bound  ...   # mysql-1 专属盘
# data-mysql-2   Bound  ...

# Pod 名字稳定 + DNS 稳定:
kubectl run t --rm -it --image=busybox -- nslookup mysql-0.mysql.default.svc

适用:数据库主从、Kafka、ES、ZooKeeper 这类"每个节点有自己身份和数据"的应用。

七、数据保护清单(血泪法则)

  1. PVC 删除 ≠ 数据安全:确认 reclaimPolicy,重要数据用 Retain 或快照先行
  2. 云盘每天快照策略 + 异地(对象存储)备份
  3. 数据库别只靠快照——逻辑备份(mysqldump/xtrabackup)+ 物理快照双保险
  4. 测试恢复!没演练过的备份等于没有备份
  5. 监控 PVC 使用率(Prometheus: kubelet_volume_stats_used_bytes),别等写满

八、常见坑

现象 原因
PVC 一直 Pending 没匹配的 PV(静态)/ provisioner 没装 / 容量/模式不匹配
Pod 调度不了 + 盘在别的可用区 volumeBindingMode 用 WaitForFirstConsumer
多 Pod 挂不上同一 PVC RWO 只支持单节点;要共享用 RWX(NFS/文件存储)
删了 PVC 数据没了 Delete 回收策略生效——重要数据 Retain
StatefulSet 缩容再扩容 新 Pod 复用旧 PVC(数据还在,好事); 删 PVC 才真清
hostPath 数据"丢失" Pod 漂移到别的节点了

小结

层 对象 一句话
使用 Volume + volumeMounts 挂进容器
声明 PVC 我要多少、什么模式
供给 PV / StorageClass 谁来满足、怎么自动创建
有状态 StatefulSet + volumeClaimTemplates 稳定身份 + 独立盘
保命 快照 + 备份 + Retain 数据无价

本文是「云原生」系列第 12 篇。