一、概述

1.1 背景介绍

容器本身是无状态的,Pod重启后容器内的数据全部丢失。数据库、消息队列、文件存储这类有状态服务跑在K8s上,必须解决持久化存储问题。Kubernetes通过PersistentVolume(PV)、PersistentVolumeClaim(PVC)和StorageClass三层抽象来管理存储。

实际生产中踩过的坑:开发团队直接在Pod里用hostPath挂载宿主机目录,Pod漂移到其他节点后数据就丢了。还有团队手动创建了100个PV,每次扩容都要运维手动操作,效率极低。StorageClass的动态供给机制就是解决这个问题的。

本文覆盖静态供给、动态供给、本地存储、NFS、Ceph RBD等常见存储方案,基于Kubernetes 1.28.x版本。

1.2 技术特点

  • 存储与计算解耦:PV是集群级别的存储资源,PVC是用户对存储的申请,两者通过绑定关系关联,Pod只需要引用PVC

  • 动态供给:StorageClass配合Provisioner自动创建PV,无需运维手动干预

  • 访问模式控制:ReadWriteOnce(单节点读写)、ReadOnlyMany(多节点只读)、ReadWriteMany(多节点读写),不同存储后端支持的模式不同

  • 回收策略:Retain(保留数据)、Delete(删除数据)、Recycle(已废弃),控制PVC删除后PV的处理方式

1.3 适用场景

  • 场景一:数据库(MySQL、PostgreSQL)持久化存储,要求高IOPS和数据安全

  • 场景二:日志、文件上传等共享存储,多个Pod需要同时读写同一份数据

  • 场景三:AI训练数据集存储,大容量、高吞吐量读取

  • 场景四:StatefulSet有状态应用,每个Pod需要独立的持久化卷

1.4 环境要求

组件

版本要求

说明

Kubernetes

1.24+

CSI驱动在1.13 GA,本文使用1.28

NFS Server

NFSv4

共享存储方案,需要独立NFS服务器

Ceph

16.x+ (Pacific)

分布式存储方案,生产推荐

nfs-subdir-external-provisioner

4.0+

NFS动态供给控制器

ceph-csi

3.9+

Ceph CSI驱动

存储节点磁盘

SSD/HDD按需

数据库用SSD,归档用HDD


二、详细步骤

2.1 准备工作

2.1.1 存储基础概念

PV、PVC、StorageClass的关系:

用户视角:Pod → PVC(我需要10Gi存储)
                ↓ 绑定
管理员视角:    PV(这块10Gi的存储可以用)
                ↑ 创建
自动化视角:    StorageClass + Provisioner(自动创建PV)
# 查看集群现有的StorageClass
kubectl get sc

# 查看PV和PVC
kubectl get pv
kubectl get pvc -A

# 查看CSI驱动
kubectl get csidrivers
2.1.2 存储方案选型

存储方案

访问模式

性能

适用场景

运维复杂度

hostPath

RWO

高(本地磁盘)

测试环境,单节点

Local PV

RWO

高(本地SSD)

数据库,对IO敏感

NFS

RWO/ROX/RWX

共享文件存储

Ceph RBD

RWO

数据库,块存储

CephFS

RWO/ROX/RWX

中高

共享文件存储

云厂商EBS/云盘

RWO

云环境数据库

Longhorn

RWO/RWX

轻量级分布式存储

2.1.3 搭建NFS服务器

NFS是最简单的共享存储方案,适合中小规模集群:

# NFS服务器(192.168.1.50)上执行
apt-get install -y nfs-kernel-server

# 创建共享目录
mkdir -p /data/nfs/k8s
chown nobody:nogroup /data/nfs/k8s
chmod 755 /data/nfs/k8s

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

# 生效配置
exportfs -ra

# 启动NFS服务
systemctl restart nfs-kernel-server
systemctl enable nfs-kernel-server

# 验证
showmount -e localhost

K8s所有Worker节点安装NFS客户端:

# Ubuntu
apt-get install -y nfs-common

# CentOS
yum install -y nfs-utils

# 验证能挂载
mount -t nfs 192.168.1.50:/data/nfs/k8s /mnt
ls /mnt
umount /mnt

注意no_root_squash允许客户端以root身份写入,生产环境如果安全要求高,改为root_squash并通过initContainer设置目录权限。

2.2 核心配置

2.2.1 静态PV/PVC(手动创建)

适合存储资源固定、数量少的场景:

# 文件:nfs-pv-static.yaml
apiVersion:v1
kind:PersistentVolume
metadata:
name:nfs-pv-data-01
labels:
    type:nfs
    env:production
spec:
capacity:
    storage:50Gi
accessModes:
    -ReadWriteMany
persistentVolumeReclaimPolicy:Retain
storageClassName:""
nfs:
    server:192.168.1.50
    path:/data/nfs/k8s/data-01
---
apiVersion:v1
kind:PersistentVolumeClaim
metadata:
name:app-data-pvc
namespace:production
spec:
accessModes:
    -ReadWriteMany
resources:
    requests:
      storage:50Gi
storageClassName:""
selector:
    matchLabels:
      type:nfs
      env:production
