乐于分享
好东西不私藏

Kubernetes 存储插件:从 In-tree 到 CSI

Kubernetes 存储插件:从 In-tree 到 CSI

Kubernetes 最早是用来运行无状态服务的。Pod 可以随时销毁、重建、漂移到别的节点,反正不丢数据。

后来用户开始在上面跑数据库、消息队列、需要持久化的文件系统。Kubernetes 不得不解决一个问题:怎么让有状态应用安全地跑在容器里?

答案在存储上。Kubernetes 的存储架构经历了三次大的变动。


第一章:In-tree 插件——一开始能用,后来不够用了

什么是 In-tree

Kubernetes 早期(v1.0 到 v1.7),所有存储驱动的源码都直接写在 K8s 核心仓库里。AWS EBS、GCE PD、Ceph RBD、NFS、iSCSI……每种存储后端对应 pkg/volume/ 下的一个子目录。

kubernetes/└── pkg/    └── volume/        ├── aws_ebs/        ├── gce_pd/        ├── ceph/        ├── nfs/        ├── iscsi/        └── ... (40+ 种存储插件)

用户在 Pod 或 PVC 里指定 volume 类型,Kubelet 就调用对应的插件完成挂载。

为什么一开始这么设计

因为快。

早期 K8s 生态还没成型,集中开发是最直接的方式。核心贡献者熟悉代码结构,合并 PR 也快,用户体验统一。

后来出了什么问题

  1. 发布节奏被绑死

    存储厂商修复一个 bug 或加一个功能,得等 K8s 的版本发布周期。K8s 每季度发一个大版本,厂商的排期完全被动。

  2. 代码块膨胀

    每个存储插件的依赖都会被编译进 kube-controller-manager 和 kubelet。即使用户只装 AWS EBS,其他的插件如NFS、Ceph、GlusterFS 的依赖也一起打进去了。编译时间越来越长,二进制文件越来越大。

  3. 质量把控困难

    核心维护者不可能精通所有存储系统。Ceph 插件出了 bug,得等 Ceph 社区的专家来修,但那个专家不一定熟悉 K8s 的合入流程。

  4. 存储厂商的开发体验差

    你是一家存储公司的工程师,想让自己的产品支持 K8s。fork 了整个仓库,写了几千行 Go 代码,提了 PR——然后等了三个月,因为核心团队的代码审查排期太长。

存储生态的多样性和 K8s 核心代码的稳定性之间有冲突。社区得选一条路。


第二章:FlexVolume——折中的过渡方案

2017 年前后,Kubernetes 推出了 FlexVolume。思路是把存储驱动从核心代码中剥离出来,作为独立的可执行文件部署在节点上。

Kubelet 在预定义路径下找驱动,通过 JSON 格式的命令行调用通信:

/usr/libexec/kubernetes/kubelet-plugins/volume/exec/vendor~driver/driver init/usr/libexec/kubernetes/kubelet-plugins/volume/exec/vendor~driver/driver attach '{"kubernetes.io/pv/name":"pv-001",...}'/usr/libexec/kubernetes/kubelet-plugins/volume/exec/vendor~driver/driver mount '{"kubernetes.io/pv/name":"pv-001",...}'

改进之处

  • 存储驱动不再需要修改 K8s 源码,独立开发、独立部署

  • 存储厂商可以自己发版本

  • 只在需要的节点上装对应驱动

遗留的问题

FlexVolume 解决了”出树”的问题,但本身还是个过渡方案,“出树”方向是对的,但实现方式还不够好:


第三章:CSI——容器存储接口

CSI 是什么

CSI(Container Storage Interface)是一套行业标准,最初由 Kubernetes社区发起,并逐步发展为容器编排领域的通用存储接口标准。它定义了一套 gRPC API,存储供应商可以用同一套接口为任何容器平台提供驱动。

核心想法很简单:存储驱动不应该跑在编排系统的进程内,应该作为独立的标准服务跑。

CSI 的架构通常由两部分组成:K8s 官方提供的通用组件(Sidecar) 和 存储厂商实现的驱动(Driver)。

