本文系统性地讲解 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)

访问模式 缩写 说明
ReadWriteOnce RWO 只能被单个节点以读写方式挂载
ReadOnlyMany ROX 可被多个节点以只读方式挂载
ReadWriteMany RWX 可被多个节点以读写方式挂载
ReadWriteOncePod RWOP 只能被单个 Pod 以读写方式挂载(v1.27 GA)

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

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

3.3 回收策略(Reclaim Policy)

策略 行为 适用场景
Retain PVC 删除后,PV 变为 Released 状态,数据保留,需手动清理 生产环境重要数据
Delete PVC 删除后,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 详解

模式 行为 适用场景
Immediate PVC 创建时立即绑定/创建 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 RBD CephFS
存储类型 块存储 文件存储
访问模式 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 不会自动删除(数据保留,需手动清理)
删除 StatefulSet PVC 不会自动删除(即使设置了级联删除)

重要提示:缩容后遗留的 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 found StorageClass 不存在 检查 SC 名称拼写,创建对应 SC
waiting for first consumer volumeBindingMode 为 WaitForFirstConsumer 正常现象,需要有 Pod 使用该 PVC
insufficient quota 配额不足 联系管理员扩大 ResourceQuota
provisioning failed Provisioner 创建存储失败 查看 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 / NFS RWX 多 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 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