# 先在NFS服务器上创建目录
mkdir -p /data/nfs/k8s/data-01

# 创建PV和PVC
kubectl apply -f nfs-pv-static.yaml

# 验证绑定状态
kubectl get pv nfs-pv-data-01
# STATUS应该是Bound

kubectl get pvc app-data-pvc -n production
# STATUS应该是Bound

注意:静态PV的storageClassName设为空字符串"",PVC也要设为空字符串,这样PVC只会绑定没有StorageClass的PV。如果不设置这个字段,PVC会尝试使用默认StorageClass进行动态供给。

2.2.2 NFS动态供给(StorageClass)

安装nfs-subdir-external-provisioner,实现NFS的动态PV创建:

# 使用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 \
  --namespace kube-system \
  --set nfs.server=192.168.1.50 \
  --set nfs.path=/data/nfs/k8s \
  --set storageClass.name=nfs-client \
  --set storageClass.defaultClass=false \
  --set storageClass.reclaimPolicy=Retain \
  --set storageClass.archiveOnDelete=true

# 验证StorageClass创建成功
kubectl get sc nfs-client

手动创建StorageClass(不用Helm的方式):

# 文件:nfs-storageclass.yaml
apiVersion:storage.k8s.io/v1
kind:StorageClass
metadata:
name:nfs-client
annotations:
    storageclass.kubernetes.io/is-default-class:"false"
provisioner:cluster.local/nfs-provisioner
parameters:
archiveOnDelete:"true"
reclaimPolicy:Retain
volumeBindingMode:Immediate
allowVolumeExpansion:true
mountOptions:
-hard
-nfsvers=4.1
-timeo=600
-retrans=3

参数说明

  • archiveOnDelete: "true":PVC删除时不删除NFS上的数据,而是重命名目录加archived-前缀,防止误删

  • reclaimPolicy: Retain:PVC删除后PV保留,需要手动清理。生产环境必须用Retain,用Delete会直接删数据

  • volumeBindingMode: Immediate:PVC创建后立即绑定PV。Local PV要用WaitForFirstConsumer

  • allowVolumeExpansion: true:允许PVC扩容,NFS支持在线扩容

2.2.3 Local PV(本地持久卷)

适合数据库等对IO性能敏感的场景,数据存储在节点本地SSD上:

# 文件:local-storageclass.yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: local-ssd
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
# 文件:local-pv.yaml
apiVersion:v1
kind:PersistentVolume
metadata:
name:local-pv-worker01-ssd01
spec:
capacity:
    storage:100Gi
accessModes:
    -ReadWriteOnce
persistentVolumeReclaimPolicy:Retain
storageClassName:local-ssd
local:
    path:/data/local-volumes/ssd01
nodeAffinity:
    required:
      nodeSelectorTerms:
        -matchExpressions:
            -key:kubernetes.io/hostname
              operator:In
              values:
                -k8s-worker-01
# 在对应节点上创建目录
mkdir -p /data/local-volumes/ssd01

# 如果是独立SSD,挂载到该目录
# mkfs.xfs /dev/sdb
# mount /dev/sdb /data/local-volumes/ssd01
# echo '/dev/sdb /data/local-volumes/ssd01 xfs defaults 0 0' >> /etc/fstab

kubectl apply -f local-storageclass.yaml
kubectl apply -f local-pv.yaml

警告:Local PV的数据和节点绑定,节点故障数据就丢了。生产环境用Local PV必须配合应用层的数据复制(如MySQL主从、Redis Sentinel)来保证数据安全。

2.2.4 Ceph RBD存储(生产推荐)

Ceph提供块存储(RBD)和文件存储(CephFS),通过ceph-csi驱动接入K8s:

# 前提:已有Ceph集群,获取以下信息
# - Ceph Monitor地址:192.168.1.60:6789,192.168.1.61:6789,192.168.1.62:6789
# - Ceph集群ID:通过 ceph fsid 获取
# - 管理员密钥:通过 ceph auth get-key client.admin 获取

# 在Ceph集群上创建K8s专用pool和用户
ceph osd pool create k8s-rbd 128 128
ceph osd pool application enable k8s-rbd rbd
rbd pool init k8s-rbd

ceph auth get-or-create client.k8s-rbd \
  mon 'profile rbd' \
  osd 'profile rbd pool=k8s-rbd' \
  -o /etc/ceph/ceph.client.k8s-rbd.keyring

在K8s中配置ceph-csi:

# 安装ceph-csi(使用Helm)
helm repo add ceph-csi https://ceph.github.io/csi-charts

helm install ceph-csi-rbd ceph-csi/ceph-csi-rbd \
  --namespace ceph-csi \
  --create-namespace \
  --set csiConfig[0].clusterID=<ceph-fsid> \
  --set csiConfig[0].monitors[0]=192.168.1.60:6789 \
  --set csiConfig[0].monitors[1]=192.168.1.61:6789 \
  --set csiConfig[0].monitors[2]=192.168.1.62:6789

创建Secret和StorageClass:

