本文系统性地讲解 Kubernetes 存储体系的核心概念、架构设计与生产实践,涵盖从基础 Volume 到 PV/PVC/StorageClass 的完整链路,并深入 NFS、Ceph、云盘三大主流存储后端的对接配置。适合已有 K8s 基础、希望深入理解和落地持久化存储方案的工程师阅读。


一、K8s 存储体系概览

1.1 为什么需要持久化存储

在 Kubernetes 中,Pod 是短暂的(ephemeral)。容器的文件系统本质上是一个临时的可写层(基于 OverlayFS 或其他联合文件系统),当容器重启时,这个可写层会被完全重建,所有写入的数据将丢失。

这带来了几个核心问题:

  • Pod 重启数据丢失:应用崩溃、节点故障、滚动更新等场景下,Pod 被重新调度后无法恢复之前的数据
  • 容器文件系统的临时性:即使在同一个 Pod 内,容器重启也会导致文件系统回到镜像初始状态
  • 多容器数据共享困难:同一 Pod 内的多个容器无法直接共享文件系统中的数据
  • 有状态应用部署需求:数据库、消息队列、文件服务等有状态应用必须依赖持久化存储

1.2 Volume → PV → PVC → StorageClass 演进历史

Kubernetes 存储体系经历了清晰的演进路径:

阶段机制特点局限
早期Volume(内联定义)直接在 Pod spec 中声明存储存储细节与应用耦合,运维负担重
v1.0+PersistentVolume (PV)集群级存储资源抽象需要管理员手动预创建 PV
v1.0+PersistentVolumeClaim (PVC)用户通过声明请求存储静态 Provisioning 不够灵活
v1.4+StorageClass动态 Provisioning完全自动化,按需创建
v1.17+CSI (Container Storage Interface)标准化存储插件接口当前主流方案

这一演进的核心思想是关注点分离

  • 集群管理员负责配置存储后端、创建 StorageClass
  • 应用开发者只需通过 PVC 声明"我需要多大、什么类型的存储"
  • Kubernetes 控制面自动完成 PV 的创建、绑定和生命周期管理

1.3 存储生命周期

一个持久化存储卷的完整生命周期分为四个阶段:

Provisioning → Binding → Using → Reclaiming
   (供给)      (绑定)    (使用)    (回收)
  1. Provisioning(供给)

    • 静态供给:管理员预先手动创建 PV
    • 动态供给:用户创建 PVC 时,StorageClass 的 Provisioner 自动创建对应 PV
  2. Binding(绑定)

    • 控制面将 PVC 与满足条件的 PV 进行一对一绑定
    • 绑定关系一旦建立,PVC 独占该 PV
  3. Using(使用)

    • Pod 通过引用 PVC 使用存储卷
    • kubelet 负责将底层存储挂载到 Pod 的容器文件系统中
  4. Reclaiming(回收)

    • 当 PVC 被删除后,根据 PV 的回收策略决定后续处理
    • Retain:保留数据,需手动清理
    • Delete:自动删除底层存储资源

二、Volume 基础类型

2.1 emptyDir —— 临时存储

emptyDir 在 Pod 被调度到节点时创建,Pod 删除时销毁。适用于临时性数据存储。

apiVersion: v1
kind: Pod
metadata:
  name: sidecar-demo
spec:
  containers:
  - name: app
    image: busybox
    command: ["sh", "-c", "while true; do echo $(date) >> /data/app.log; sleep 5; done"]
    volumeMounts:
    - name: shared-data
      mountPath: /data
  - name: log-collector
    image: busybox
    command: ["sh", "-c", "tail -f /logs/app.log"]
    volumeMounts:
    - name: shared-data
      mountPath: /logs
  volumes:
  - name: shared-data
    emptyDir: {}

使用 Memory 作为存储介质(tmpfs):

volumes:
- name: cache-volume
  emptyDir:
    medium: Memory        # 使用内存作为存储介质
    sizeLimit: 256Mi      # 限制大小,计入容器内存限额

适用场景:

  • Sidecar 容器间的数据共享(如上例日志收集)
  • 临时计算缓存(中间文件处理)
  • 使用 medium: Memory 作为高性能临时缓存

2.2 hostPath —— 节点目录挂载

hostPath 将宿主机节点上的文件或目录挂载到 Pod 中。

apiVersion: v1
kind: Pod
metadata:
  name: hostpath-demo
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: host-data
      mountPath: /usr/share/nginx/html
  volumes:
  - name: host-data
    hostPath:
      path: /data/web      # 宿主机路径
      type: DirectoryOrCreate  # 不存在则创建

type 可选值:

  • "" :空字符串,不做任何检查
  • DirectoryOrCreate:目录不存在则创建
  • Directory:目录必须存在
  • FileOrCreate:文件不存在则创建
  • File:文件必须存在

生产环境慎用的原因:

  • Pod 调度到不同节点时,数据不一致
  • 存在安全隐患(容器可访问宿主机文件系统)
  • 破坏了 Pod 的可移植性
  • 适用场景仅限:DaemonSet 采集节点日志、访问节点设备文件(如 /dev)、单节点开发调试

2.3 configMap/secret Volume

将 ConfigMap 或 Secret 中的数据以文件形式挂载到容器中:

apiVersion: v1
kind: Pod
metadata:
  name: config-demo
spec:
  containers:
  - name: app
    image: nginx
    volumeMounts:
    - name: config-vol
      mountPath: /etc/nginx/conf.d
    - name: secret-vol
      mountPath: /etc/ssl/certs
      readOnly: true
  volumes:
  - name: config-vol
    configMap:
      name: nginx-config
      items:                    # 可选:只挂载指定 key
      - key: default.conf
        path: default.conf     # 挂载后的文件名
  - name: secret-vol
    secret:
      secretName: tls-certs
      defaultMode: 0400        # 文件权限

关键特性:

  • ConfigMap/Secret 更新后,挂载的文件会自动更新(有延迟,约 1-2 分钟)
  • 使用 subPath 挂载时不会自动更新
  • Secret 在 tmpfs 中存储,不落盘

2.4 downwardAPI Volume

将 Pod 的元数据信息以文件形式注入容器:

apiVersion: v1
kind: Pod
metadata:
  name: downward-demo
  labels:
    app: web
    version: v2
  annotations:
    build: "20240101"
spec:
  containers:
  - name: app
    image: busybox
    command: ["sh", "-c", "cat /etc/podinfo/labels && sleep 3600"]
    volumeMounts:
    - name: podinfo
      mountPath: /etc/podinfo
  volumes:
  - name: podinfo
    downwardAPI:
      items:
      - path: "labels"
        fieldRef:
          fieldPath: metadata.labels
      - path: "annotations"
        fieldRef:
          fieldPath: metadata.annotations
      - path: "cpu_limit"
        resourceFieldRef:
          containerName: app
          resource: limits.cpu

