Skip to content

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 插件内部做了这些事:

  1. 接收请求: Sidecar external-provisioner 监听 PVC 对象,调用 CSI 驱动的 CreateVolume gRPC 接口。

  2. 翻译请求: CSI 驱动将 gRPC 请求中携带的参数(size=100Gicsi.storage.k8s.io/pvc/name=mypvc)解析出来。

  3. 调用后端 API: 驱动将这些参数转换成对后端存储系统的 API 调用(例如 CreateFileSystem(name=mypvc, capacity=100))。

  4. 返回结果: 存储系统创建好卷后,返回卷的 ID 和访问路径。

  5. 完成绑定: 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 规范中的 NodeStageVolumeNodePublishVolume 流程允许存储厂商实现 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 模式让功能可以按需组合

本文核心收获:

  1. CSI 的两个核心组件:Controller(管理面)和 Node(数据面)
  2. 完整挂载流程:PVC → Provisioner → Controller → 后端存储 → PV → Pod
  3. 存储厂商只需实现 CSI 接口,无需修改 K8s 代码
  4. 工程中需关注幂等性、残留清理、RWX 支持等实际问题

读完这篇,你不仅知道如何部署和使用 CSI 驱动,更能理解其背后的设计原理和工程实践。


延伸阅读:

最后更新2026/06/06 06:12
如果你觉得这篇文章有帮助,或者想聊聊技术、工作,欢迎通过下面方式联系我:
contact fishfinal