# 文件:ceph-rbd-secret.yaml
apiVersion:v1
kind:Secret
metadata:
name:ceph-rbd-secret
namespace:ceph-csi
type:kubernetes.io/rbd
stringData:
userID:k8s-rbd
userKey:<cephauthget-keyclient.k8s-rbd的输出>
---
apiVersion:v1
kind:Secret
metadata:
name:ceph-rbd-admin-secret
namespace:ceph-csi
type:kubernetes.io/rbd
stringData:
userID:admin
userKey:<cephauthget-keyclient.admin的输出>
# 文件:ceph-rbd-storageclass.yaml
apiVersion:storage.k8s.io/v1
kind:StorageClass
metadata:
name:ceph-rbd
annotations:
    storageclass.kubernetes.io/is-default-class:"true"
provisioner:rbd.csi.ceph.com
parameters:
clusterID:<ceph-fsid>
pool:k8s-rbd
imageFormat:"2"
imageFeatures:layering
csi.storage.k8s.io/provisioner-secret-name:ceph-rbd-secret
csi.storage.k8s.io/provisioner-secret-namespace:ceph-csi
csi.storage.k8s.io/controller-expand-secret-name:ceph-rbd-secret
csi.storage.k8s.io/controller-expand-secret-namespace:ceph-csi
csi.storage.k8s.io/node-stage-secret-name:ceph-rbd-secret
csi.storage.k8s.io/node-stage-secret-namespace:ceph-csi
reclaimPolicy:Retain
allowVolumeExpansion:true
volumeBindingMode:Immediate
mountOptions:
-discard
kubectl apply -f ceph-rbd-secret.yaml
kubectl apply -f ceph-rbd-storageclass.yaml

# 验证
kubectl get sc ceph-rbd
2.2.5 PVC扩容
# 确认StorageClass支持扩容
kubectl get sc ceph-rbd -o jsonpath='{.allowVolumeExpansion}'
# 输出:true

# 编辑PVC,修改storage大小
kubectl patch pvc mysql-data -n database -p '{"spec":{"resources":{"requests":{"storage":"200Gi"}}}}'

# 查看扩容状态
kubectl get pvc mysql-data -n database
# 如果显示 FileSystemResizePending,需要Pod重启后才能完成文件系统扩容
# Ceph RBD支持在线扩容,不需要重启Pod

kubectl get events -n database --field-selector involvedObject.name=mysql-data

注意:PVC只能扩容不能缩容。NFS支持在线扩容,Ceph RBD支持在线扩容,Local PV不支持扩容。

2.3 启动和验证

2.3.1 使用PVC的Pod
# 文件:app-with-pvc.yaml
apiVersion:apps/v1
kind:Deployment
metadata:
name:app-with-storage
namespace:default
spec:
replicas:3
selector:
    matchLabels:
      app:app-storage
template:
    metadata:
      labels:
        app:app-storage
    spec:
      containers:
        -name:app
          image:nginx:1.24
          volumeMounts:
            -name:shared-data
              mountPath:/usr/share/nginx/html
            -name:logs
              mountPath:/var/log/nginx
          resources:
            requests:
              cpu:100m
              memory:128Mi
      volumes:
        -name:shared-data
          persistentVolumeClaim:
            claimName:app-shared-pvc
        -name:logs
          persistentVolumeClaim:
            claimName:app-logs-pvc
---
apiVersion:v1
kind:PersistentVolumeClaim
metadata:
name:app-shared-pvc
namespace:default
spec:
accessModes:
    -ReadWriteMany
storageClassName:nfs-client
resources:
    requests:
      storage:10Gi
---
apiVersion:v1
kind:PersistentVolumeClaim
metadata:
name:app-logs-pvc
namespace:default
spec:
accessModes:
    -ReadWriteMany
storageClassName:nfs-client
resources:
    requests:
      storage:20Gi
kubectl apply -f app-with-pvc.yaml

# 验证PVC绑定
kubectl get pvc -n default
# NAME             STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS
# app-shared-pvc   Bound    pvc-xxxx-xxxx                              10Gi       RWX            nfs-client
# app-logs-pvc     Bound    pvc-yyyy-yyyy                              20Gi       RWX            nfs-client

# 验证Pod挂载
kubectl exec -it $(kubectl get pod -l app=app-storage -o jsonpath='{.items[0].metadata.name}') -- df -h /usr/share/nginx/html
2.3.2 StatefulSet的volumeClaimTemplates

StatefulSet的每个Pod自动创建独立的PVC:

# 文件:redis-statefulset.yaml
apiVersion:apps/v1
kind:StatefulSet
metadata:
name:redis
namespace:default
spec:
serviceName:redis
replicas:3
selector:
    matchLabels:
      app:redis
template:
    metadata:
      labels:
        app:redis
    spec:
      containers:
        -name:redis
          image:redis:7.2
          ports:
            -containerPort:6379
          volumeMounts:
            -name:redis-data
              mountPath:/data
          resources:
            requests:
              cpu:200m
              memory:256Mi
volumeClaimTemplates:
    -metadata:
        name:redis-data
      spec:
        accessModes:["ReadWriteOnce"]
        storageClassName:ceph-rbd
        resources:
          requests:
            storage:10Gi
kubectl apply -f redis-statefulset.yaml

