技术博客

Kubernetes 存储实战:PV、PVC、StorageClass 与动态供给

系统讲解 Kubernetes 存储体系:PersistentVolume 静态供给、StorageClass 动态供给、PVC 存储申请、访问模式与回收策略,覆盖 NFS、Ceph RBD、阿里云 CSI 等主流存储后端,以及 StatefulSet 有状态存储、数据备份迁移等生产实战场景。

Kubernetes存储PVPVCStorageClassCSIStatefulSet

Kubernetes 把存储抽象为三层:StorageClass(存储类型/供给方式)→ PersistentVolume(PV)(实际存储资源)→ PersistentVolumeClaim(PVC)(应用申请的存储)。理解这三层及其关系,是运维有状态应用(数据库、消息队列、文件存储)的基础。

存储体系架构

┌──────────────────────────────────────────────────────────┐
│  应用层                                                    │
│  Pod → volumeMounts → PVC(申请 10Gi,ReadWriteOnce)       │
└────────────────────────┬─────────────────────────────────┘
                         │ 绑定
┌────────────────────────▼─────────────────────────────────┐
│  K8s 存储层                                                │
│  PV(10Gi,绑定到具体存储后端)                              │
│    ↑ 动态创建(StorageClass)或 静态预先创建                  │
└────────────────────────┬─────────────────────────────────┘

┌────────────────────────▼─────────────────────────────────┐
│  存储后端                                                   │
│  NFS / Ceph RBD / 阿里云云盘 / AWS EBS / 本地磁盘            │
└──────────────────────────────────────────────────────────┘

一、静态 PV(手动预先创建)

适合:存储资源固定、数量少、需要精确控制的场景。

# 创建 PV(以 NFS 为例)
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv-01
  labels:
    type: nfs
    env: production
spec:
  capacity:
    storage: 100Gi             # 存储容量
  accessModes:
  - ReadWriteMany              # 访问模式(见下文)
  persistentVolumeReclaimPolicy: Retain   # 回收策略
  storageClassName: ""         # 空字符串:不绑定 StorageClass(手动绑定)
  nfs:
    server: 192.168.1.100      # NFS 服务器地址
    path: /exports/k8s/data    # NFS 导出路径
    readOnly: false
# 创建 PVC 绑定到这个 PV
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: data-pvc
  namespace: production
spec:
  accessModes:
  - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
  storageClassName: ""    # 空字符串匹配 storageClassName="" 的 PV
  selector:
    matchLabels:
      type: nfs           # 精确匹配有这些标签的 PV
# 查看 PV 和 PVC 绑定状态
kubectl get pv
# NAME         CAPACITY   ACCESS MODES   RECLAIM POLICY   STATUS   CLAIM
# nfs-pv-01   100Gi      RWX            Retain           Bound    production/data-pvc

kubectl get pvc -n production
# NAME       STATUS   VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS
# data-pvc   Bound    nfs-pv-01   100Gi      RWX

访问模式详解

ReadWriteOnce (RWO):只能被一个节点以读写方式挂载
  → 适合:数据库、单实例应用
  → 支持:云盘(阿里云、AWS EBS)、本地磁盘

ReadOnlyMany (ROX):可以被多个节点以只读方式挂载
  → 适合:静态内容分发

ReadWriteMany (RWX):可以被多个节点以读写方式挂载
  → 适合:日志收集、共享文件
  → 支持:NFS、CephFS、Azure Files

ReadWriteOncePod (RWOP):只能被一个 Pod 以读写方式挂载(K8s 1.22+)
  → 最严格的独占模式

回收策略

Retain:PVC 删除后,PV 保留(状态变为 Released,需手动清理后才能重新绑定)
        → 生产数据库首选,防止误删数据

Delete:PVC 删除后,PV 和底层存储一起删除(动态供给默认策略)
        → 临时存储可以用

Recycle:已废弃,不用

二、StorageClass 与动态供给

动态供给:PVC 创建时,K8s 自动调用存储插件(CSI Driver)创建 PV,无需手动。

NFS 动态供给(使用 nfs-subdir-external-provisioner)

# 安装 NFS 动态供给器(Helm)
helm repo add nfs-subdir-external-provisioner \
  https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/

helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
  --set nfs.server=192.168.1.100 \
  --set nfs.path=/exports/k8s \
  --set storageClass.name=nfs-client \
  --set storageClass.defaultClass=false \
  -n kube-system
# StorageClass(NFS 动态供给)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-client
  annotations:
    storageclass.kubernetes.io/is-default-class: "false"
provisioner: cluster.local/nfs-subdir-external-provisioner
parameters:
  archiveOnDelete: "true"   # 删除 PVC 时归档而不是删除数据
reclaimPolicy: Retain       # 默认 Delete,这里改为 Retain 更安全
volumeBindingMode: Immediate

阿里云 CSI 存储类(生产常用)

# 阿里云云盘(适合 RWO 场景)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: alicloud-disk-ssd
provisioner: diskplugin.csi.alibabacloud.com
parameters:
  type: cloud_ssd            # cloud_efficiency=高效盘, cloud_ssd=SSD盘, cloud_essd=ESSD盘
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer  # 等 Pod 调度后再供给(匹配 AZ)

---
# 阿里云 NAS(适合 RWX 场景)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: alicloud-nas
provisioner: nasplugin.csi.alibabacloud.com
parameters:
  server: "xxx.cn-shanghai.nas.aliyuncs.com"
  path: "/k8s"
  vers: "3"
reclaimPolicy: Retain

使用 StorageClass 动态申请存储

# PVC 动态申请(指定 StorageClass)
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data
  namespace: production
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi
  storageClassName: alicloud-disk-ssd   # 指定 StorageClass,会自动创建 PV