三、PersistentVolume (PV) 详解

3.1 PV 的本质

PersistentVolume 是集群级别的存储资源抽象(非命名空间资源)。它代表了一块实际的物理存储——可以是 NFS 共享目录、Ceph RBD 块设备、云厂商的云盘,或者本地磁盘。PV 由集群管理员创建和维护,其生命周期独立于任何使用它的 Pod。

3.2 访问模式(Access Modes)

访问模式缩写说明
ReadWriteOnceRWO只能被单个节点以读写方式挂载
ReadOnlyManyROX可被多个节点以只读方式挂载
ReadWriteManyRWX可被多个节点以读写方式挂载
ReadWriteOncePodRWOP只能被单个 Pod 以读写方式挂载(v1.27 GA)

注意:访问模式是存储后端的能力声明,不是强制约束。不同存储后端支持的模式不同:

  • 块存储(EBS、Ceph RBD、云盘):通常仅支持 RWO
  • 文件存储(NFS、CephFS、GlusterFS):支持 RWX
  • 对象存储不直接支持 PV 挂载

3.3 回收策略(Reclaim Policy)

策略行为适用场景
RetainPVC 删除后,PV 变为 Released 状态,数据保留,需手动清理生产环境重要数据
DeletePVC 删除后,PV 及底层存储资源一并删除动态 Provisioning 默认策略
Recycle执行 rm -rf /thevolume/* 后重新变为 Available已废弃,不建议使用

3.4 PV 状态流转

Available ──→ Bound ──→ Released ──→ Available (手动清理后)
    ↑                       │
    │                       ↓
    └──────────────── Failed (回收失败)
  • Available:空闲状态,可被 PVC 绑定
  • Bound:已与 PVC 绑定
  • Released:PVC 已删除,但 PV 尚未被回收
  • Failed:自动回收失败

3.5 静态 Provisioning 完整 YAML 示例

apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-nfs-data01
  labels:
    type: nfs
    environment: production
spec:
  capacity:
    storage: 50Gi                          # 存储容量
  accessModes:
    - ReadWriteMany                        # 支持多节点读写
  persistentVolumeReclaimPolicy: Retain    # 回收策略:保留
  storageClassName: nfs-slow               # 关联的 StorageClass 名称
  mountOptions:                            # NFS 挂载选项
    - hard
    - nfsvers=4.1
    - rsize=1048576
    - wsize=1048576
  nfs:
    server: 192.168.1.100                  # NFS 服务器地址
    path: /exports/data01                  # 导出路径
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: pv-local-ssd
spec:
  capacity:
    storage: 200Gi
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Delete
  storageClassName: local-ssd
  local:
    path: /mnt/disks/ssd0                  # 本地磁盘路径
  nodeAffinity:                            # 本地卷必须设置节点亲和性
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - node-01

3.6 核心字段说明

字段说明
capacity.storage存储容量声明
accessModes支持的访问模式列表
persistentVolumeReclaimPolicy回收策略
storageClassName关联的 StorageClass,用于与 PVC 匹配
mountOptions底层存储的挂载选项
nodeAffinity本地卷必须指定节点亲和性

四、PersistentVolumeClaim (PVC) 详解

4.1 PVC 的本质

PersistentVolumeClaim 是用户对存储资源的声明性请求。它是命名空间级别的资源,描述了应用需要多大容量、什么访问模式的存储。PVC 实现了存储消费者(开发者)与存储提供者(管理员)之间的解耦。

4.2 PVC 与 PV 的绑定规则

控制面(PV Controller)按以下规则寻找匹配的 PV:

  1. storageClassName 一致:PVC 请求的 StorageClass 必须与 PV 声明的一致
  2. accessModes 匹配:PV 的 accessModes 必须包含 PVC 请求的所有模式
  3. capacity 满足:PV 的容量必须 ≥ PVC 请求的容量
  4. selector 匹配(可选):如果 PVC 定义了 selector,则 PV 的 labels 必须满足选择条件
  5. volumeName 指定(可选):PVC 可通过 volumeName 直接指定绑定某个 PV

绑定是一对一的排他性关系。一个 PV 只能绑定一个 PVC,反之亦然。

4.3 PVC 在 Pod 中的使用方式

apiVersion: v1
kind: Pod
metadata:
  name: app-with-pvc
spec:
  containers:
  - name: app
    image: nginx:1.25
    volumeMounts:
    - name: data-volume
      mountPath: /var/www/html    # 容器内挂载路径
      subPath: website            # 可选:使用 PV 中的子目录
  volumes:
  - name: data-volume
    persistentVolumeClaim:
      claimName: my-pvc           # 引用 PVC 名称
      readOnly: false             # 是否只读挂载

4.4 完整 YAML 示例(PVC + Pod 引用)

# 1. 创建 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: app-data-pvc
  namespace: production
  labels:
    app: webapp
spec:
  storageClassName: nfs-slow        # 指定 StorageClass
  accessModes:
    - ReadWriteMany                 # 请求多节点读写
  resources:
    requests:
      storage: 20Gi                 # 请求 20Gi 容量
  selector:                         # 可选:通过标签筛选 PV
    matchLabels:
      type: nfs
      environment: production
---
# 2. Pod 使用 PVC
apiVersion: apps/v1
kind: Deployment
metadata:
  name: webapp
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: webapp
  template:
    metadata:
      labels:
        app: webapp
    spec:
      containers:
      - name: nginx
        image: nginx:1.25
        ports:
        - containerPort: 80
        volumeMounts:
        - name: web-data
          mountPath: /usr/share/nginx/html
        - name: logs
          mountPath: /var/log/nginx
      volumes:
      - name: web-data
        persistentVolumeClaim:
          claimName: app-data-pvc
      - name: logs
        emptyDir: {}

验证绑定状态:

# 查看 PVC 状态
kubectl get pvc -n production
# NAME           STATUS   VOLUME          CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# app-data-pvc   Bound    pv-nfs-data01   50Gi       RWX            nfs-slow       5m

# 查看详细绑定信息
kubectl describe pvc app-data-pvc -n production

五、StorageClass 与动态 Provisioning

5.1 为什么需要 StorageClass

在静态 Provisioning 模式下,管理员需要预先创建足够多的 PV 等待 PVC 绑定。这在大规模集群中带来严重的运维负担:

  • 无法预知应用需要多少个多大的 PV
  • 人工创建容易出错且效率低
  • 存储资源容易碎片化(预创建的 PV 容量与实际需求不匹配)

StorageClass 解决了这个问题:它定义了一类存储的"模板",当 PVC 引用某个 StorageClass 时,对应的 Provisioner 会自动创建符合要求的 PV。

5.2 StorageClass 核心字段

apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: fast-ssd
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"  # 设为默认
provisioner: ebs.csi.aws.com              # CSI 驱动名称
parameters:                                # 传递给 Provisioner 的参数
  type: gp3
  iops: "5000"
  throughput: "250"
  encrypted: "true"
reclaimPolicy: Delete                      # 动态创建 PV 的回收策略
volumeBindingMode: WaitForFirstConsumer    # 延迟绑定
allowVolumeExpansion: true                 # 允许在线扩容
mountOptions:                              # 挂载选项
  - discard
  - noatime

5.3 volumeBindingMode 详解

模式行为适用场景
ImmediatePVC 创建时立即绑定/创建 PV存储不受拓扑限制(如 NFS)
WaitForFirstConsumer等到 Pod 被调度后,根据 Pod 所在节点再创建 PV拓扑敏感存储(本地盘、云盘同 AZ)

WaitForFirstConsumer 的价值在于:

场景:集群有 3 个可用区(AZ-a, AZ-b, AZ-c)
- Immediate 模式:PV 可能创建在 AZ-a,但 Pod 被调度到 AZ-b → 挂载失败
- WaitForFirstConsumer 模式:等 Pod 调度到 AZ-b 后,再在 AZ-b 创建 PV → 确保同 AZ

5.4 allowVolumeExpansion —— 在线扩容

启用后,可通过修改 PVC 的 spec.resources.requests.storage 来触发在线扩容:

# 扩容操作
kubectl patch pvc my-pvc -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'

# 观察扩容进度
kubectl get pvc my-pvc -w
# 等待 CAPACITY 字段更新

# 查看扩容事件
kubectl describe pvc my-pvc | grep -A5 "Conditions"

注意:

  • 文件系统扩展(FileSystemResize)需要 Pod 重新挂载(部分 CSI 驱动支持在线 Resize)
  • 只能扩容,不能缩容
  • 底层存储后端必须支持在线扩容

5.5 设置默认 StorageClass

# 方式一:创建时通过 annotation 指定
# storageclass.kubernetes.io/is-default-class: "true"

# 方式二:事后修改
kubectl patch storageclass fast-ssd -p \
  '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'

# 取消默认
kubectl patch storageclass old-default -p \
  '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"false"}}}'

当 PVC 没有指定 storageClassName 时,会使用默认 StorageClass(集群中只应有一个默认)。

5.6 完整 StorageClass YAML 示例

# NFS 动态 Provisioning StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-dynamic
provisioner: nfs.csi.k8s.io
parameters:
  server: 192.168.1.100
  share: /exports/dynamic
reclaimPolicy: Delete
volumeBindingMode: Immediate
allowVolumeExpansion: true
mountOptions:
  - nfsvers=4.1
  - hard
  - rsize=1048576
  - wsize=1048576

六、NFS 存储对接实战

6.1 NFS Server 搭建

服务端配置(CentOS/RHEL):

# 安装 NFS 服务
yum install -y nfs-utils

# 创建共享目录
mkdir -p /exports/k8s-data
chmod 777 /exports/k8s-data

# 配置导出规则
cat >> /etc/exports << 'EOF'
/exports/k8s-data  192.168.1.0/24(rw,sync,no_subtree_check,no_root_squash)
EOF

# 刷新导出配置
exportfs -rav

# 启动 NFS 服务
systemctl enable --now nfs-server rpcbind

# 验证导出
showmount -e localhost

客户端验证(所有 K8s 节点):

# 安装 NFS 客户端工具
yum install -y nfs-utils    # CentOS/RHEL
apt install -y nfs-common   # Ubuntu/Debian

# 测试挂载
mount -t nfs 192.168.1.100:/exports/k8s-data /mnt/test
ls /mnt/test
umount /mnt/test

6.2 静态 PV 方式对接 NFS

# PV
apiVersion: v1
kind: PersistentVolume
metadata:
  name: nfs-pv-static
spec:
  capacity:
    storage: 100Gi
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: ""          # 空字符串表示不关联任何 StorageClass
  mountOptions:
    - hard
    - nfsvers=4.1
  nfs:
    server: 192.168.1.100
    path: /exports/k8s-data
---
# PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: nfs-pvc-static
spec:
  storageClassName: ""          # 必须与 PV 一致
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 100Gi
  volumeName: nfs-pv-static     # 直接指定 PV 名称

6.3 NFS CSI Driver 动态 Provisioning

推荐使用 Kubernetes 官方的 NFS CSI Driver:

# 添加 Helm 仓库
helm repo add csi-driver-nfs https://raw.githubusercontent.com/kubernetes-csi/csi-driver-nfs/master/charts
helm repo update

# 安装 NFS CSI Driver
helm install csi-driver-nfs csi-driver-nfs/csi-driver-nfs \
  --namespace kube-system \
  --set controller.replicas=2 \
  --set controller.runOnControlPlane=true

# 验证安装
kubectl get pods -n kube-system -l app.kubernetes.io/name=csi-driver-nfs
kubectl get csidrivers

6.4 nfs-subdir-external-provisioner 安装配置

这是另一种广泛使用的 NFS 动态 Provisioner(适合不支持 CSI 的较老集群):

# 添加 Helm 仓库
helm repo add nfs-subdir-external-provisioner \
  https://kubernetes-sigs.github.io/nfs-subdir-external-provisioner/
helm repo update

# 安装
helm install nfs-provisioner nfs-subdir-external-provisioner/nfs-subdir-external-provisioner \
  --namespace kube-system \
  --set nfs.server=192.168.1.100 \
  --set nfs.path=/exports/k8s-data \
  --set storageClass.name=nfs-dynamic \
  --set storageClass.defaultClass=false \
  --set storageClass.reclaimPolicy=Delete \
  --set storageClass.archiveOnDelete=true \
  --set storageClass.accessModes=ReadWriteMany

# 验证
kubectl get storageclass nfs-dynamic
kubectl get pods -n kube-system | grep nfs-provisioner

6.5 完整端到端示例

# Step 1: StorageClass(使用 NFS CSI Driver)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: nfs-csi
provisioner: nfs.csi.k8s.io
parameters:
  server: 192.168.1.100
  share: /exports/k8s-data
  subDir: ${pvc.metadata.namespace}/${pvc.metadata.name}  # 按命名空间/PVC名创建子目录
reclaimPolicy: Delete
volumeBindingMode: Immediate
mountOptions:
  - nfsvers=4.1
  - hard
---
# Step 2: PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: test-nfs-pvc
  namespace: default
spec:
  storageClassName: nfs-csi
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 5Gi
---
# Step 3: 验证 Pod
apiVersion: v1
kind: Pod
metadata:
  name: nfs-test-pod
  namespace: default
spec:
  containers:
  - name: writer
    image: busybox
    command: ["sh", "-c", "echo 'Hello NFS Persistent Storage!' > /data/test.txt && cat /data/test.txt && sleep 3600"]
    volumeMounts:
    - name: nfs-data
      mountPath: /data
  volumes:
  - name: nfs-data
    persistentVolumeClaim:
      claimName: test-nfs-pvc

验证数据持久化:

# 1. 部署
kubectl apply -f nfs-demo.yaml

# 2. 确认 PVC 绑定
kubectl get pvc test-nfs-pvc
# STATUS 应为 Bound

# 3. 查看 Pod 日志
kubectl logs nfs-test-pod
# 输出: Hello NFS Persistent Storage!

# 4. 删除 Pod 并重建
kubectl delete pod nfs-test-pod
kubectl apply -f nfs-demo.yaml  # 只创建 Pod 部分

# 5. 验证数据依然存在
kubectl exec nfs-test-pod -- cat /data/test.txt
# 输出: Hello NFS Persistent Storage!

6.6 NFS 的优缺点与注意事项

优点缺点
支持 RWX 多节点读写性能相对较低(网络 I/O 瓶颈)
部署简单,成本低单点故障(NFS Server)
兼容性好,几乎所有系统支持文件锁机制在容器场景下可能有问题
支持在线扩容不适合高 IOPS 场景(数据库等)

注意事项:

  • 生产环境务必做 NFS Server 高可用(Keepalived + DRBD 或使用企业级 NAS)
  • 关注 no_root_squash 的安全隐患
  • 大文件场景调大 rsize/wsize(建议 1MB)
  • 监控 NFS Server 的网络带宽和磁盘 I/O

七、Ceph 存储对接实战

7.1 Ceph RBD(块存储)对接

Ceph RBD(RADOS Block Device)提供高性能块存储,适合数据库等需要 RWO 的场景。

7.1.1 Ceph 集群准备
# 在 Ceph 集群上操作

# 创建专用存储池
ceph osd pool create k8s-rbd-pool 128 128
ceph osd pool application enable k8s-rbd-pool rbd

# 初始化 pool
rbd pool init k8s-rbd-pool

# 创建专用用户(限制权限)
ceph auth get-or-create client.k8s-rbd \
  mon 'profile rbd' \
  osd 'profile rbd pool=k8s-rbd-pool' \
  mgr 'profile rbd pool=k8s-rbd-pool'

# 获取 key(后续配置需要)
ceph auth get-key client.k8s-rbd | base64
# 输出: QVFBdmxxxxxxxxxxxxxxxxxxxx==

# 获取集群 ID
ceph fsid
# 输出: 7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c
7.1.2 ceph-csi 安装
# 添加 Helm 仓库
helm repo add ceph-csi https://ceph.github.io/csi-charts
helm repo update

# 创建命名空间
kubectl create namespace ceph-csi

# 准备配置
cat > ceph-csi-rbd-values.yaml << 'EOF'
csiConfig:
  - clusterID: "7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
    monitors:
      - "192.168.1.10:6789"
      - "192.168.1.11:6789"
      - "192.168.1.12:6789"

provisioner:
  replicaCount: 2

storageClass:
  create: true
  name: ceph-rbd
  clusterID: "7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
  pool: k8s-rbd-pool
  imageFeatures: "layering"
  reclaimPolicy: Delete
  allowVolumeExpansion: true

secret:
  create: true
  userID: k8s-rbd
  userKey: QVFBdmxxxxxxxxxxxxxxxxxxxx==
EOF

# 安装 ceph-csi-rbd
helm install ceph-csi-rbd ceph-csi/ceph-csi-rbd \
  --namespace ceph-csi \
  -f ceph-csi-rbd-values.yaml

# 验证
kubectl get pods -n ceph-csi
kubectl get storageclass ceph-rbd
7.1.3 StorageClass 配置(手动方式)
# Secret(存储 Ceph 认证信息)
apiVersion: v1
kind: Secret
metadata:
  name: csi-rbd-secret
  namespace: ceph-csi
type: kubernetes.io/rbd
stringData:
  userID: k8s-rbd
  userKey: QVFBdmxxxxxxxxxxxxxxxxxxxx==
---
# StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ceph-rbd-ssd
provisioner: rbd.csi.ceph.com
parameters:
  clusterID: "7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
  pool: k8s-rbd-pool
  imageFormat: "2"
  imageFeatures: "layering,exclusive-lock,object-map,fast-diff"
  csi.storage.k8s.io/provisioner-secret-name: csi-rbd-secret
  csi.storage.k8s.io/provisioner-secret-namespace: ceph-csi
  csi.storage.k8s.io/controller-expand-secret-name: csi-rbd-secret
  csi.storage.k8s.io/controller-expand-secret-namespace: ceph-csi
  csi.storage.k8s.io/node-stage-secret-name: csi-rbd-secret
  csi.storage.k8s.io/node-stage-secret-namespace: ceph-csi
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
7.1.4 RWO 场景实战
# MySQL 使用 Ceph RBD
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data
spec:
  storageClassName: ceph-rbd-ssd
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql
spec:
  replicas: 1              # RBD 是块设备,只能单 Pod 挂载
  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-storage
          mountPath: /var/lib/mysql
        resources:
          requests:
            memory: "512Mi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "2000m"
      volumes:
      - name: mysql-storage
        persistentVolumeClaim:
          claimName: mysql-data

7.2 CephFS(文件存储)对接

7.2.1 RWX 场景(多 Pod 共享)
# Ceph 集群准备
# 确保 MDS 服务运行
ceph fs ls
# 如果没有文件系统,创建一个
ceph osd pool create cephfs-metadata 32
ceph osd pool create cephfs-data 128
ceph fs new k8s-cephfs cephfs-metadata cephfs-data

# 创建 SubVolumeGroup
ceph fs subvolumegroup create k8s-cephfs csi

# 创建认证用户
ceph auth get-or-create client.k8s-cephfs \
  mon 'allow r' \
  osd 'allow rw pool=cephfs-data' \
  mds 'allow rw' \
  mgr 'allow rw'
7.2.2 CephFS StorageClass 配置
apiVersion: v1
kind: Secret
metadata:
  name: csi-cephfs-secret
  namespace: ceph-csi
stringData:
  adminID: k8s-cephfs
  adminKey: QVFCeHhxxxxxxxxxxxxxxxx==
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: cephfs-shared
provisioner: cephfs.csi.ceph.com
parameters:
  clusterID: "7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
  fsName: k8s-cephfs
  pool: cephfs-data
  csi.storage.k8s.io/provisioner-secret-name: csi-cephfs-secret
  csi.storage.k8s.io/provisioner-secret-namespace: ceph-csi
  csi.storage.k8s.io/controller-expand-secret-name: csi-cephfs-secret
  csi.storage.k8s.io/controller-expand-secret-namespace: ceph-csi
  csi.storage.k8s.io/node-stage-secret-name: csi-cephfs-secret
  csi.storage.k8s.io/node-stage-secret-namespace: ceph-csi
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
---
# 多 Pod 共享 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: shared-data
spec:
  storageClassName: cephfs-shared
  accessModes:
    - ReadWriteMany             # CephFS 支持 RWX
  resources:
    requests:
      storage: 100Gi

7.3 Rook-Ceph 一体化方案

Rook 是 Kubernetes 上部署 Ceph 的最佳实践方案——将 Ceph 集群完全运行在 K8s 内部:

# 部署 Rook Operator
helm repo add rook-release https://charts.rook.io/release
helm repo update

helm install rook-ceph rook-release/rook-ceph \
  --namespace rook-ceph \
  --create-namespace \
  --set csi.enableRbdDriver=true \
  --set csi.enableCephfsDriver=true

# 等待 Operator 就绪
kubectl -n rook-ceph wait --for=condition=Ready pod -l app=rook-ceph-operator --timeout=300s

# 部署 Ceph 集群(使用集群内节点的裸盘)
cat > ceph-cluster.yaml << 'EOF'
apiVersion: ceph.rook.io/v1
kind: CephCluster
metadata:
  name: rook-ceph
  namespace: rook-ceph
spec:
  cephVersion:
    image: quay.io/ceph/ceph:v18.2
  dataDirHostPath: /var/lib/rook
  mon:
    count: 3
    allowMultiplePerNode: false
  mgr:
    count: 2
  dashboard:
    enabled: true
  storage:
    useAllNodes: true
    useAllDevices: false
    devices:
    - name: "sdb"              # 指定使用的裸盘
    - name: "sdc"
EOF

kubectl apply -f ceph-cluster.yaml

# 创建 Block Pool 和 StorageClass
cat > ceph-block-pool.yaml << 'EOF'
apiVersion: ceph.rook.io/v1
kind: CephBlockPool
metadata:
  name: replicapool
  namespace: rook-ceph
spec:
  failureDomain: host
  replicated:
    size: 3
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: rook-ceph-block
provisioner: rook-ceph.rbd.csi.ceph.com
parameters:
  clusterID: rook-ceph
  pool: replicapool
  imageFormat: "2"
  imageFeatures: layering
  csi.storage.k8s.io/provisioner-secret-name: rook-csi-rbd-provisioner
  csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
  csi.storage.k8s.io/node-stage-secret-name: rook-csi-rbd-node
  csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
EOF

kubectl apply -f ceph-block-pool.yaml

7.4 RBD vs CephFS 对比

维度Ceph RBDCephFS
存储类型块存储文件存储
访问模式RWO(单节点读写)RWX(多节点读写)
性能高(直接块 I/O)中等(文件系统开销)
适用场景数据库、单实例有状态应用多 Pod 共享数据、CMS、日志存储
文件系统需要在 RBD 上格式化(ext4/xfs)自带 POSIX 兼容文件系统
快照支持支持支持
扩展性优秀优秀
运维复杂度中高(需要 MDS)

八、云盘存储对接

8.1 阿里云云盘(ACK + Disk CSI)

# 阿里云 ESSD StorageClass
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: alicloud-disk-essd-pl1
provisioner: diskplugin.csi.alibabacloud.com
parameters:
  type: cloud_essd           # cloud_efficiency | cloud_ssd | cloud_essd
  performanceLevel: PL1      # PL0 | PL1 | PL2 | PL3
  encrypted: "true"
  fstype: ext4
  # 按需付费
  # chargeType: PostPaid
  # 包年包月(需配合 annotation)
  # chargeType: PrePaid
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer   # 确保云盘与 Pod 同可用区
---
# 高性能 ESSD PL3(面向数据库)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: alicloud-disk-essd-pl3
provisioner: diskplugin.csi.alibabacloud.com
parameters:
  type: cloud_essd
  performanceLevel: PL3      # 最高 1000000 IOPS
  fstype: xfs                # XFS 对大文件和并发写入更友好
reclaimPolicy: Retain        # 生产数据保留
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

使用 PVC:

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: db-data
  annotations:
    # 快照恢复(可选)
    # volume.alibabacloud.com/snapshot-id: "s-xxxxxxxxxx"
spec:
  storageClassName: alicloud-disk-essd-pl1
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi

8.2 AWS EBS(EKS + EBS CSI Driver)

# 安装 EBS CSI Driver(EKS 推荐方式)
eksctl create addon --name aws-ebs-csi-driver --cluster my-cluster \
  --service-account-role-arn arn:aws:iam::111122223333:role/AmazonEKS_EBS_CSI_DriverRole \
  --force
# gp3 StorageClass(推荐默认)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-gp3
  annotations:
    storageclass.kubernetes.io/is-default-class: "true"
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
  iops: "3000"              # gp3 基线 3000 IOPS(免费)
  throughput: "125"         # 基线 125 MiB/s
  encrypted: "true"
  kmsKeyId: "arn:aws:kms:us-east-1:111122223333:key/xxxx"  # 可选 KMS 加密
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
---
# io2 StorageClass(高性能数据库)
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ebs-io2
provisioner: ebs.csi.aws.com
parameters:
  type: io2
  iops: "10000"
  encrypted: "true"
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

8.3 腾讯云 CBS(TKE)

# 腾讯云高性能云盘
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: cbs-premium
provisioner: com.tencent.cloud.csi.cbs
parameters:
  diskType: CLOUD_PREMIUM        # CLOUD_PREMIUM | CLOUD_SSD | CLOUD_HSSD
  diskChargeType: POSTPAID_BY_HOUR
  encrypt: "ENCRYPT"
  # aspId: "asp-xxxxx"           # 可选:关联快照策略
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
---
# 腾讯云增强型 SSD
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: cbs-hssd
provisioner: com.tencent.cloud.csi.cbs
parameters:
  diskType: CLOUD_HSSD
  diskChargeType: POSTPAID_BY_HOUR
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

8.4 通用配置模式

各云厂商的 CSI 存储对接遵循统一的架构模式:

CSI Driver (DaemonSet + Deployment)
         ↓
    StorageClass (定义存储模板)
         ↓
    PVC (用户声明)
         ↓
    CSI Provisioner 调用云 API 创建云盘
         ↓
    PV 自动创建并绑定
         ↓
    kubelet 通过 CSI Node Plugin 完成 Attach + Mount

关键 Annotations(通用):

Annotation作用
volume.kubernetes.io/selected-node记录 PV 所在节点
pv.kubernetes.io/provisioned-by记录 Provisioner 名称
volume.beta.kubernetes.io/storage-provisioner兼容旧版标注

九、StatefulSet 与存储的配合

9.1 volumeClaimTemplates 详解

StatefulSet 通过 volumeClaimTemplates 为每个 Pod 副本自动创建独立的 PVC:

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: redis-cluster
spec:
  serviceName: redis-cluster
  replicas: 6
  selector:
    matchLabels:
      app: redis-cluster
  template:
    metadata:
      labels:
        app: redis-cluster
    spec:
      containers:
      - name: redis
        image: redis:7.2
        ports:
        - containerPort: 6379
          name: client
        - containerPort: 16379
          name: gossip
        command: ["redis-server", "/etc/redis/redis.conf"]
        volumeMounts:
        - name: redis-data
          mountPath: /data
        - name: redis-config
          mountPath: /etc/redis
        resources:
          requests:
            cpu: "200m"
            memory: "256Mi"
          limits:
            cpu: "1000m"
            memory: "2Gi"
      volumes:
      - name: redis-config
        configMap:
          name: redis-cluster-config
  volumeClaimTemplates:              # 关键:PVC 模板
  - metadata:
      name: redis-data               # PVC 名称前缀
    spec:
      storageClassName: ceph-rbd-ssd
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 10Gi

9.2 有序创建 PVC

StatefulSet 创建的 PVC 命名遵循固定规则:

{volumeClaimTemplate.name}-{statefulset.name}-{ordinal}

对于上述 Redis 集群示例,会创建:

redis-data-redis-cluster-0
redis-data-redis-cluster-1
redis-data-redis-cluster-2
redis-data-redis-cluster-3
redis-data-redis-cluster-4
redis-data-redis-cluster-5
# 查看 StatefulSet 创建的所有 PVC
kubectl get pvc -l app=redis-cluster
# NAME                          STATUS   VOLUME       CAPACITY   STORAGECLASS
# redis-data-redis-cluster-0    Bound    pvc-a1b2c3   10Gi       ceph-rbd-ssd
# redis-data-redis-cluster-1    Bound    pvc-d4e5f6   10Gi       ceph-rbd-ssd
# redis-data-redis-cluster-2    Bound    pvc-g7h8i9   10Gi       ceph-rbd-ssd
# ...

9.3 扩缩容对 PVC 的影响

操作PVC 行为
扩容(replicas 增加)自动创建新的 PVC(如 redis-data-redis-cluster-6)
缩容(replicas 减少)PVC 不会自动删除(数据保留,需手动清理)
删除 StatefulSetPVC 不会自动删除(即使设置了级联删除)

重要提示:缩容后遗留的 PVC 需要手动清理,否则会造成存储资源浪费。但这也是一种安全机制——防止误缩容导致数据丢失。

# 手动清理缩容后遗留的 PVC
kubectl delete pvc redis-data-redis-cluster-5
kubectl delete pvc redis-data-redis-cluster-4

9.4 完整 StatefulSet + 存储示例(MySQL 主从)

# MySQL 主从 StatefulSet
apiVersion: v1
kind: ConfigMap
metadata:
  name: mysql-config
data:
  primary.cnf: |
    [mysqld]
    server-id=1
    log-bin=mysql-bin
    binlog-format=ROW
    gtid-mode=ON
    enforce-gtid-consistency=ON
  replica.cnf: |
    [mysqld]
    server-id=2
    relay-log=relay-bin
    read-only=ON
    gtid-mode=ON
    enforce-gtid-consistency=ON
---
apiVersion: v1
kind: Service
metadata:
  name: mysql
  labels:
    app: mysql
spec:
  ports:
  - port: 3306
    name: mysql
  clusterIP: None           # Headless Service
  selector:
    app: mysql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
spec:
  serviceName: mysql
  replicas: 3
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      initContainers:
      - name: init-mysql
        image: mysql:8.0
        command:
        - bash
        - "-c"
        - |
          set -ex
          # 根据 Pod 序号生成 server-id
          [[ $(hostname) =~ -([0-9]+)$ ]] || exit 1
          ordinal=${BASH_REMATCH[1]}
          echo "[mysqld]" > /mnt/conf.d/server-id.cnf
          echo "server-id=$((100 + $ordinal))" >> /mnt/conf.d/server-id.cnf
          # 序号 0 为主节点
          if [[ $ordinal -eq 0 ]]; then
            cp /mnt/config-map/primary.cnf /mnt/conf.d/
          else
            cp /mnt/config-map/replica.cnf /mnt/conf.d/
          fi
        volumeMounts:
        - name: conf
          mountPath: /mnt/conf.d
        - name: config-map
          mountPath: /mnt/config-map
      containers:
      - name: mysql
        image: mysql:8.0
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: root-password
        ports:
        - containerPort: 3306
          name: mysql
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        - name: conf
          mountPath: /etc/mysql/conf.d
        resources:
          requests:
            cpu: "500m"
            memory: "1Gi"
          limits:
            cpu: "2000m"
            memory: "4Gi"
        livenessProbe:
          exec:
            command: ["mysqladmin", "ping", "-uroot", "-p${MYSQL_ROOT_PASSWORD}"]
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command: ["mysql", "-uroot", "-p${MYSQL_ROOT_PASSWORD}", "-e", "SELECT 1"]
          initialDelaySeconds: 5
          periodSeconds: 5
      volumes:
      - name: conf
        emptyDir: {}
      - name: config-map
        configMap:
          name: mysql-config
  volumeClaimTemplates:
  - metadata:
      name: mysql-data
    spec:
      storageClassName: ceph-rbd-ssd
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 50Gi

十、存储常见问题排查

10.1 PVC 一直 Pending

现象:

kubectl get pvc
# NAME       STATUS    VOLUME   CAPACITY   ACCESS MODES   STORAGECLASS   AGE
# my-pvc     Pending                                      fast-ssd       5m

排查步骤:

# 第一步:查看 PVC 事件
kubectl describe pvc my-pvc

# 常见原因及解决方案:
事件信息原因解决方案
no persistent volumes available没有匹配的 PV(静态模式)创建满足条件的 PV
storageclass "xxx" not foundStorageClass 不存在检查 SC 名称拼写,创建对应 SC
waiting for first consumervolumeBindingMode 为 WaitForFirstConsumer正常现象,需要有 Pod 使用该 PVC
insufficient quota配额不足联系管理员扩大 ResourceQuota
provisioning failedProvisioner 创建存储失败查看 Provisioner Pod 日志
# 查看 Provisioner Pod 日志(以 NFS CSI 为例)
kubectl logs -n kube-system -l app.kubernetes.io/name=csi-driver-nfs -c nfs

# 查看存储类是否存在
kubectl get sc

# 检查 CSI Driver 状态
kubectl get csidrivers
kubectl get csinodes

10.2 Pod 挂载失败

现象:

kubectl get pod my-pod
# NAME     READY   STATUS              RESTARTS   AGE
# my-pod   0/1     ContainerCreating   0          3m

排查步骤:

# 查看 Pod 事件
kubectl describe pod my-pod

# 常见错误信息:
# - "Multi-Attach error": PV 已被其他节点挂载(RWO 限制)
# - "mount failed: exit status 32": 节点无法连接存储后端
# - "rpc error: code = Internal": CSI 插件内部错误

Multi-Attach 问题解决:

# 找出谁在使用该 PV
kubectl get pv <pv-name> -o yaml | grep -A5 claimRef

# 找出使用该 PVC 的 Pod
kubectl get pods --all-namespaces -o json | \
  jq '.items[] | select(.spec.volumes[]?.persistentVolumeClaim.claimName == "my-pvc") | .metadata.name'

# 如果是旧 Pod 未正常终止导致 VolumeAttachment 残留
kubectl get volumeattachments
kubectl delete volumeattachment <name>  # 谨慎操作!确认旧 Pod 确实已终止

NFS 挂载失败排查:

# 在问题节点上测试 NFS 连通性
showmount -e 192.168.1.100
mount -t nfs 192.168.1.100:/exports/k8s-data /mnt/test

# 检查节点是否安装了 nfs-utils
rpm -qa | grep nfs-utils   # CentOS
dpkg -l | grep nfs-common  # Ubuntu

10.3 数据丢失排查

场景: 删除 PVC 后数据丢失

# 检查 StorageClass 的回收策略
kubectl get sc <storageclass-name> -o yaml | grep reclaimPolicy
# 如果是 Delete,PVC 删除时 PV 和底层数据会一并删除!

# 检查已有 PV 的回收策略
kubectl get pv -o custom-columns=NAME:.metadata.name,RECLAIM:.spec.persistentVolumeReclaimPolicy

# 修改 PV 回收策略为 Retain(保护现有数据)
kubectl patch pv <pv-name> -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'

数据恢复(Retain 策略下):

当 PVC 被删除、PV 变为 Released 状态后,需要手动操作才能重新使用:

# 1. 查看 Released 状态的 PV
kubectl get pv | grep Released

# 2. 清除 PV 的 claimRef(使其重新变为 Available)
kubectl patch pv <pv-name> --type json -p '[{"op":"remove","path":"/spec/claimRef"}]'

# 3. 创建新的 PVC 绑定到该 PV
# 使用 volumeName 指定绑定

10.4 扩容失败

# 检查 StorageClass 是否启用扩容
kubectl get sc <sc-name> -o yaml | grep allowVolumeExpansion
# 如果没有或为 false,需要先修改 SC:
kubectl patch sc <sc-name> -p '{"allowVolumeExpansion": true}'

# 检查扩容进度
kubectl describe pvc <pvc-name>
# 关注 Conditions 中的 FileSystemResizePending

# 部分存储需要 Pod 重启才能完成文件系统扩展
kubectl delete pod <pod-name>  # Pod 重建后触发 resize

10.5 排查命令汇总

# ========== 全局状态 ==========
# 查看所有 StorageClass
kubectl get sc

# 查看所有 PV 及状态
kubectl get pv -o wide

# 查看指定命名空间 PVC
kubectl get pvc -n <namespace> -o wide

# ========== 详细诊断 ==========
# PV 详情(查看 capacity、accessModes、claimRef)
kubectl describe pv <pv-name>

# PVC 详情(查看 Events、Conditions)
kubectl describe pvc <pvc-name> -n <namespace>

# Pod 挂载详情
kubectl describe pod <pod-name> -n <namespace> | grep -A20 "Volumes:"

# ========== 事件排查 ==========
# 查看存储相关事件
kubectl get events --field-selector reason=ProvisioningFailed -A
kubectl get events --field-selector reason=FailedMount -A
kubectl get events --field-selector involvedObject.kind=PersistentVolumeClaim -A

# ========== CSI 排查 ==========
# CSI Driver 状态
kubectl get csidrivers
kubectl get csinodes

# CSI 插件 Pod 日志
kubectl logs -n kube-system <csi-controller-pod> -c csi-provisioner
kubectl logs -n kube-system <csi-node-pod> -c csi-driver

# VolumeAttachment(卷挂载状态)
kubectl get volumeattachments -o wide

# ========== 节点层面 ==========
# 查看节点上已挂载的卷
kubectl get pods -o json --field-selector spec.nodeName=<node> | \
  jq '.items[].spec.volumes[] | select(.persistentVolumeClaim)'

# 进入节点查看实际挂载
# ssh node-01
# mount | grep pvc
# df -h | grep pvc

十一、最佳实践总结

11.1 生产环境存储选型建议

应用类型推荐存储理由
数据库(MySQL/PostgreSQL)Ceph RBD / 云盘 ESSD高 IOPS,RWO 满足需求
消息队列(Kafka/RabbitMQ)本地 SSD + Ceph RBD极致性能选本地盘,可靠性选 RBD
文件共享(CMS、媒体)CephFS / NFSRWX 多 Pod 共享
日志存储CephFS / 对象存储大容量、顺序写入
AI/ML 训练数据CephFS / 高性能 NAS大文件、高带宽
临时缓存emptyDir (Memory)高速、重启可丢
CI/CD 构建emptyDir / 本地 SSD临时性、高速

通用原则:

  1. 性能优先:数据库、中间件选块存储(RBD/云盘)
  2. 共享优先:多 Pod 共享选文件存储(CephFS/NFS)
  3. 成本敏感:评估实际 IOPS 需求,避免过度配置
  4. 数据安全:生产数据务必使用 Retain 回收策略
  5. 拓扑感知:使用 WaitForFirstConsumer 避免跨 AZ 挂载

11.2 备份策略(VolumeSnapshot)

Kubernetes 从 v1.20 起 GA 支持 VolumeSnapshot,可实现存储卷的快照备份:

# VolumeSnapshotClass
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
  name: ceph-rbd-snapshot
driver: rbd.csi.ceph.com
deletionPolicy: Delete
parameters:
  clusterID: "7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c"
  csi.storage.k8s.io/snapshotter-secret-name: csi-rbd-secret
  csi.storage.k8s.io/snapshotter-secret-namespace: ceph-csi
---
# 创建快照
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
  name: mysql-data-snapshot-20240101
spec:
  volumeSnapshotClassName: ceph-rbd-snapshot
  source:
    persistentVolumeClaimName: mysql-data-mysql-0  # 要备份的 PVC
---
# 从快照恢复新 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-data-restored
spec:
  storageClassName: ceph-rbd-ssd
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi
  dataSource:
    name: mysql-data-snapshot-20240101
    kind: VolumeSnapshot
    apiGroup: snapshot.storage.k8s.io

定期备份 CronJob:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: pvc-snapshot-backup
spec:
  schedule: "0 2 * * *"         # 每天凌晨 2 点
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: snapshot-creator
          containers:
          - name: snapshot-creator
            image: bitnami/kubectl:latest
            command:
            - /bin/sh
            - -c
            - |
              DATE=$(date +%Y%m%d-%H%M%S)
              cat <<SNAP | kubectl apply -f -
              apiVersion: snapshot.storage.k8s.io/v1
              kind: VolumeSnapshot
              metadata:
                name: mysql-backup-${DATE}
              spec:
                volumeSnapshotClassName: ceph-rbd-snapshot
                source:
                  persistentVolumeClaimName: mysql-data-mysql-0
              SNAP
              # 清理 7 天前的快照
              kubectl get volumesnapshots -o json | \
                jq -r '.items[] | select(.metadata.creationTimestamp < (now - 604800 | strftime("%Y-%m-%dT%H:%M:%SZ"))) | .metadata.name' | \
                xargs -r kubectl delete volumesnapshot
          restartPolicy: OnFailure

11.3 监控存储容量

使用 Prometheus + Grafana 监控:

关键监控指标:

# kubelet 暴露的 PVC 容量指标
kubelet_volume_stats_capacity_bytes{persistentvolumeclaim="xxx"}
kubelet_volume_stats_available_bytes{persistentvolumeclaim="xxx"}
kubelet_volume_stats_used_bytes{persistentvolumeclaim="xxx"}
kubelet_volume_stats_inodes{persistentvolumeclaim="xxx"}
kubelet_volume_stats_inodes_free{persistentvolumeclaim="xxx"}
kubelet_volume_stats_inodes_used{persistentvolumeclaim="xxx"}

PrometheusRule 告警规则:

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: storage-alerts
  namespace: monitoring
spec:
  groups:
  - name: storage.rules
    rules:
    # PVC 容量使用超过 80% 告警
    - alert: PVCUsageHigh
      expr: |
        (kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes) > 0.8
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "PVC {{ $labels.persistentvolumeclaim }} 使用率超过 80%"
        description: "命名空间 {{ $labels.namespace }} 的 PVC {{ $labels.persistentvolumeclaim }} 已使用 {{ $value | humanizePercentage }},请及时扩容。"

    # PVC 容量使用超过 90% 紧急告警
    - alert: PVCUsageCritical
      expr: |
        (kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes) > 0.9
      for: 2m
      labels:
        severity: critical
      annotations:
        summary: "PVC {{ $labels.persistentvolumeclaim }} 使用率超过 90%(紧急)"
        description: "命名空间 {{ $labels.namespace }} 的 PVC {{ $labels.persistentvolumeclaim }} 已使用 {{ $value | humanizePercentage }},即将写满!"

    # inode 使用率告警
    - alert: PVCInodesHigh
      expr: |
        (kubelet_volume_stats_inodes_used / kubelet_volume_stats_inodes) > 0.85
      for: 5m
      labels:
        severity: warning
      annotations:
        summary: "PVC {{ $labels.persistentvolumeclaim }} inode 使用率超过 85%"

    # PV 处于 Failed 状态
    - alert: PVFailed
      expr: kube_persistentvolume_status_phase{phase="Failed"} == 1
      for: 1m
      labels:
        severity: critical
      annotations:
        summary: "PV {{ $labels.persistentvolume }} 处于 Failed 状态"
        description: "请立即检查该 PV 对应的存储后端状态。"

日常巡检脚本:

#!/bin/bash
# storage-check.sh - K8s 存储日常巡检

echo "========== PV 状态汇总 =========="
kubectl get pv -o custom-columns=\
NAME:.metadata.name,\
CAPACITY:.spec.capacity.storage,\
ACCESS:.spec.accessModes[0],\
RECLAIM:.spec.persistentVolumeReclaimPolicy,\
STATUS:.status.phase,\
STORAGECLASS:.spec.storageClassName

echo ""
echo "========== 异常 PVC(非 Bound 状态) =========="
kubectl get pvc -A --field-selector='status.phase!=Bound'

echo ""
echo "========== Released 状态 PV(需关注) =========="
kubectl get pv --field-selector='status.phase=Released'

echo ""
echo "========== PVC 容量使用 Top10 =========="
kubectl get --raw /api/v1/persistentvolumeclaims | \
  jq -r '.items[] | "\(.metadata.namespace)/\(.metadata.name) \(.spec.resources.requests.storage)"' | \
  sort -k2 -hr | head -10

echo ""
echo "========== 存储相关事件(最近1小时) =========="
kubectl get events -A --field-selector reason=ProvisioningFailed --sort-by=.metadata.creationTimestamp | tail -20
kubectl get events -A --field-selector reason=FailedMount --sort-by=.metadata.creationTimestamp | tail -20

总结

Kubernetes 持久化存储体系的设计遵循了"声明式 API + 关注点分离"的核心理念。从简单的 Volume 到完整的 PV/PVC/StorageClass 动态供给体系,再到 CSI 标准化插件接口,存储架构不断演进以适应云原生场景下的多样化需求。

在实际生产落地中,关键要把握以下几点:

  1. 选型匹配:根据应用特征(IOPS需求、共享需求、数据重要性)选择合适的存储后端
  2. 自动化运维:通过 StorageClass + CSI 实现全自动化的存储生命周期管理
  3. 安全兜底:生产环境必须配置 Retain 回收策略 + 定期快照备份
  4. 容量监控:建立完善的存储容量告警体系,防止容量耗尽导致业务中断
  5. 拓扑感知:使用 WaitForFirstConsumer 绑定模式确保存储和计算在同一拓扑域

掌握了本文的知识体系,你应该能够自信地设计和实施任何规模的 Kubernetes 持久化存储方案。


参考资料:

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