# 验证:每个Pod有独立的PVC
kubectl get pvc -l app=redis
# redis-data-redis-0   Bound   pvc-aaa   10Gi   RWO   ceph-rbd
# redis-data-redis-1   Bound   pvc-bbb   10Gi   RWO   ceph-rbd
# redis-data-redis-2   Bound   pvc-ccc   10Gi   RWO   ceph-rbd

注意:StatefulSet删除后PVC不会自动删除,需要手动清理。这是设计如此,防止误删数据。


三、示例代码和配置

3.1 完整配置示例

3.1.1 MySQL主从 + Ceph RBD完整存储方案
# 文件:mysql-storage-complete.yaml
# MySQL主库 - 使用Ceph RBD高性能块存储
apiVersion:v1
kind:PersistentVolumeClaim
metadata:
name:mysql-master-data
namespace:database
spec:
accessModes:
    -ReadWriteOnce
storageClassName:ceph-rbd
resources:
    requests:
      storage:100Gi
---
apiVersion:v1
kind:PersistentVolumeClaim
metadata:
name:mysql-master-logs
namespace:database
spec:
accessModes:
    -ReadWriteOnce
storageClassName:ceph-rbd
resources:
    requests:
      storage:50Gi
---
apiVersion:apps/v1
kind:Deployment
metadata:
name:mysql-master
namespace:database
spec:
replicas:1
strategy:
    type:Recreate
selector:
    matchLabels:
      app:mysql
      role:master
template:
    metadata:
      labels:
        app:mysql
        role:master
    spec:
      containers:
        -name:mysql
          image:mysql:8.0.35
          ports:
            -containerPort:3306
          env:
            -name:MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name:mysql-secret
                  key:root-password
          args:
            ---innodb-buffer-pool-size=2G
            ---innodb-log-file-size=512M
            ---innodb-flush-method=O_DIRECT
            ---innodb-io-capacity=2000
            ---innodb-io-capacity-max=4000
          resources:
            requests:
              cpu:"2"
              memory:4Gi
            limits:
              cpu:"4"
              memory:8Gi
          volumeMounts:
            -name:mysql-data
              mountPath:/var/lib/mysql
            -name:mysql-logs
              mountPath:/var/log/mysql
            -name:mysql-config
              mountPath:/etc/mysql/conf.d
          livenessProbe:
            exec:
              command:
                -mysqladmin
                -ping
                --h
                -localhost
            initialDelaySeconds:30
            periodSeconds:10
          readinessProbe:
            exec:
              command:
                -mysql
                --h
                -localhost
                --e
                -"SELECT 1"
            initialDelaySeconds:10
            periodSeconds:5
      volumes:
        -name:mysql-data
          persistentVolumeClaim:
            claimName:mysql-master-data
        -name:mysql-logs
          persistentVolumeClaim:
            claimName:mysql-master-logs
        -name:mysql-config
          configMap:
            name:mysql-config

注意:MySQL的Deployment用strategy: Recreate而不是RollingUpdate,因为RBD卷是RWO模式,不能同时被两个Pod挂载。RollingUpdate会先创建新Pod再删旧Pod,新Pod挂载会失败。

3.1.2 存储容量监控脚本
#!/bin/bash
# PVC使用率监控脚本
# 文件:/opt/scripts/pvc-usage-monitor.sh
# 通过kubelet的metrics获取PVC使用率

set -euo pipefail

ALERT_THRESHOLD=85
LOG_FILE="/var/log/pvc-monitor.log"

echo"[$(date)] Starting PVC usage check..." >> "${LOG_FILE}"

# 获取所有节点的kubelet metrics中的volume信息
for node in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do
# 通过API proxy访问kubelet metrics
  kubectl get --raw "/api/v1/nodes/${node}/proxy/stats/summary" 2>/dev/null | \
    jq -r '.pods[]? | select(.volume != null) | .podRef.namespace + "/" + .podRef.name + " " + (.volume[]? | select(.pvcRef != null) | .pvcRef.name + " " + (.usedBytes|tostring) + " " + (.capacityBytes|tostring))' 2>/dev/null | \
    whileread ns_pod pvc used capacity; do
      if [ -n "${capacity}" ] && [ "${capacity}" != "0" ]; then
        usage_pct=$((used * 100 / capacity))
        used_gi=$((used / 1073741824))
        cap_gi=$((capacity / 1073741824))

        if [ "${usage_pct}" -ge "${ALERT_THRESHOLD}" ]; then
          echo"[ALERT] ${ns_pod} PVC:${pvc} Usage:${usage_pct}% (${used_gi}Gi/${cap_gi}Gi)" >> "${LOG_FILE}"
        fi
      fi
    done
done

echo"[$(date)] PVC usage check completed." >> "${LOG_FILE}"

3.2 实际应用案例

案例一:多Pod共享NFS存储实现文件上传

场景描述:Web应用有3个副本,用户上传的文件需要所有副本都能访问。用NFS的RWX模式实现共享存储。

实现代码

# 文件:file-upload-app.yaml
apiVersion:v1
kind:PersistentVolumeClaim
metadata:
name:upload-files-pvc
namespace:production
spec:
accessModes:
    -ReadWriteMany
