Kubernetes 存储实战:CSI 动态供给与扩容
Kubernetes 的存储分三层:StorageClass 定义“哪种盘、怎么供给”,PVC 是用户的容量申请,PV 是实际的卷。三层之间任何一环对不上,PVC 就会一直 Pending,Pod 也起不来。本文以云盘 CSI 与本地盘为例,讲动态供给、扩容、快照与排障的完整流程。
先说两个常见误解:StorageClass 的 provisioner 必须与集群里实际安装的 CSI 驱动一致,名字差一个字符都会导致 PVC 永远等不到供给;扩容能力不是默认就有的,StorageClass 必须显式开启 allowVolumeExpansion: true。
一、动态供给:StorageClass、PVC、PV 的配合
动态供给的流程:用户创建 PVC → 集群按 StorageClass 找 CSI 驱动 → CSI 驱动在云平台上创建真实卷 → 集群自动创建 PV 并绑定。任何一步失败,PVC 的事件里都会有明确提示,先看事件再查驱动日志。
<pre class="wp-block-syntaxhighlighter-code"># StorageClass 示例(云盘 CSI,需按实际 provisioner 修改)
kubectl apply -f - <<'EOF'
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: csi-disk
provisioner: disk.csi.example.com
parameters:
type: cloud_ssd
reclaimPolicy: Retain
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer
EOF
# PVC 示例
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
spec:
accessModes: [ReadWriteOnce]
storageClassName: csi-disk
resources:
requests:
storage: 20Gi
EOF
kubectl get pvc app-data -o wide
kubectl describe pvc app-data | tail -20</pre>
volumeBindingMode: WaitForFirstConsumer 是推荐值:等 Pod 调度到节点后再创建卷,避免卷建在 A 区、Pod 调度到 B 区导致挂载失败。Immediate 模式在多可用区集群上很容易踩这个坑。
二、PVC 一直 Pending:按顺序查四项
<pre class="wp-block-syntaxhighlighter-code"># 1. 看 PVC 事件,定位卡在哪一步
kubectl describe pvc <pvc> | grep -A10 Events
# 2. StorageClass 是否存在、provisioner 是否正确
kubectl get sc
kubectl get csidriver
# 3. CSI 控制器是否正常
kubectl -n kube-system get pods -l app=csi-controller
kubectl -n kube-system logs -l app=csi-controller --tail=50 | grep -i -E 'error|failed' | tail -20
# 4. 云平台侧是否有限额或权限问题(看 CSI 日志里的云 API 报错)
kubectl -n kube-system logs -l app=csi-controller --tail=100 | grep -i -E 'quota|permission|denied' | tail -20</pre>
高频原因排序:StorageClass 名字写错、CSI 驱动没装或控制器挂了、云盘配额满了、云账号权限不足。事件里一般会直接写明,照着修就行。
三、Pod 挂载失败:节点侧的排查
PVC 绑定成功、Pod 却起不来,问题多在节点侧:kubelet 调 CSI node 插件挂载失败,或卷已被挂到别的节点(ReadWriteOnce 只能挂一个节点)。先看 Pod 事件,再看节点上 kubelet 与 CSI node 的日志。
<pre class="wp-block-syntaxhighlighter-code"># Pod 事件
kubectl describe pod <pod> | grep -A15 Events
# 节点上 CSI node 插件是否正常
kubectl -n kube-system get pods -l app=csi-node -o wide
# 看卷是否已被挂载、挂载点是否残留
lsblk | grep -E 'disk|nvme'
mount | grep <pvc-uuid> | head
# 残留挂载的清理:先确认 Pod 已删,再卸载残留挂载点
umount /var/lib/kubelet/pods/<pod-uid>/volumes/kubernetes.io~csi/<pvc>/mount</pre>
节点异常重启后最容易出现残留挂载:Pod 已被驱逐,卷还挂在旧节点上,新 Pod 调度到别的节点挂载失败。清理残留挂载前务必确认旧 Pod 已彻底删除,否则可能损坏数据。
四、在线扩容:PVC 扩容的完整流程
在线扩容分两步:先扩 PVC(改 spec.resources.requests.storage),再等文件系统扩容。StorageClass 必须开启 allowVolumeExpansion,文件系统扩容由 kubelet 自动做,ext4/xfs 都支持在线扩,但前提是 CSI 驱动支持扩容(看 CSIDriver 的能力列表)。
<pre class="wp-block-syntaxhighlighter-code"># 扩容 PVC(只能扩、不能缩)
kubectl patch pvc app-data -p '{"spec":{"resources":{"requests":{"storage":"50Gi"}}}}'
# 观察扩容状态:FileSystemResizePending 表示等 Pod 重启或 kubelet 处理
kubectl get pvc app-data -o wide
kubectl describe pvc app-data | grep -A5 Conditions
# 进 Pod 验证文件系统已扩
kubectl exec -it <pod> -- df -h /data
# 若一直卡在 Pending,检查 StorageClass 是否开启扩容
kubectl get sc csi-disk -o yaml | grep -E 'allowVolumeExpansion|provisioner'</pre>
注意:块设备模式(volumeMode: Block)的卷扩容后不会自动扩文件系统,需要应用自己处理;文件系统模式扩容后若 Pod 内 df 仍显示旧大小,重启 Pod 触发 kubelet 重新扩容。
五、快照与恢复:VolumeSnapshot 的用法
快照是 PVC 级别的备份:创建 VolumeSnapshot 会调用 CSI 驱动在云平台上做卷快照,恢复时从快照新建 PVC。快照与 Velero 的区别:快照只备卷、不备 K8s 对象,适合数据库这类有状态应用的快速回滚。
<pre class="wp-block-syntaxhighlighter-code"># 创建快照(需先安装 snapshot controller 与对应 CRD)
kubectl apply -f - <<'EOF'
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: app-data-snap-20261109
spec:
volumeSnapshotClassName: csi-snapclass
source:
persistentVolumeClaimName: app-data
EOF
kubectl get volumesnapshot app-data-snap-20261109 -o wide
# 从快照恢复出新 PVC
kubectl apply -f - <<'EOF'
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data-restored
spec:
storageClassName: csi-disk
dataSource:
name: app-data-snap-20261109
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 20Gi
EOF</pre>
六、拓扑与亲和性:卷要跟 Pod 落在同一个地方
多可用区的集群里,PVC 绑定的卷可能落在 A 区,而 Pod 被调度到了 B 区,结果 Pod 一直 Pending,事件里只写“卷与节点不在同一区”。StorageClass 的 volumeBindingMode: WaitForFirstConsumer 就是为了解决这个问题:等 Pod 调度完成、节点定了,再按节点的区来创建卷,从根本上避免卷与 Pod 错区。
<pre class="wp-block-syntaxhighlighter-code"># 延迟绑定:等 Pod 调度后再创建卷,避免卷与 Pod 错可用区
# apiVersion: storage.k8s.io/v1
# kind: StorageClass
# metadata:
# name: csi-delayed
# provisioner: csi.example.com
# volumeBindingMode: WaitForFirstConsumer
# allowedTopologies:
# - matchLabelExpressions:
# - key: topology.kubernetes.io/zone
# values: ["zone-a", "zone-b"]
# parameters:
# type: ssd
# ---
# 验证:PVC 创建后先不绑定,等 Pod 调度后再看卷落在哪
# kubectl get pvc data-pvc -o yaml | grep -A2 volumeName
# kubectl get pv <pv-name> -o yaml | grep -A3 nodeAffinity
# 排查错区:看 Pod 事件里的 FailedAttachVolume 与卷的节点亲和性
# kubectl describe pod <pod> | grep -A5 Events</pre>
延迟绑定有个副作用:PVC 创建后不会立刻绑定,监控上“PVC 长时间 Pending”可能是正常现象,要结合 Pod 是否已调度来判断。已有 Immediate 模式的 StorageClass 不要直接改绑定模式,已有 PVC 不受影响,新 PVC 才走新模式,改完要在测试命名空间先验证一轮。
七、从快照恢复与克隆:不用重新导数据就能起新卷
从快照恢复 PVC、或克隆一个现有 PVC,是测试环境复制数据、故障后快速恢复的标准做法。恢复出来的卷是快照时间点的一致副本,数据库类应用恢复后还要按数据库自身的流程做崩溃恢复,不要以为卷回来了数据就一定能直接用。
# 从快照恢复出新 PVC(dataSource 指向 VolumeSnapshot)
# apiVersion: v1
# kind: PersistentVolumeClaim
# metadata:
# name: data-restored
# spec:
# storageClassName: csi-standard
# dataSource:
# name: data-snapshot-2026-11-09
# kind: VolumeSnapshot
# apiGroup: snapshot.storage.k8s.io
# accessModes: [ReadWriteOnce]
# resources:
# requests:
# storage: 50Gi
# ---
# 克隆现有 PVC(dataSource 指向原 PVC,需 CSI 驱动支持)
# dataSource:
# name: data-pvc
# kind: PersistentVolumeClaim
# 验证恢复结果:挂到临时 Pod 上检查数据
# kubectl run checker --rm -it --image=busybox --overrides='{"spec":{"volumes":[{"name":"d","persistentVolumeClaim":{"claimName":"data-restored"}}]}}' -- sh
快照恢复有两个前提:StorageClass 要支持快照、快照本身要是 ReadyToUse 状态。恢复出来的卷容量不能小于快照时的原卷,扩容可以在恢复时顺手做。克隆功能依赖 CSI 驱动实现,不是所有驱动都支持,动手前先在测试卷上验证一次。
八、CSI 驱动升级与故障:先看 sidecar,再看驱动
CSI 出问题时,报错经常不在业务 Pod 上,而在 csi-provisioner、csi-attacher 这些 sidecar 的日志里。排查顺序是:先看 StorageClass 的 provisioner 对不对,再看对应 sidecar 的日志,最后看节点上的 csi-node 插件是否注册成功。驱动升级同样要逐节点灰度,节点插件重启期间该节点上的卷操作会短暂失败。
# 看 CSI 驱动是否注册成功(节点上要有对应的 socket)
kubectl get csidriver
kubectl -n kube-system get pods -l app=csi-node
# 看 sidecar 日志(provisioner / attacher / snapshotter)
kubectl -n kube-system logs -l app=csi-controller --tail=100 | grep -iE 'error|fail'
# 看节点上的 CSI 插件注册状态
ls /var/lib/kubelet/plugins_registry/ 2>/dev/null
ls /var/lib/kubelet/pods/*/volumes/kubernetes.io~csi/ 2>/dev/null | head
# 驱动升级后验证:建一个测试 PVC 走完整流程
kubectl apply -f /tmp/test-pvc.yaml && kubectl get pvc test-pvc -w
# 升级回滚:恢复旧版 controller 与 node 的 DaemonSet/Deployment
# kubectl rollout undo deployment/csi-controller -n kube-system
CSI 驱动升级前要核对 sidecar 版本与驱动版本的兼容矩阵,版本错配会导致卷操作静默失败。节点插件异常时,先看 kubelet 日志里的 plugin registration,而不是直接重启 kubelet。升级期间不要同时做 PVC 扩容、快照等操作,等驱动稳定后再继续。
九、存储选型与避坑清单
- reclaimPolicy:生产用 Retain,删 PVC 不删真实卷,误删 PVC 后还能找回数据;测试环境可用 Delete。
- 访问模式:ReadWriteOnce 只能一个节点读写,多副本应用要共享存储时用 ReadWriteMany(NFS/对象存储网关),不要强行用 RWO 跑多副本。
- 本地盘:local StorageClass 性能最好,但 Pod 与盘绑定,节点故障时数据跟着丢,只适合可重建的缓存类应用。
- 监控项:PVC 使用率(kubelet_volume_stats_used_bytes)、CSI 驱动健康、云盘配额余量,磁盘使用率超 85% 就告警。
- 备份:快照不是备份的全部,跨可用区/跨云的容灾还要靠 Velero 或数据库自身的备份机制。