# 查看动态创建的 PV
kubectl get pv
# NAME                                       CAPACITY   ACCESS MODES   RECLAIM   STATUS   STORAGECLASS
# pvc-a1b2c3d4-...                          50Gi       RWO            Retain    Bound    alicloud-disk-ssd

三、StatefulSet 存储(volumeClaimTemplates)

StatefulSet 每个 Pod 需要独立的存储,使用 volumeClaimTemplates 自动为每个 Pod 创建独立 PVC。

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: production
spec:
  serviceName: mysql        # 需要配套 Headless Service
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:8.4
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: password
        volumeMounts:
        - name: data
          mountPath: /var/lib/mysql    # 数据目录
        - name: config
          mountPath: /etc/mysql/conf.d
        resources:
          requests:
            memory: 2Gi
            cpu: 500m
          limits:
            memory: 4Gi
            cpu: 2000m
      volumes:
      - name: config
        configMap:
          name: mysql-config
  # 每个 Pod 自动创建独立的 PVC
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: ["ReadWriteOnce"]
      storageClassName: alicloud-disk-ssd
      resources:
        requests:
          storage: 100Gi
# StatefulSet 创建后,自动生成的 PVC 命名规则:<volumeClaimTemplate名>-<pod名>
kubectl get pvc -n production
# NAME           STATUS   VOLUME                                     CAPACITY
# data-mysql-0   Bound    pvc-aaa...                                100Gi
# data-mysql-1   Bound    pvc-bbb...                                100Gi
# data-mysql-2   Bound    pvc-ccc...                                100Gi

# StatefulSet 缩容后,PVC 不会自动删除(保护数据)
# 需要手动删除:kubectl delete pvc data-mysql-2 -n production

四、存储容量扩容

# 前提:StorageClass 开启了 AllowVolumeExpansion
kubectl get storageclass alicloud-disk-ssd -o yaml | grep allowVolumeExpansion
# allowVolumeExpansion: true

# 直接编辑 PVC 扩容(不需要删除 Pod)
kubectl edit pvc mysql-data -n production
# 修改 spec.resources.requests.storage 为更大的值

# 或者 patch
kubectl patch pvc mysql-data -n production -p \
  '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'

# 查看扩容状态
kubectl describe pvc mysql-data -n production
# Conditions:
#   Type: FileSystemResizePending  → 等待 Pod 重启后文件系统扩展
# 或者
#   Type: Resizing                 → 正在扩容

# 某些存储驱动需要 Pod 重启才能完成文件系统扩容
kubectl rollout restart deployment mysql -n production

五、数据备份与迁移

使用 VolumeSnapshot(K8s 原生快照)

# 先创建 VolumeSnapshotClass
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: alicloud-disk-snapshot
driver: diskplugin.csi.alibabacloud.com
deletionPolicy: Retain

---
# 对 PVC 做快照
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: mysql-data-snapshot-20260722
  namespace: production
spec:
  volumeSnapshotClassName: alicloud-disk-snapshot
  source:
    persistentVolumeClaimName: mysql-data
# 查看快照状态
kubectl get volumesnapshot -n production
# NAME                            READYTOUSE   SOURCEPVC    RESTORESIZE   AGE
# mysql-data-snapshot-20260722   true         mysql-data   100Gi         5m

# 从快照恢复到新 PVC
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data-restored
  namespace: production
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 100Gi
  storageClassName: alicloud-disk-ssd
  dataSource:
    name: mysql-data-snapshot-20260722
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io
EOF

PVC 数据迁移(跨命名空间/集群)

# 迁移数据:使用 rsync Pod
kubectl run migration \
  --image=alpine \
  --restart=Never \
  --overrides='
{
  "spec": {
    "containers": [{
      "name": "migration",
      "image": "alpine",
      "command": ["sh", "-c", "apk add rsync && rsync -av /src/ /dst/"],
      "volumeMounts": [
        {"name": "src", "mountPath": "/src"},
        {"name": "dst", "mountPath": "/dst"}
      ]
    }],
    "volumes": [
      {"name": "src", "persistentVolumeClaim": {"claimName": "old-data"}},
      {"name": "dst", "persistentVolumeClaim": {"claimName": "new-data"}}
    ]
  }
}'

存储问题排查

# PVC 一直 Pending
kubectl describe pvc <pvc-name> -n <namespace>
# 常见原因:
# 1. 没有匹配的 PV(静态)或 StorageClass 不存在
# 2. StorageClass provisioner 故障
# 3. 存储容量不足
# 4. AZ 不匹配(云盘和 Pod 不在同一 AZ)

# 查看存储供给器日志
kubectl logs -n kube-system -l app=nfs-subdir-external-provisioner

# Pod 挂载 PVC 失败
kubectl describe pod <pod-name> | grep -A 10 Events
# Events: FailedMount → 节点无法挂载存储
# 检查节点是否安装了必要的客户端(nfs-utils、ceph-common 等)

# 查看 CSI 驱动状态
kubectl get csinode
kubectl get csidrivers
kubectl get pods -n kube-system -l app.kubernetes.io/name=csi-disk-plugin

小结

K8s 存储体系:静态 PV 适合存量存储资源的精确控制;StorageClass 动态供给 是生产中的主流方式,PVC 提交即自动创建 PV;StatefulSet + volumeClaimTemplates 是有状态应用(数据库、消息队列)的标准模式,每个 Pod 独享独立存储;VolumeSnapshot 提供原生快照能力,是数据库备份的基础设施。选择存储时的关键考量:访问模式(RWO 单点 vs RWX 多节点共享)决定存储类型,回收策略(Retain vs Delete)决定数据安全级别。