storageClassName:nfs-client
resources:
    requests:
      storage:100Gi
---
apiVersion:apps/v1
kind:Deployment
metadata:
name:file-upload-api
namespace:production
spec:
replicas:3
selector:
    matchLabels:
      app:file-upload-api
template:
    metadata:
      labels:
        app:file-upload-api
    spec:
      containers:
        -name:api
          image:file-upload-api:v1.2.0
          ports:
            -containerPort:8080
          volumeMounts:
            -name:uploads
              mountPath:/app/uploads
          resources:
            requests:
              cpu:200m
              memory:256Mi
      volumes:
        -name:uploads
          persistentVolumeClaim:
            claimName:upload-files-pvc

运行结果

# 3个Pod都挂载了同一个NFS卷
kubectl exec file-upload-api-xxx -- ls /app/uploads
# 在任一Pod中上传的文件,其他Pod都能看到
案例二:PV数据迁移(从NFS迁移到Ceph RBD)

场景描述:业务初期用NFS存储MySQL数据,随着数据量增长NFS的IO性能成为瓶颈,需要迁移到Ceph RBD。

实现步骤

  1. 创建新的Ceph RBD PVC

  2. 启动数据迁移Job

  3. 切换应用到新PVC

  4. 验证后清理旧PVC

# 文件:data-migration-job.yaml
apiVersion:batch/v1
kind:Job
metadata:
name:pvc-data-migration
namespace:database
spec:
template:
    spec:
      containers:
        -name:migrator
          image:ubuntu:22.04
          command:
            -/bin/bash
            --c
            -|
              apt-get update && apt-get install -y rsync
              echo "Starting data migration..."
              rsync -avz --progress /source/ /destination/
              echo "Migration completed. Verifying..."
              diff -r /source/ /destination/ && echo "Verification passed" || echo "Verification FAILED"
          volumeMounts:
            -name:source-vol
              mountPath:/source
              readOnly:true
            -name:dest-vol
              mountPath:/destination
          resources:
            requests:
              cpu:"1"
              memory:1Gi
      volumes:
        -name:source-vol
          persistentVolumeClaim:
            claimName:mysql-data-nfs
        -name:dest-vol
          persistentVolumeClaim:
            claimName:mysql-data-ceph
      restartPolicy:Never
backoffLimit:3
# 1. 停止MySQL写入(设为只读)
kubectl exec -it mysql-master-xxx -n database -- mysql -e "SET GLOBAL read_only=1;"

# 2. 创建目标PVC
kubectl apply -f mysql-data-ceph-pvc.yaml

# 3. 执行迁移Job
kubectl apply -f data-migration-job.yaml
kubectl logs -f job/pvc-data-migration -n database

# 4. 修改MySQL Deployment引用新PVC
kubectl patch deployment mysql-master -n database --type='json' \
  -p='[{"op":"replace","path":"/spec/template/spec/volumes/0/persistentVolumeClaim/claimName","value":"mysql-data-ceph"}]'

# 5. 验证MySQL正常启动
kubectl get pods -n database -l app=mysql,role=master
kubectl exec -it mysql-master-xxx -n database -- mysql -e "SHOW DATABASES;"

# 6. 取消只读
kubectl exec -it mysql-master-xxx -n database -- mysql -e "SET GLOBAL read_only=0;"

四、最佳实践和注意事项

4.1 最佳实践

4.1.1 性能优化
  • NFS挂载参数调优:默认NFS挂载参数在高并发写入时性能差,生产环境必须调整。hard模式保证数据一致性,nfsvers=4.1支持并行IO,timeo=600避免网络抖动导致超时。

    # StorageClass中设置mountOptions
    mountOptions:
    -hard
    -nfsvers=4.1
    -timeo=600
    -retrans=3
    -rsize=1048576
    -wsize=1048576
    

    实测调整rsize/wsize从默认的32KB到1MB后,大文件顺序写入吞吐量从80MB/s提升到350MB/s。

  • Ceph RBD性能调优:RBD镜像默认特性包含exclusive-lockobject-mapfast-diff,生产环境建议全部开启。Ceph OSD的bluestore_cache_size默认1GB,SSD节点建议调到4GB。

    # 查看RBD镜像特性
    rbd info k8s-rbd/csi-vol-xxx
    
    # Ceph OSD调优(在Ceph节点执行)
    ceph config set osd bluestore_cache_size_ssd 4294967296
    ceph config set osd osd_op_num_threads_per_shard_ssd 2
    
  • Local PV使用XFS文件系统:相比ext4,XFS在大文件和高并发场景下性能更好,MySQL的InnoDB引擎在XFS上的随机写性能提升约15%。

    mkfs.xfs -f /dev/sdb
    mount -o noatime,nodiratime /dev/sdb /data/local-volumes/ssd01
    
