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 也快,用户体验统一。
后来出了什么问题
发布节奏被绑死
存储厂商修复一个 bug 或加一个功能,得等 K8s 的版本发布周期。K8s 每季度发一个大版本,厂商的排期完全被动。
代码块膨胀
每个存储插件的依赖都会被编译进 kube-controller-manager 和 kubelet。即使用户只装 AWS EBS,其他的插件如NFS、Ceph、GlusterFS 的依赖也一起打进去了。编译时间越来越长,二进制文件越来越大。
质量把控困难
核心维护者不可能精通所有存储系统。Ceph 插件出了 bug,得等 Ceph 社区的专家来修,但那个专家不一定熟悉 K8s 的合入流程。
存储厂商的开发体验差
你是一家存储公司的工程师,想让自己的产品支持 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
夜雨聆风