Kubernetes 基础体系 · 第 38/83 篇。示例基于 Kubernetes 当前稳定 API;弃用、版本偏差、云厂商差异和生产风险会明确说明。
Kubernetes CSI:Controller、Node、Attach、Mount、快照和故障
CSI(Container Storage Interface)是 Kubernetes 与存储系统之间的插件接口。它把“创建一块云盘”“把云盘连接到某台节点”“在节点上格式化并挂载”“创建快照”等操作,从 Kubernetes 核心代码中分离出来,由存储厂商或社区插件实现。
理解 CSI 不能只记住“Controller 管控制面、Node 管节点”。一个 Pod 使用 PVC 时,至少涉及以下对象和过程:
- 用户通过 PVC 请求存储;
- StorageClass 指定动态供应参数;
- external-provisioner 调用 CSI Controller 创建后端卷;
- Kubernetes 创建 PV,并将 PVC 绑定到 PV;
- external-attacher 使后端卷连接到目标节点;
- kubelet 通过 CSI Node 插件完成设备发现、格式化、挂载和 bind mount;
- Pod 使用挂载后的目录;
- 删除或迁移 Pod 时,流程反向执行;
- 快照则由 VolumeSnapshot API、snapshot-controller 和 external-snapshotter 协作完成。
这些步骤不是所有存储都完全相同。块存储、文件存储、拓扑受限存储、支持或不支持 Attach 的 CSI 驱动,在中间步骤上会有差异。
一、CSI 的责任边界
1. CSI 不负责容器运行时和网络
Kubernetes 中常被并列提到的 CRI、CNI、CSI,解决的是三个不同问题:
| 接口 | 解决的问题 | 典型调用方 | 典型结果 |
|---|---|---|---|
| CRI | 如何创建和管理容器 | kubelet → 容器运行时 | 容器进程、网络命名空间、容器生命周期 |
| CNI | Pod 如何接入网络 | 容器运行时或网络插件 | 网卡、IP、路由、网络策略 |
| CSI | Pod 如何获得持久化存储 | kubelet、控制器侧边车 → 存储插件 | 云盘、文件卷、节点挂载目录 |
例如:
- CRI 创建 PostgreSQL 容器;
- CNI 为 PostgreSQL Pod 分配
10.0.2.15; - CSI 将一个云盘挂载到 Pod 的
/var/lib/postgresql/data。
CSI 不创建容器,也不分配 Pod IP;CNI 不创建 PV;CRI 通常也不知道后端云盘的快照和复制策略。
1. CSI 的两个服务
CSI 插件通常提供两个逻辑服务:
Controller 服务
Controller 服务负责与存储后端交互,典型 RPC 包括:
CreateVolumeDeleteVolumeControllerPublishVolumeControllerUnpublishVolumeValidateVolumeCapabilitiesCreateSnapshotDeleteSnapshotListSnapshotsControllerExpandVolume
它通常运行在控制器 Pod 中,可以是 Deployment,也可以是 StatefulSet。Controller 不应该假设自己运行在目标节点上。
Node 服务
Node 服务负责某一台节点上的本地操作,典型 RPC 包括:
NodeStageVolumeNodeUnstageVolumeNodePublishVolumeNodeUnpublishVolumeNodeGetInfoNodeExpandVolumeNodeGetVolumeStats
它通常以 DaemonSet 运行,每个可使用该存储的节点都有一个 Node 插件实例。Node 服务能够访问宿主机设备、挂载点和内核能力,因此需要较高权限,常见配置包括 hostNetwork、hostPID、privileged 或特定的 hostPath。
Controller 和 Node 不是两个可选的命名概念,而是两类不同的执行位置:
- Controller 负责“后端资源和节点级连接”;
- Node 负责“目标节点上的设备和文件系统”。
部分 CSI 驱动只提供文件系统网络挂载,不需要传统意义上的 Attach;但仍然需要 Node 服务执行挂载。
二、CSI 插件不是 Kubernetes 单体组件
CSI 驱动一般由以下部分组成:
graph LR
U[用户: PVC / Pod / VolumeSnapshot] --> API[Kubernetes API Server]
API --> P[external-provisioner]
API --> A[external-attacher]
API --> R[external-resizer]
API --> S[snapshot-controller]
API --> ES[external-snapshotter]
P --> CC[CSI Controller Service]
A --> CC
R --> CC
ES --> CC
K[kubelet] --> NR[csi-node-driver-registrar]
K --> NS[CSI Node Service]
NR --> NS
CC --> B[存储后端]
NS --> N[节点设备 / 文件系统 / 挂载点]
N --> Pod[Pod 容器]
这里有一个容易混淆的事实:Kubernetes 通常不会直接实现所有 CSI RPC,而是通过一组 sidecar 调用 CSI gRPC 服务。
例如:
external-provisioner监听 PVC 和 StorageClass,调用CreateVolume;external-attacher监听VolumeAttachment,调用ControllerPublishVolume;external-resizer监听扩容请求,调用控制器和节点扩容 RPC;external-snapshotter监听VolumeSnapshotContent,调用CreateSnapshot;snapshot-controller负责 VolumeSnapshot API 对象与 VolumeSnapshotContent 的协调;csi-node-driver-registrar将 CSI Node 插件注册到 kubelet。
这些组件的具体镜像版本必须与 Kubernetes 版本、CSI 驱动版本和 CSI sidecar 兼容。不能根据某篇旧文档直接复制镜像标签到生产环境。
三、PVC、PV 和动态供应
1. PVC 是需求,PV 是绑定结果
PVC(PersistentVolumeClaim)描述工作负载需要什么存储:
- 容量;
- 访问模式;
- StorageClass;
- 可选的数据源。
PV(PersistentVolume)描述一个已经存在或由控制器创建的存储资源:
- 后端卷标识;
- CSI 驱动名;
- 容量;
- 回收策略;
- 节点拓扑;
- 挂载参数。
动态供应的逻辑可以写成:
其中 CreateVolume 的输入通常包括 PVC 请求的容量、卷能力、拓扑和 StorageClass 参数。
一个最小的 StorageClass 和 PVC 示例:
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: example-csi
provisioner: example.csi.vendor
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Delete
allowVolumeExpansion: true
parameters:
type: fast
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-demo
spec:
accessModes:
- ReadWriteOnce
storageClassName: example-csi
resources:
requests:
storage: 20Gi
这个示例不能直接创建真实磁盘,因为 example.csi.vendor 是占位的驱动名。只有集群中已经安装并注册了同名 CSI 驱动,kubectl apply 后 PVC 才可能变为 Bound。
检查过程:
kubectl apply -f storage.yaml
kubectl get pvc data-demo
kubectl describe pvc data-demo
kubectl get pv
kubectl get storageclass example-csi -o yaml
可能看到:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-demo Pending example-csi 12s
Pending 不是“磁盘已经创建但挂载失败”的同义词。它可能表示:
- 没有安装匹配的 provisioner;
- StorageClass 参数无效;
- 后端配额不足;
- 使用
WaitForFirstConsumer,尚未出现能决定拓扑的 Pod; - external-provisioner 没有运行;
- PVC 与静态 PV 无法匹配。
2. WaitForFirstConsumer 的因果关系
如果存储有可用区或节点拓扑限制,立即创建卷可能选错区域。因此 volumeBindingMode: WaitForFirstConsumer 会延迟绑定:
- PVC 创建,但暂不供应;
- Pod 被调度器分析;
- 调度器根据节点拓扑和卷约束筛选候选节点;
- provisioner 使用选定拓扑调用
CreateVolume; - PV 与 PVC 绑定;
- Pod 继续挂载。
这不是“挂载失败后的重试”,而是为了让调度结果参与卷供应决策。
四、Attach:把后端卷连接到节点
1. Attach 与 Mount 不是一回事
对云块存储而言:
- Attach 通常意味着云平台将卷连接到某个节点实例;
- Mount 意味着节点操作系统把设备或远端文件系统挂载到目录;
- Pod 使用的是最终的 bind mount 或文件系统路径。
因此:
一个卷可能已经创建,但尚未连接到任何节点;也可能已经 Attach,但文件系统尚未挂载。
2. VolumeAttachment 的作用
当 CSI 驱动需要控制器侧 Attach 时,external-attacher 会创建或协调 VolumeAttachment 对象。它表达类似的意图:
apiVersion: storage.k8s.io/v1
kind: VolumeAttachment
metadata:
name: example-attachment
spec:
attacher: example.csi.vendor
nodeName: worker-1
source:
persistentVolumeName: pvc-xxxx
status:
attached: true
attachmentMetadata:
devicePath: /dev/example
实际名称和 metadata 由控制器生成。排查时可以查询:
kubectl get volumeattachment
kubectl describe volumeattachment <name>
status.attached: true 表示控制器认为 ControllerPublishVolume 已成功,不表示 Pod 内已经可以访问目录。之后还必须经过 NodeStage 和 NodePublish。
3. AttachRequired
CSIDriver 对象描述驱动能力:
kubectl get csidriver
kubectl get csidriver example.csi.vendor -o yaml
其中 spec.attachRequired 表示是否需要控制器侧 Attach:
true:通常需要ControllerPublishVolume,会有 VolumeAttachment;false:Kubernetes 跳过控制器 Attach,直接进入节点侧流程。
attachRequired: false 常见于某些网络文件系统或不需要将块设备连接到节点的驱动。不能因为没有 VolumeAttachment 就认定 CSI 异常;必须先看 CSIDriver 的能力声明。
4. Attach 的并发和幂等
控制器可能因为重试、leader 切换或网络超时重复调用 Attach。CSI 驱动必须实现幂等语义:
- 已经 Attach 到目标节点,再次 Attach 应返回成功或等价结果;
- 正在 Attach 时发生超时,控制器会重新查询或重试;
- 一个
ReadWriteOnce卷通常不能安全地同时 Attach 到多个节点; - 存储后端和驱动必须处理“旧节点仍认为已连接,但 Kubernetes 已尝试迁移”的中间状态。
如果云厂商 API 返回“卷已连接到其他实例”,Kubernetes 通常不会强行覆盖这个状态,而是等待驱动完成 Unpublish、后端解除连接或人工修复。
五、Mount:NodeStage、NodePublish 和 Pod 目录
1. Node 插件如何被 kubelet 使用
Node 插件通过 Unix Domain Socket 提供 CSI gRPC 服务。csi-node-driver-registrar 将插件信息注册给 kubelet,使 kubelet 知道:
- 节点上有哪些 CSI 驱动;
- 驱动的 socket 在哪里;
- 驱动是否支持某些 Node 能力。
在节点上检查 CSI 相关 Pod:
kubectl -n kube-system get pods -o wide | grep -i csi
kubectl -n kube-system logs <csi-node-pod> -c csi-node-driver-registrar
如果 Node 插件没有注册,常见结果是 kubelet 事件中出现:
driver name example.csi.vendor not found in the list of registered CSI drivers
这时问题还没有到“格式化失败”阶段,而是 kubelet 根本找不到 Node 驱动。
2. NodeStageVolume:节点级暂存
对于块设备文件系统卷,kubelet 通常先调用:
NodeStageVolume(volume_id, staging_target_path, volume_capability)
Node 插件可能执行:
- 找到 Attach 后出现的设备;
- 检查文件系统;
- 必要时执行
mkfs; - 将设备挂载到节点级 staging 目录。
例如逻辑上可能形成:
/dev/sdb
└── /var/lib/kubelet/plugins/kubernetes.io/csi/.../globalmount
NodeStage 的设计允许同一节点上的多个 Pod 共享一次节点级挂载,再通过不同目标目录暴露给各个 Pod。
对于某些文件系统型驱动,可能不存在本地块设备,但仍可以使用 staging 目录建立节点级挂载。
3. NodePublishVolume:暴露给 Pod
之后 kubelet 调用:
NodePublishVolume(volume_id, target_path, staging_target_path, volume_capability)
Node 插件通常将 staging 目录 bind mount 到 Pod 的目标路径:
globalmount
├── Pod A volume target
└── Pod B volume target
NodePublishVolume 的目标路径通常位于 kubelet 管理的 Pod volume 目录。容器运行时随后看到的是这个已经准备好的路径,而不是 CSI gRPC 调用本身。
4. 反向卸载
Pod 删除、卷不再使用或节点清理时,通常按相反方向执行:
NodeUnpublishVolume
↓
NodeUnstageVolume
↓
ControllerUnpublishVolume
↓
DeleteVolume(取决于 PV reclaimPolicy)
并非每次都立即执行所有步骤。例如:
- 同一节点仍有其他 Pod 使用该卷时,不能提前 Unstage;
- Pod 删除但 PVC 仍存在时,不能 DeleteVolume;
- PV 的
reclaimPolicy: Retain时,删除 PVC 不会自动删除后端卷; - 节点失联时,正常 Unpublish 可能无法完成。
5. Mount 失败的典型层次
事件中的错误位置很重要:
| 错误位置 | 更可能的原因 |
|---|---|
CreateVolume |
后端配额、参数、权限、区域 |
ControllerPublishVolume |
Attach 权限、卷已被其他节点占用、云 API 延迟 |
NodeStageVolume |
设备未出现、文件系统损坏、mkfs 或 mount 参数错误 |
NodePublishVolume |
目标路径、权限、bind mount、SELinux |
| 容器启动后读写失败 | 文件系统权限、fsGroup、应用自身、I/O 错误 |
应先查看 Pod 事件:
kubectl describe pod <pod-name>
kubectl get events --sort-by=.lastTimestamp
再查看 PV、PVC 和 CSI Pod 日志:
kubectl describe pvc <pvc-name>
kubectl describe pv <pv-name>
kubectl -n kube-system logs <csi-controller-pod> -c csi-provisioner
kubectl -n kube-system logs <csi-controller-pod> -c csi-attacher
kubectl -n kube-system logs <csi-node-pod> -c csi-plugin
不要在节点上直接删除 kubelet 的挂载目录来“清理故障”。这可能造成仍在使用该目录的容器失去文件系统,甚至损坏数据。应先确认 Pod 已停止、引用已解除,再根据驱动文档处理残留挂载。
六、一个完整的 Pod 使用流程
下面的 Pod 假设前面的 PVC 已经成功绑定:
apiVersion: v1
kind: Pod
metadata:
name: storage-demo
spec:
containers:
- name: app
image: busybox:1.36
command: ["/bin/sh", "-c"]
args:
- |
date > /data/created-at
while true; do
date >> /data/heartbeat
sleep 10
done
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: data-demo
执行:
kubectl apply -f pod.yaml
kubectl get pod storage-demo -w
kubectl exec storage-demo -- sh -c 'cat /data/created-at; tail /data/heartbeat'
预期过程是:
- 调度器选择节点;
- 若使用延迟绑定,PVC 可能在此时才供应;
- external-provisioner 创建后端卷;
- external-attacher 按需要创建 VolumeAttachment;
- Node 插件完成 staging 和 publish;
- kubelet 启动容器;
- 容器在
/data看到持久化文件。
删除 Pod 后重新创建:
kubectl delete pod storage-demo
kubectl apply -f pod.yaml
kubectl exec storage-demo -- cat /data/created-at
如果 PVC、PV 和后端卷没有被删除,created-at 应仍然存在。这个验证的是“Pod 生命周期与卷生命周期分离”,不是“所有数据都具备备份能力”。
七、访问模式与并发边界
访问模式是 Kubernetes 对卷使用方式的声明,常见模式包括:
ReadWriteOnce(RWO):一个节点可以读写;ReadOnlyMany(ROX):多个节点只读;ReadWriteMany(RWX):多个节点读写;ReadWriteOncePod(RWOP):一个卷只能被一个 Pod 使用。
这些模式不是所有 CSI 驱动和后端都支持。特别是 RWO 经常被误解为“只能有一个 Pod 使用”。更准确地说,RWO 主要约束节点级读写;同一节点上的多个 Pod 是否允许同时使用,还取决于驱动、kubelet 和应用语义。RWOP 才是更强的单 Pod 约束,但也要求支持对应能力的 Kubernetes 与 CSI 组件。
一个反例是:两个 Pod 分别被调度到两个节点,二者都引用同一个 RWO PVC。Kubernetes 可能让其中一个 Pod 成功挂载,另一个长期处于 ContainerCreating,事件中出现类似:
Multi-Attach error for volume ...
这不是简单的 Mount 命令错误,而是卷的并发使用约束与调度结果冲突。
八、扩容:容量变大不等于文件系统立即变大
PVC 扩容通常包含两个阶段:
- 控制器侧扩容后端卷;
- 节点侧扩展分区或文件系统。
如果驱动支持扩容,StorageClass 需要:
allowVolumeExpansion: true
修改 PVC:
kubectl patch pvc data-demo \
-p '{"spec":{"resources":{"requests":{"storage":"40Gi"}}}}'
观察:
kubectl get pvc data-demo -w
kubectl describe pvc data-demo
可能出现以下中间状态:
- 后端云盘已经从 20 GiB 变为 40 GiB;
- PV 容量状态尚未更新;
- 节点侧文件系统尚未执行扩展;
- Pod 重启或重新挂载后才完成文件系统扩容,取决于驱动和文件系统。
因此,不能只检查云厂商控制台中的容量,也不能只看 PVC 的 spec.resources.requests。应同时检查 PVC 状态、PV 状态、CSI resizer 日志和节点内的 df -h。
九、VolumeSnapshot:快照 API 的对象关系
Kubernetes 的快照 API 使用 snapshot.storage.k8s.io/v1。核心对象有三个:
VolumeSnapshotClass:指定哪个 CSI 驱动和删除策略;VolumeSnapshot:用户请求创建的快照;VolumeSnapshotContent:集群中某个实际后端快照的表示。
对象关系可以表示为:
VolumeSnapshotClass
↓ driver / deletionPolicy
VolumeSnapshot
↔ VolumeSnapshotContent
↓
后端 Snapshot
VolumeSnapshot 是命名空间级对象,通常由用户创建;VolumeSnapshotContent 是集群级对象,通常由控制器创建或绑定。
1. 创建快照
示例:
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: example-snapshot-class
driver: example.csi.vendor
deletionPolicy: Delete
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: data-demo-snapshot
spec:
volumeSnapshotClassName: example-snapshot-class
source:
persistentVolumeClaimName: data-demo
执行:
kubectl apply -f snapshot.yaml
kubectl get volumesnapshot data-demo-snapshot -w
kubectl describe volumesnapshot data-demo-snapshot
kubectl get volumesnapshotcontent
成功后应关注:
status:
readyToUse: true
restoreSize: 20Gi
readyToUse: true 表示控制器认为后端快照已经可以作为恢复源。它不等价于“应用已经一致地提交了所有事务”。
deletionPolicy 的语义很关键:
Delete:删除 VolumeSnapshot 时,控制器请求删除后端快照;Retain:删除 Kubernetes 快照对象后保留后端快照资源。
后端快照是否真正独立、是否占用完整容量、删除是否异步,属于存储厂商实现差异。
2. 从快照恢复 PVC
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-restored
spec:
storageClassName: example-csi
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
dataSource:
name: data-demo-snapshot
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
恢复要求通常包括:
- 快照已经
readyToUse: true; - CSI 驱动支持从快照创建卷;
dataSource指向同一命名空间内的 VolumeSnapshot;- 请求容量不小于快照恢复所需容量;
- StorageClass 和驱动能力匹配。
恢复创建的是一个新的 PVC 和通常新的后端卷,不是把原 PVC 的名字“回滚”到过去状态。
3. 从 PVC 克隆 PVC
某些 CSI 驱动支持卷克隆:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-clone
spec:
storageClassName: example-csi
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
dataSource:
name: data-demo
kind: PersistentVolumeClaim
克隆与快照恢复不同:
- 克隆的源是 PVC;
- 快照恢复的源是 VolumeSnapshot;
- 两者都不是跨所有驱动通用的能力;
- 后端可能实现为快速复制、写时复制或完整复制。
必须查驱动文档确认支持情况,不能仅因为 API 对象可以提交,就推断后端一定支持。
十、快照的一致性和备份边界
1. 崩溃一致性不等于应用一致性
假设应用向数据库写入一笔事务,数据可能同时存在于:
- 数据库内存;
- 数据库 WAL 或 redo log;
- 操作系统页缓存;
- 存储设备缓存;
- 后端卷数据块。
直接创建卷快照通常只能保证某种后端定义的瞬时状态。它可能接近“机器突然断电后磁盘留下的状态”,即崩溃一致性(crash consistency),但不一定保证应用一致性(application consistency)。
应用一致性通常需要:
- 暂停写入或执行数据库 checkpoint;
- 刷新应用和操作系统缓存;
- 创建快照;
- 恢复应用写入。
例如,不能把下面的流程当作 PostgreSQL 的严格备份协议:
kubectl create -f volumesnapshot.yaml
更可靠的数据库备份可能需要数据库原生备份工具、WAL 归档、备份代理或 CSI 协调机制。具体方式取决于数据库和存储系统。
2. 快照不是完整备份
卷快照通常只覆盖卷数据,不自动覆盖:
- Deployment、StatefulSet、Service 等 Kubernetes 对象;
- Secret、ConfigMap、RBAC;
- 应用版本和配置;
- 数据库集群拓扑;
- 跨集群恢复所需的映射关系;
- 快照所在存储系统本身的可用性。
因此,完整灾备至少要同时考虑:
同一存储阵列中的快照,不能自动抵御阵列级故障;同一区域的复制,也不能自动抵御区域级故障。
十一、故障排查:按状态边界定位
CSI 故障最有效的排查方法不是反复重启 Pod,而是确定流程停在哪个状态。
1. PVC 长期 Pending
检查:
kubectl get pvc,pv,sc
kubectl describe pvc <pvc-name>
kubectl get pods -A | grep -E 'provision|csi'
判断逻辑:
- 没有 PV,且 PVC 需要动态供应:检查 provisioner;
- PVC 事件提示无匹配 StorageClass:检查
storageClassName; - 使用
WaitForFirstConsumer且没有 Pod:这是预期延迟; - 后端权限或配额错误:查看 controller 和 provisioner 日志;
- provisioner 名称不匹配:StorageClass 的
provisioner必须与 CSI 驱动注册名一致。
2. PVC 已 Bound,但 Pod Pending 或 ContainerCreating
检查:
kubectl describe pod <pod-name>
kubectl get volumeattachment
kubectl describe volumeattachment <attachment-name>
kubectl get csinode
kubectl get csidriver
如果是 Attach 阶段,重点看:
- VolumeAttachment 是否存在;
attached是否为true;- 节点名是否正确;
- 后端是否仍把卷连接到旧节点;
- 驱动是否声明
attachRequired: false。
如果 Attach 已成功,继续看 Node 插件日志和 kubelet 事件,因为失败可能发生在 NodeStage 或 NodePublish。
3. 设备存在但 mount 失败
这通常已经越过了供应和 Attach 阶段。可能原因包括:
- 文件系统类型与
fsType不匹配; - 文件系统损坏;
- 节点缺少 mount helper;
- 挂载参数不受内核支持;
- SELinux、AppArmor 或权限策略阻止操作;
- 设备路径尚未稳定出现;
- 块设备被其他进程占用。
应在目标节点上以只读、谨慎方式检查设备和挂载状态。生产环境不要随意执行 fsck -y、重新格式化或删除挂载目录;这些动作可能直接破坏数据。
4. 卷无法卸载
常见原因:
- 进程仍持有工作目录或文件句柄;
- 容器运行时没有完成清理;
- Node 插件重启导致状态丢失;
- 节点失联;
- 网络文件系统连接中断;
- 驱动返回非幂等错误。
可以检查节点上的挂载关系和进程占用,但修复动作必须遵循驱动和云平台的 fencing 规则。对于块设备,如果旧节点仍可能写入,直接在新节点强制 Attach 可能导致文件系统损坏。
十二、故障恢复中的幂等、超时和状态漂移
CSI 调用通过网络完成,因此调用者可能遇到这种情况:
- kubelet 调用
NodePublishVolume; - CSI 插件已经完成挂载;
- 网络响应丢失;
- kubelet 认为调用失败并重试;
- 插件发现目标路径已经存在。
如果 CSI 驱动不是幂等的,重试可能导致“目标已存在”“重复挂载”或错误清理。正确实现应把“已经达到目标状态”视为成功,或者通过查询确认实际状态。
状态漂移也可能发生:
- Kubernetes 对象显示未 Attach,但云平台已连接;
- VolumeAttachment 显示 attached,但节点设备没有出现;
- Pod 已删除,但 Node 插件仍保留挂载;
- 后端卷已删除,但 PV 对象因控制器故障仍存在。
这时应分别对比四类事实:
Kubernetes 对象状态
CSI sidecar 日志
CSI 驱动日志
存储后端实际状态
不要只相信其中一层。status 是控制器观察到的状态,不一定是后端实时事实。
十三、节点故障与强制迁移风险
对于 RWO 块卷,节点宕机后,Kubernetes 可能需要把 Pod 调度到新节点。但安全迁移要求旧节点不会继续写入,也就是需要 fencing 或等价的存储侧保护。
危险流程如下:
旧节点失联
↓
Kubernetes 认为 Pod 应迁移
↓
新节点尝试 Attach
↓
后端仍认为旧节点持有卷
如果此时绕过保护强制把卷接到新节点,而旧节点实际上仍运行并写入,就可能产生双写和文件系统损坏。生产环境的恢复策略应明确:
- 云平台是否支持强制分离;
- 强制分离是否有数据损坏风险;
- CSI 驱动如何报告旧节点状态;
- StatefulSet 的故障转移时间;
- 数据库是否能通过 WAL 或日志恢复;
- 是否需要人工确认 fencing。
“Pod 一直 Terminating”不应直接通过删除 VolumeAttachment 或 PV 解决。删除 Kubernetes 对象可能掩盖后端仍然占用卷的事实,增加双挂载风险。
十四、CSI 与 StatefulSet 的关系
StatefulSet 常通过 volumeClaimTemplates 为每个副本创建独立 PVC。以三个副本为例,通常得到:
data-db-0
data-db-1
data-db-2
这三个 PVC 不会因为属于同一个 StatefulSet 就自动共享一个卷。每个副本的存储生命周期通常与其稳定网络身份和序号相关。
删除 StatefulSet 与删除 PVC 是两个不同动作。是否保留 PVC,取决于资源策略和具体配置;生产环境不能只凭“删除 Pod 会不会丢数据”的经验判断,应实际检查 PVC、PV 的回收策略以及 StatefulSet 的保留行为。
CSI 只负责卷的供应、连接、挂载和快照等存储动作,不负责数据库主从复制、选主、事务一致性或应用级故障转移。
十五、生产中最容易混淆的几个结论
“PVC Bound 就代表应用可以读写”
不成立。Bound 只说明 PVC 与 PV 已建立绑定关系。Attach、NodeStage、NodePublish 和容器权限仍可能失败。
“VolumeAttachment attached 就代表 Pod 已经挂载”
不成立。它只代表控制器侧 Attach 成功。节点侧仍可能找不到设备、无法格式化或无法完成 mount。
“快照成功就代表数据库可恢复”
不成立。快照可能只是崩溃一致性状态,且恢复还需要 Kubernetes 对象、数据库配置、密钥和应用编排。
“RWO 只能被一个 Pod 使用”
不完全准确。RWO 的核心是单节点读写约束;同节点多 Pod 的具体行为与驱动及挂载方式有关。需要单 Pod 约束时,应确认 RWOP 支持。
“删除 PV 就能修复存储故障”
高风险。PV 是 Kubernetes 对后端卷的引用和生命周期对象。删除它可能导致引用丢失、回收动作触发或排障线索消失,不会自动修复后端卷、文件系统或旧节点占用问题。
“CSI 驱动版本只要能启动就兼容”
不成立。CSI sidecar、Kubernetes API、CSI 驱动、快照 CRD、云厂商 API 和节点内核能力都存在兼容性边界。升级前应查对应版本矩阵,并在测试集群验证供应、Attach、挂载、扩容、快照和恢复,而不只是验证 Pod 能启动。
十六、用一条状态链理解 CSI
一个动态供应的块存储卷,可以抽象为以下状态:
PVC Pending
↓ CreateVolume 成功
PV Created / PVC Bound
↓ 需要 Attach 时创建 VolumeAttachment
ControllerPublishVolume 成功
↓ NodeStageVolume
节点级 Staged
↓ NodePublishVolume
Pod 目标路径 Published
↓
容器可访问
每个箭头都有独立的前置条件:
反向清理则要求引用关系逐层消失:
这条链解释了为什么“PVC 已绑定但 Pod 仍启动失败”是正常可能性,也解释了为什么强行删除中间对象可能造成更严重的数据风险。
CSI 的核心不是某个 YAML 字段,而是控制器侧的后端资源协调、节点侧的设备和挂载操作、Kubernetes 对象状态以及存储系统真实状态之间的闭环。掌握 Controller、Node、Attach、Mount 和快照的边界后,才能把一次故障准确归因到供应、拓扑、连接、节点挂载、应用一致性或后端存储,而不是笼统地称为“PVC 挂载失败”。
系列导航与关联阅读
- 系列入口:Kubernetes 完整学习路线:从 Pod 与控制面到安全、运维和 Operator
- 上一篇:StorageClass 与动态供给:Provisioner、Binding、扩容和拓扑
- 下一篇:Kubernetes VolumeSnapshot:一致性、Class、恢复、克隆和备份边界
- 延伸:Kubernetes CRI、CNI 与 CSI:运行时、网络、存储插件责任边界
官方资料
本文依据 Kubernetes、CNCF 与相关项目官方文档重新梳理;正文和生产清单由 WR BLOG 编写。

评论
0 条讨论