4.1.2 安全加固
  • PV访问权限控制:通过SecurityContext设置Pod的fsGroup,确保挂载的卷只有指定用户组能访问。

    spec:
      securityContext:
        runAsUser:1000
        runAsGroup:1000
        fsGroup:1000
    containers:
        -name:app
          volumeMounts:
            -name:data
              mountPath:/data
    
  • 加密存储:Ceph RBD支持LUKS加密,数据在磁盘上是加密的,即使磁盘被盗也无法读取数据。

    # StorageClass中启用加密
    parameters:
      encrypted: "true"
      encryptionKMSID: vault-kms
    
  • 限制PVC大小:通过LimitRange限制namespace中PVC的最大容量,防止用户申请过大的存储。

    apiVersion: v1
    kind:LimitRange
    metadata:
    name:storage-limits
    namespace:production
    spec:
    limits:
        -type:PersistentVolumeClaim
          max:
            storage:500Gi
          min:
            storage:1Gi
    
4.1.3 高可用配置
  • HA方案一:Ceph RBD三副本存储,任一OSD节点故障数据不丢失,RBD卷自动恢复

  • HA方案二:NFS服务器用DRBD + Pacemaker做主备高可用,VIP自动漂移

  • 备份策略:Ceph RBD使用快照功能定期备份,NFS使用rsync同步到备份服务器。VolumeSnapshot是K8s原生的快照机制:

    apiVersion: snapshot.storage.k8s.io/v1
    kind:VolumeSnapshot
    metadata:
    name:mysql-data-snapshot
    namespace:database
    spec:
    volumeSnapshotClassName:ceph-rbd-snapclass
    source:
        persistentVolumeClaimName:mysql-master-data
    

4.2 注意事项

4.2.1 配置注意事项

警告:存储配置错误可能导致数据丢失,修改前务必备份数据。

  • 注意reclaimPolicy: Delete会在PVC删除时同时删除PV和后端存储数据。生产环境必须用Retain,宁可手动清理也不要自动删除。

  • 注意 Local PV的volumeBindingMode必须设为WaitForFirstConsumer,否则PVC可能绑定到一个节点的PV上,但Pod被调度到另一个节点,导致挂载失败。

  • 注意 NFS的no_root_squash选项允许客户端以root身份操作文件,安全风险高。生产环境用root_squash,通过initContainer以root身份设置目录权限后,主容器以非root用户运行。

4.2.2 常见错误

错误现象

原因分析

解决方案

PVC一直Pending

没有匹配的PV或StorageClass的Provisioner未运行

kubectl describe pvc

查看事件;检查Provisioner Pod状态

Pod挂载卷失败,报Multi-Attach error

RWO卷被多个节点的Pod同时挂载

确认旧Pod已完全终止;检查是否有残留的VolumeAttachment

NFS挂载超时

NFS服务器不可达或防火墙阻断

检查NFS服务器状态;确认2049端口可访问;Worker节点安装nfs-common

Ceph RBD挂载报rbd: map failed

Ceph集群不健康或认证失败

检查Ceph集群状态ceph -s;验证Secret中的key是否正确

PVC扩容后容量没变

文件系统未扩展,需要Pod重启

删除Pod触发重建,kubelet会自动扩展文件系统

StatefulSet缩容后PVC残留

设计如此,PVC不随Pod删除

手动删除不需要的PVC:kubectl delete pvc <name>

4.2.3 兼容性问题
  • 版本兼容:CSI驱动版本需要和K8s版本匹配,ceph-csi 3.9.x支持K8s 1.24-1.28

  • 平台兼容:NFS在所有Linux发行版上都支持;Ceph RBD需要内核4.x+;CephFS需要内核4.17+

  • 组件依赖:VolumeSnapshot需要安装snapshot-controller和对应的CSI驱动支持


五、故障排查和监控

5.1 故障排查

5.1.1 日志查看
# 查看CSI驱动日志
kubectl logs -n ceph-csi -l app=ceph-csi-rbd-nodeplugin --tail=50
kubectl logs -n ceph-csi -l app=ceph-csi-rbd-provisioner --tail=50

# 查看NFS Provisioner日志
kubectl logs -n kube-system -l app=nfs-subdir-external-provisioner --tail=50

# 查看kubelet的卷挂载日志
journalctl -u kubelet | grep -i "volume\|mount\|pvc" | tail -30

# 查看PVC事件
kubectl describe pvc <pvc-name> -n <namespace>

# 查看VolumeAttachment
kubectl get volumeattachment
5.1.2 常见问题排查

问题一:PVC Pending,事件显示no persistent volumes available

# 诊断命令
kubectl describe pvc <pvc-name> -n <namespace>
kubectl get pv --show-labels
kubectl get sc

解决方案

  1. 静态供给:检查是否有可用的PV,PV的容量、accessModes、storageClassName是否和PVC匹配

  2. 动态供给:检查StorageClass是否存在,Provisioner Pod是否Running

  3. 检查PV的状态是否为Available(已绑定的PV不能再绑定其他PVC)

问题二:Pod卡在ContainerCreating,报FailedMount

# 诊断命令
kubectl describe pod <pod-name> -n <namespace> | grep -A 10 "Events"
kubectl get volumeattachment | grep <pv-name>

# 检查节点上的挂载情况
kubectl debug node/<node-name> -it --image=ubuntu:22.04 -- \
  mount | grep <pv-name>