CSI 架构

Sidecar 

CSI 把不同的存储操作拆分到独立的容器中,每个 Sidecar 负责与 K8s API 交互,通过 gRPC 调用存储厂商的CSI 驱动。

  • external-provisioner:

    监听 PVC 的创建,调用 Driver 去底层的云存储或物理存储上创建(Create)真实的存储卷。

  • external-attacher:

    监听 VolumeAttachment 对象,调用 Driver 将创建好的存储卷挂载(Attach)到指定的 K8s 工作节点(Node)上。

  • external-resizer:

    监听 PVC 的容量变化,调用 Driver 动态扩容存储卷。

  • external-snapshotter:

    负责处理存储快照(Snapshot)的创建和删除。

  • node-driver-registrar:

    负责将存储驱动注册到该节点上的 kubelet 中。

好处:

  • 如果存储不支持快照,就不部署 snapshotter

  • Sidecar 版本和驱动版本可以各自升级

  • 每个组件只做一件事

  • 同一个 Sidecar 可以和任何 CSI 驱动配合

这些Sidecar组件大部分都是K8s社区维护的。

Github仓库链接:

https://github.com/kubernetes-csi。

CSI 的 gRPC 接口

厂商编写的驱动通常会实现三个gRPC服务:

Controller Service——负责存储卷的生命周期管理(创建、删除、Attach、Detach 等),通常以 Deployment 形式全局运行:

Node Service——运行在每个工作节点上(通常是 DaemonSet),负责将已经挂载到机器上的存储卷格式化并挂载(Mount)到 Pod 的专属目录中:

Identity Service——提供驱动的基础信息(名称、版本、支持的能力等)

CSI标准只定义了一组gRPC接口,K8s本身不会直接调用这些接口。官方Sidecar负责:

存储厂商通常只实现CSI Driver,这个部分就是上面列举的三组核心服务的实现部分。

卷的生命周期

1. 用户创建 PVC       ↓2. external-provisioner 监听到 PVC 事件       ↓3. 调用 CSI Driver 的 CreateVolume → 存储后端创建卷       ↓4. K8s 调度器将 Pod 调度到某个节点       ↓5. external-attacher 调用 ControllerPublishVolume → 卷挂载到目标节点       ↓6. Kubelet 调用 NodeStageVolume → 设备准备与预挂载       ↓7. Kubelet 调用 NodePublishVolume → 挂载到 Pod 目录       ↓8. Pod 运行,读写数据       ↓9. Pod 删除 → NodeUnpublishVolume       ↓10. 节点不再需要卷 → ControllerUnpublishVolume       ↓11. PVC 删除 → DeleteVolume

这个顺序非常重要,客户端发起PVC创建请求后,调度器先将要运行Pod的节点选择出来,然后厂商驱动调用后端存储API创建卷并将卷挂到目标节点,这些动作由“Controller Service”完成。之后Pod才能真正起来,随后运行在节点上的“Node Service”完成卷的初始化并将可以使用的卷挂载到Pod的指定目录中。

这样K8s核心代码不需要针对每个存储系统写逻辑。

CSI 控制器部署示例

在实际部署中,存储厂商实现的 “Controller Service” 是和 K8s 官方的多个 Sidecar 容器运行在同一个 Pod 中。

这个 Pod 通常被称为 “CSI Controller Pod”(或 CSI Plugin Controller),一般以 Deployment 的形式部署,在集群中运行 1 个或多个副本(通过 Leader Election 实现高可用)。

下面是一个示例:

---apiVersion: apps/v1kind: Deploymentmetadata:name: ebs-csi-controllernamespace: kube-systemspec:replicas:2selector:matchLabels:app: ebs-csi-controllertemplate:spec:serviceAccountName: ebs-csi-controller-sacontainers:-name: csi-provisionerimage: registry.k8s.io/sig-storage/csi-provisioner:v4.0.0args:- --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock- --feature-gates=Topology=true-name: csi-attacherimage: registry.k8s.io/sig-storage/csi-attacher:v4.5.0args:- --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock-name: csi-resizerimage: registry.k8s.io/sig-storage/csi-resizer:v1.10.0args:- --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock-name: csi-snapshotterimage: registry.k8s.io/sig-storage/csi-snapshotter:v7.0.0args:- --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock-name: ebs-pluginimage: amazon/aws-ebs-csi-driver:v1.28.0args:- --endpoint=$(CSI_ENDPOINT)env:-name: CSI_ENDPOINTvalue: unix:///var/lib/csi/sockets/pluginproxy/csi.sockvolumeMounts:-name: socket-dirmountPath: /var/lib/csi/sockets/pluginproxy/volumes:-name: socket-diremptyDir:{}

为什么它们要挤在同一个Pod中?

这是一种非常经典的 Sidecar 设计模式。其核心逻辑是:“分工合作,本地通信”。

官方 Sidecar 负责“听”:external-provisioner、external-attacher 等容器各自各司其职,专门负责监听 K8s 集群里的 API 对象(如 PVC、VolumeAttachment)的变化。

厂商 Driver 负责“干”:存储厂商的 Controller Service 容器(比如 aws-ebs-csi-driver)只负责接收具体的指令并操作底层存储。

通过 localhost (Unix Socket) 通信:

  • 当 Sidecar 监听到需要创建卷时,它会通过 gRPC 协议调用厂商的 Controller Service。

  • 因为它们在同一个 Pod 内,它们可以通过共享的 Unix Domain Socket(通常挂载一个特殊的 emptyDir 卷)进行极高效率的本地通信,而不需要走复杂的网络。


第四章:三者对比


第五章:迁移过程

CSI 再好,也不能一夜之间替换掉所有 In-tree 插件。社区采取了渐进式迁移策略。

迁移时间线

  • 2018(v1.13):CSI 正式 GA,为迁移奠定标准底座

  • 2019(v1.14 - v1.17):引入 CSI Migration 机制(Alpha -> Beta),开始支持透明路由

  • 2021(v1.21):绝大多数 In-tree 存储插件被正式标记为 Deprecated

  • 2022(v1.24): 核心插件(AWS EBS、GCE PD)迁移正式 GA 并默认锁定开启

  • 2022(v1.26):大部分厂商In-tree存储插件移除,迁移到CSI

  • 2023 - 2024(v1.27+):其余长尾插件(如 vSphere 等)代码分批完成最终清理

迁移机制:CSI Migration Feature Gate

K8s 引入了CSIMigration特性门控,用户不需要修改 YAML,In-tree 插件的请求会自动转发到对应的 CSI 驱动:

# PVC 依然用 in-tree 的 provisioner 标识kind: StorageClassmetadata:name: my-scprovisioner: kubernetes.io/aws-ebs   # in-tree 标识符# 内部:CSIMigration 自动转发到 ebs.csi.aws.com

平台团队部署好 CSI 驱动,打开特性门控,业务方的 YAML 不用改。


回顾

Kubernetes 存储插件走了这么一条路:

  • In-tree:所有驱动写进核心仓库,快但臃肿
  • FlexVolume:驱动拆到节点上,解决了独立开发但实现方式粗糙
  • CSI:定义标准 gRPC 接口,驱动作为独立服务运行

In-tree 时期,K8s 核心团队试图覆盖所有存储场景。FlexVolume 先跑起来再说。CSI 则划清了边界,让生态在标准接口上各自发展。

现在用 kubectl create pvc 的时候,背后可能是阿里云、Ceph或任何存储厂商的 CSI 驱动在跑。不同团队开发,同一套接口。


参考资料

  • CSI Specification: 
    https://github.com/container-storage-interface/spec
  • Kubernetes CSI Documentation:
    https://kubernetes.io/docs/concepts/storage/volumes/#csi
  • CSI Migration Design Doc: 
    https://github.com/kubernetes/enhancements/tree/master/keps/sig-storage/1495-csi-migration