Appearance
Kubernetes CSI 核心原理:从挂载流程到工程实践
前言:为什么需要 CSI?
如果你在生产环境跑过 Kubernetes 的有状态应用(如数据库、AI 训练、消息队列),你一定遇到过这个问题:Pod 迁移了,数据怎么办?
Kubernetes 本身并不生产存储,它只负责调度。为了让云厂商、开源存储和商业存储都能无缝接入,CSI(Container Storage Interface)应运而生。
一句话定义 CSI: CSI 是 Kubernetes 给所有存储厂商定的一套统一接口标准。有了它,无论底层是 AWS EBS、Ceph 还是自研存储,K8s 都能用同样的方式(创建、挂载、快照、扩容)来操作。
CSI 核心组件与架构
CSI 的实现通常由两个核心组件(以 Pod 形式运行在 K8s 中)构成,分工明确:
1. CSI Controller(管理面)
- 部署方式: Deployment(通常 1 个或 HA 多副本)
- 核心职责: 管理存储卷的生命周期,不关心数据怎么挂进 Pod
- 主要接口:
CreateVolume/DeleteVolume(创建/删除后端存储)CreateSnapshot/DeleteSnapshot(快照)ControllerExpandVolume(扩容)
2. CSI Node(数据面)
- 部署方式: DaemonSet(每个 Node 一个 Pod)
- 核心职责: 执行具体的挂载动作,把存储卷挂到 Pod 的文件系统里
- 主要接口:
NodePublishVolume/NodeUnpublishVolume(挂载/卸载到 Pod 目录)NodeStageVolume/NodeUnstageVolume(格式化/挂载到全局目录,用于 Raw Block 或 iSCSI 等多节点共享场景)
3. 外部辅助容器(Sidecar)
官方提供多个 Sidecar 容器,与 CSI 驱动一起运行,简化开发:
| Sidecar | 职责 |
|---|---|
external-provisioner | 监听 PVC 事件,自动调用 CreateVolume |
external-attacher | 处理 VolumeAttachment,调用 ControllerPublishVolume |
external-resizer | 监听 PVC 扩容请求,调用 ControllerExpandVolume |
external-snapshotter | 管理 VolumeSnapshot CRD |
一次完整的存储挂载流程
让我们跟踪一个 PVC 从创建到 Pod 使用的全过程:
存储厂商如何实现 CSI 插件?
以自研存储系统为例,当用户执行 kubectl apply -f pvc.yaml 时,CSI 插件内部做了这些事:
接收请求: Sidecar
external-provisioner监听 PVC 对象,调用 CSI 驱动的CreateVolumegRPC 接口。翻译请求: CSI 驱动将 gRPC 请求中携带的参数(
size=100Gi、csi.storage.k8s.io/pvc/name=mypvc)解析出来。调用后端 API: 驱动将这些参数转换成对后端存储系统的 API 调用(例如
CreateFileSystem(name=mypvc, capacity=100))。返回结果: 存储系统创建好卷后,返回卷的 ID 和访问路径。
完成绑定: Provisioner 将卷 ID 写入 PV 对象,PVC 与 PV 完成绑定。
关键点: 存储厂商不需要修改一行 Kubernetes 核心代码,只需要实现这套 gRPC 接口,就能让任何 Kubernetes 集群使用该存储。
常见工程问题与深度思考
在生产环境中使用 CSI,有几个值得深入探讨的问题:
1. Pod 被强制删除,NodeUnpublish 没来得及执行怎么办?
现象: 宿主机上可能存在残留的挂载点,导致下次 Pod 重新调度到同一节点时,因为目录非空或设备忙而挂载失败。
解决方案:
- 幂等性设计: CSI 驱动必须处理好重复调用
NodePublishVolume的场景(检查是否已挂载,已挂载则直接返回成功) - 自愈机制: 利用 Kubernetes 的 VolumeAttachment 状态机,配合
external-attacher进行解绑和清理 - 运维兜底: 提供节点维护脚本,定期扫描并清理孤儿挂载点
2. 如何支持多节点读写(ReadWriteMany, RWX)?
- 挑战: 大多数块存储(如 AWS EBS)不支持多个 Pod 同时挂载读写
- CSI 的做法: CSI 规范中的
NodeStageVolume和NodePublishVolume流程允许存储厂商实现 NFS 或并行文件系统(如 CephFS)的挂载逻辑,从而向 Kubernetes 返回ReadWriteMany的能力
3. 如何从头实现一个最简单的 CSI 驱动?
生产级驱动很复杂,但教学级 demo 可以在几小时内完成:
go
// 伪代码示例:一个极简 CSI 的 CreateVolume 接口
func (s *identityServer) CreateVolume(ctx context.Context, req *csi.CreateVolumeRequest) (*csi.CreateVolumeResponse, error) {
name := req.GetName() // 卷名称
size := req.GetCapacityRange().GetRequiredBytes() // 容量
// 伪造一个后端存储操作
volumeID := "fake-vol-" + name
// 在宿主机创建一个空目录作为"后端存储"
os.MkdirAll("/tmp/csi-volumes/"+volumeID, 0755)
return &csi.CreateVolumeResponse{
Volume: &csi.Volume{
VolumeId: volumeID,
CapacityBytes: size,
},
}, nil
}生产级驱动还需要考虑:身份验证、限流、监控、优雅关闭、错误处理等细节。
总结
理解 CSI,关键是掌握它的分层设计思想:
- 解耦: Kubernetes 核心不关心存储实现细节
- 标准化: 统一接口让任意存储厂商都能接入
- 可扩展: Sidecar 模式让功能可以按需组合
本文核心收获:
- CSI 的两个核心组件:Controller(管理面)和 Node(数据面)
- 完整挂载流程:PVC → Provisioner → Controller → 后端存储 → PV → Pod
- 存储厂商只需实现 CSI 接口,无需修改 K8s 代码
- 工程中需关注幂等性、残留清理、RWX 支持等实际问题
读完这篇,你不仅知道如何部署和使用 CSI 驱动,更能理解其背后的设计原理和工程实践。
延伸阅读:
- Kubernetes CSI 官方文档
- CSI Spec on GitHub
- 开源 CSI 驱动源码参考:
csi-driver-nfs、csi-driver-smb