解决方案

  • NFS挂载失败:确认Worker节点安装了nfs-common,NFS服务器可达

  • RBD挂载失败:检查Ceph集群健康状态,确认Secret配置正确

  • Multi-Attach错误:等待旧Pod完全终止,或手动删除残留的VolumeAttachment

问题三:存储性能差,IO延迟高

  • 症状:应用响应慢,数据库查询超时

  • 排查

    # 在Pod内测试IO性能
    kubectl exec -it <pod-name> -- bash
    # 安装fio
    apt-get update && apt-get install -y fio
    
    # 随机读写测试
    fio --name=randwrite --ioengine=libaio --iodepth=32 --rw=randwrite \
      --bs=4k --direct=1 --size=1G --numjobs=4 --runtime=60 \
      --filename=/data/fio-test --group_reporting
    
    # 顺序写测试
    fio --name=seqwrite --ioengine=libaio --iodepth=32 --rw=write \
      --bs=1M --direct=1 --size=1G --numjobs=1 --runtime=60 \
      --filename=/data/fio-test --group_reporting
    
  • 解决

    1. NFS性能差:调整rsize/wsize到1MB,升级到NFSv4.1

    2. Ceph RBD性能差:检查OSD磁盘是否为SSD,调整bluestore_cache_size

    3. 考虑迁移到Local PV获取本地磁盘性能

5.1.3 调试模式
# 查看CSI驱动详细日志
kubectl logs -n ceph-csi <csi-pod-name> -c csi-rbdplugin --tail=200

# 手动测试Ceph连接
kubectl run ceph-test --image=quay.io/ceph/ceph:v18 --rm -it -- bash
ceph -s --conf /etc/ceph/ceph.conf

# 查看节点上的块设备
kubectl debug node/<node-name> -it --image=ubuntu:22.04 -- lsblk

# 查看挂载点
kubectl debug node/<node-name> -it --image=ubuntu:22.04 -- df -h

5.2 性能监控

5.2.1 关键指标监控
# PVC使用率(需要kubelet metrics)
kubectl get --raw "/api/v1/nodes/<node>/proxy/stats/summary" | \
  jq '.pods[].volume[]? | select(.pvcRef != null) | {pvc: .pvcRef.name, used: .usedBytes, capacity: .capacityBytes}'

# Ceph集群状态
ceph -s
ceph osd pool stats k8s-rbd

# NFS服务器IO
nfsstat -s
iostat -x 1 5
5.2.2 监控指标说明

指标名称

正常范围

告警阈值

说明

PVC使用率

<70%

>85%

超过85%需要扩容或清理数据

Ceph集群健康状态

HEALTH_OK

HEALTH_WARN

WARN需要排查,ERR需要立即处理

Ceph OSD延迟

<10ms

>50ms

OSD延迟高影响所有RBD卷性能

NFS服务器IOPS

按硬件

接近硬件上限

IOPS饱和需要扩容NFS或迁移到分布式存储

卷挂载失败次数

0

>0

任何挂载失败都需要排查

PV Available数量

>5

<2

静态供给时可用PV不足需要提前创建

5.2.3 Prometheus监控规则
# 文件:storage-alerts.yaml
apiVersion:monitoring.coreos.com/v1
kind:PrometheusRule
metadata:
name:storage-alerts
namespace:monitoring
spec:
groups:
    -name:kubernetes-storage
      rules:
        -alert:PVCUsageHigh
          expr:|
            kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes > 0.85
          for:10m
          labels:
            severity:warning
          annotations:
            summary:"PVC {{ $labels.persistentvolumeclaim }} usage exceeds 85%"
            description:"Namespace: {{ $labels.namespace }}"

        -alert:PVCUsageCritical
          expr:|
            kubelet_volume_stats_used_bytes / kubelet_volume_stats_capacity_bytes > 0.95
          for:5m
          labels:
            severity:critical
          annotations:
            summary:"PVC {{ $labels.persistentvolumeclaim }} usage exceeds 95%"

        -alert:PVCPending
          expr:|
            kube_persistentvolumeclaim_status_phase{phase="Pending"} == 1
          for:15m
          labels:
            severity:warning
          annotations:
            summary:"PVC {{ $labels.persistentvolumeclaim }} has been Pending for 15 minutes"

        -alert:PVAvailableLow
          expr:|
            count(kube_persistentvolume_status_phase{phase="Available"}) < 2
          for:10m
          labels:
            severity:warning
          annotations:
            summary:"Less than 2 PVs available in the cluster"

        -alert:VolumeAttachmentFailed
          expr:|
            increase(storage_operation_errors_total[5m]) > 0
          for:5m
          labels:
            severity:warning
          annotations:
            summary:"Volume operation errors detected"

5.3 备份与恢复

5.3.1 备份策略
#!/bin/bash
# Ceph RBD快照备份脚本
# 文件:/opt/scripts/rbd-snapshot-backup.sh

set -euo pipefail

POOL="k8s-rbd"
BACKUP_PREFIX="scheduled-backup"
KEEP_SNAPSHOTS=24

# 获取pool中所有RBD镜像
for image in $(rbd ls ${POOL}); do
  SNAP_NAME="${BACKUP_PREFIX}-$(date +%Y%m%d-%H%M%S)"

# 创建快照
  rbd snap create "${POOL}/${image}@${SNAP_NAME}"
echo"[$(date)] Created snapshot: ${POOL}/${image}@${SNAP_NAME}"

# 清理过期快照(保留最近24个)
  rbd snap ls "${POOL}/${image}" | grep "${BACKUP_PREFIX}" | \
    head -n -${KEEP_SNAPSHOTS} | awk '{print $2}' | \
    whileread old_snap; do
      rbd snap rm "${POOL}/${image}@${old_snap}"
      echo"[$(date)] Removed old snapshot: ${POOL}/${image}@${old_snap}"
    done
done
5.3.2 恢复流程
  1. 停止服务:缩容使用该PVC的Deployment/StatefulSet到0副本

  2. 恢复数据:从快照恢复RBD镜像或从备份rsync数据

  3. 验证完整性:挂载卷检查数据完整性

  4. 重启服务:恢复Deployment/StatefulSet副本数


六、总结

6.1 技术要点回顾

  • 要点一:生产环境reclaimPolicy必须设为RetainDelete策略会在PVC删除时直接删除后端存储数据

  • 要点二:Local PV的volumeBindingMode必须用WaitForFirstConsumer,避免PV和Pod调度到不同节点

  • 要点三:NFS适合共享文件存储(RWX),Ceph RBD适合数据库块存储(RWO),根据业务需求选型

  • 要点四:StorageClass + Provisioner实现动态供给,消除手动创建PV的运维负担

  • 要点五:PVC只能扩容不能缩容,容量规划时预留30%余量,配合监控在85%使用率时告警

6.2 进阶学习方向

  1. CSI驱动开发:理解CSI接口规范,为自研存储系统开发K8s CSI驱动

    • 学习资源:CSI规范

    • 实践建议:从ceph-csi源码入手,理解Provisioner和NodePlugin的工作流程

  2. Rook-Ceph Operator:用Rook在K8s中自动化部署和管理Ceph集群,降低Ceph运维复杂度

    • 学习资源:Rook官方文档

    • 实践建议:测试环境用Rook部署3节点Ceph集群,体验自动化运维

  3. VolumeSnapshot和数据保护:基于CSI的快照机制实现应用一致性备份

6.3 参考资料

  • Kubernetes存储官方文档 - PV/PVC/SC完整说明

  • Ceph CSI项目 - Ceph CSI驱动

  • NFS Provisioner - NFS动态供给

  • Longhorn项目 - 轻量级分布式存储


附录

A. 命令速查表

# PV操作
kubectl get pv                                    # 查看所有PV
kubectl get pv -o wide                            # 查看PV详情(含StorageClass)
kubectl describe pv <pv-name>                     # PV详细信息
kubectl delete pv <pv-name>                       # 删除PV(Retain策略下数据不丢)

# PVC操作
kubectl get pvc -A                                # 查看所有namespace的PVC
kubectl get pvc -n <namespace>                    # 查看指定namespace的PVC
kubectl describe pvc <pvc-name> -n <namespace>    # PVC详细信息和事件
kubectl patch pvc <name> -p '{"spec":{"resources":{"requests":{"storage":"100Gi"}}}}'# 扩容

# StorageClass操作
kubectl get sc                                    # 查看StorageClass列表
kubectl describe sc <sc-name>                     # SC详细信息
kubectl patch sc <name> -p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'# 设为默认SC

# 排查命令
kubectl get volumeattachment                      # 查看卷挂载关系
kubectl get events -A --field-selector reason=FailedMount  # 挂载失败事件

B. 配置参数详解

StorageClass关键参数

参数

说明

可选值

provisioner

存储供给器

rbd.csi.ceph.com

nfs.csi.k8s.io

reclaimPolicy

回收策略

Retain

(保留)、Delete(删除)

volumeBindingMode

绑定模式

Immediate

(立即)、WaitForFirstConsumer(延迟)

allowVolumeExpansion

允许扩容

true

/false

mountOptions

挂载选项

依存储类型而定

PV访问模式

模式

缩写

说明

支持的存储

ReadWriteOnce

RWO

单节点读写

所有存储类型

ReadOnlyMany

ROX

多节点只读

NFS、CephFS、云盘

ReadWriteMany

RWX

多节点读写

NFS、CephFS、GlusterFS

ReadWriteOncePod

RWOP

单Pod读写(1.27 GA)

CSI驱动支持

C. 术语表

术语

英文

解释

PV

PersistentVolume

集群级别的存储资源,由管理员创建或动态供给

PVC

PersistentVolumeClaim

用户对存储的申请,绑定到PV后供Pod使用

SC

StorageClass

存储类,定义动态供给的参数和策略

CSI

Container Storage Interface

容器存储接口标准,存储厂商实现CSI驱动接入K8s

RBD

RADOS Block Device

Ceph的块存储设备,提供高性能块级IO

Provisioner

供给器

负责根据PVC自动创建PV的控制器

VolumeSnapshot

卷快照

基于CSI的存储快照机制,用于数据备份

fsGroup

文件系统组

Pod SecurityContext中设置,控制挂载卷的文件组权限

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