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——折中的过渡方案
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 解决了"出树"的问题,但本身还是个过渡方案:
| 问题 | 说明 |
|---|---|
| 必须部署到每个节点 | 驱动是节点上的可执行文件 |
| 生命周期绑定 Kubelet | 驱动的更新和重启依赖 Kubelet |
| 功能有限 | 最初只支持 attach/mount/detach/unmount,后来才补上 resize、snapshot |
| 接口设计粗糙 | 基于 JSON + 命令行通信,没有类型安全和结构化错误处理 |
| 权限模型粗糙 | 驱动以 Kubelet 的权限运行,没有细粒度的 RBAC |
"出树"方向是对的,但实现方式还不够好。
第三章:CSI——容器存储接口
CSI 是什么
CSI(Container Storage Interface)是一套行业标准,由 Kubernetes、Docker、Containerd、Mesos 等多个容器编排项目的社区共同制定。它定义了一套 gRPC API,存储供应商可以用同一套接口为任何容器平台提供驱动。
核心想法很简单:存储驱动不应该跑在编排系统的进程内,应该作为独立的标准服务跑。
CSI 的架构通常由两部分组成:K8s 官方提供的通用组件(Sidecar) 和 存储厂商实现的驱动(Driver)。
CSI 架构

Sidecar
CSI 把不同的存储操作拆分到独立的容器中,每个 Sidecar 负责与 K8s API 交互,通过 gRPC 调用存储厂商的CSI 驱动。
| Sidecar 容器 | 职责 | 监听的事件 |
|---|---|---|
external-provisioner |
创建/删除卷 | PVC 创建事件 |
external-attacher |
挂载/卸载卷到节点 | VolumeAttachment 事件 |
external-resizer |
在线扩容卷 | PVC 扩容请求 |
external-snapshotter |
卷快照 | VolumeSnapshot 事件 |
好处:
- 如果存储不支持快照,就不部署 snapshotter
- sidecar 版本和驱动版本可以各自升级
- 每个组件只做一件事
- 同一个 sidecar 可以和任何 CSI 驱动配合
这些Sidecar组件大部分都是K8s社区维护的。
Github仓库链接:
https://github.com/kubernetes-csi。
CSI 的 gRPC 接口
厂商编写的驱动通常会实现三个gRPC服务:
Controller Service——Control Plane 侧:
CreateVolume/DeleteVolumeControllerPublishVolume/ControllerUnpublishVolumeValidateVolumeCapabilitiesControllerExpandVolumeCreateSnapshot/DeleteSnapshotListVolumes
Node Service——节点上:
NodeStageVolume——预挂载(格式化、文件系统准备)NodePublishVolume——最终挂载到 Pod 目录NodeUnpublishVolume——卸载NodeUnstageVolume——清理预挂载状态NodeExpandVolume——节点侧扩容NodeGetVolumeStats——获取卷容量和使用量
Identity Service:
GetPluginInfoProbe
卷的生命周期
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/v1
kind: Deployment
metadata:
name: ebs-csi-controller
namespace: kube-system
spec:
replicas: 2
selector:
matchLabels:
app: ebs-csi-controller
template:
spec:
serviceAccountName: ebs-csi-controller-sa
containers:
- name: csi-provisioner
image: registry.k8s.io/sig-storage/csi-provisioner:v4.0.0
args:
- --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock
- --feature-gates=Topology=true
- name: csi-attacher
image: registry.k8s.io/sig-storage/csi-attacher:v4.5.0
args:
- --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock
- name: csi-resizer
image: registry.k8s.io/sig-storage/csi-resizer:v1.10.0
args:
- --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock
- name: csi-snapshotter
image: registry.k8s.io/sig-storage/csi-snapshotter:v7.0.0
args:
- --csi-address=/var/lib/csi/sockets/pluginproxy/csi.sock
- name: ebs-plugin
image: amazon/aws-ebs-csi-driver:v1.28.0
args:
- --endpoint=$(CSI_ENDPOINT)
env:
- name: CSI_ENDPOINT
value: unix:///var/lib/csi/sockets/pluginproxy/csi.sock
volumeMounts:
- name: socket-dir
mountPath: /var/lib/csi/sockets/pluginproxy/
volumes:
- name: socket-dir
emptyDir: {}
为什么它们要挤在同一个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 卷)进行极高效率的本地通信,而不需要走复杂的网络。
第四章:三者对比
| 维度 | In-tree | FlexVolume | CSI |
|---|---|---|---|
| 代码位置 | K8s 核心仓库 | 节点上的可执行文件 | 独立容器/进程 |
| 通信方式 | 函数调用 | JSON 命令行 | gRPC |
| 部署方式 | 内置 | 每个节点装可执行文件 | DaemonSet + Deployment |
| 开发门槛 | 高(需理解 K8s 内部) | 中(写脚本处理 JSON) | 低(实现标准 gRPC) |
| 高级功能 | 需 K8s 版本支持 | 不完整 | 完整(快照/扩容/拓扑) |
| 权限模型 | 共享 K8s 内部权限 | 以 Kubelet 权限运行 | 独立 RBAC |
| 跨平台 | 仅 Kubernetes | 部分支持 | 全容器生态通用 |
| 生命周期 | 随 K8s 发布 | 绑定 Kubelet | 完全独立 |
| 社区状态 | 已废弃 | 已废弃 | 当前标准 |
第五章:迁移过程
CSI 再好,也不能一夜之间替换掉所有 In-tree 插件。社区采取了渐进式迁移策略。
迁移时间线
- 2018(v1.9):CSI Alpha
- 2019(v1.13):CSI Beta,开始支持 In-tree 到 CSI 的迁移
- 2020(v1.17):CSI GA,AWS EBS、GCE PD 等核心插件完成迁移
- 2021(v1.21):大部分 In-tree 插件标记为 Deprecated
- 2022(v1.26):In-tree 存储插件正式移除
迁移机制:CSI Migration Feature Gate
K8s 引入了 CSIMigration 特性门控,用户不需要修改 YAML,In-tree 插件的请求会自动转发到对应的 CSI 驱动:
# PVC 依然用 in-tree 的 provisioner 标识
kind: StorageClass
metadata:
name: my-sc
provisioner: kubernetes.io/aws-ebs # in-tree 标识符
# 内部:CSIMigration 自动转发到 ebs.csi.aws.com
平台团队部署好 CSI 驱动,打开特性门控,业务方的 YAML 不用改。
第六章:常用 CSI 驱动
主流驱动
| 驱动 | 存储后端 | 成熟度 | 备注 |
|---|---|---|---|
aws-ebs-csi-driver |
AWS EBS | GA | EKS 官方推荐 |
gcp-compute-persistent-disk-csi-driver |
GCE PD | GA | GKE 官方推荐 |
azuredisk-csi-driver |
Azure Disk | GA | AKS 官方推荐 |
ceph-csi |
Ceph RBD/CephFS | GA | 开源存储 |
nfs-subdir-external-provisioner |
NFS | 社区 | 开发测试常用 |
longhorn |
Longhorn | GA | 轻量级分布式块存储 |
rook-ceph |
Ceph (via Rook) | GA | 全栈 Ceph on K8s |
portworx |
Portworx | GA | 企业级方案 |
juicefs-csi-driver |
JuiceFS | 社区 | 高性能 POSIX 文件系统 |
常见问题
provisioner 写错了
kubectl get storageclass -o custom-columns=NAME:.metadata.name,PROVISIONER:.provisioner
provisioner 名称和 CSI 驱动注册的不匹配,PVC 会卡在 Pending。
CSI Node 服务没有在目标节点运行
CSI Node Service 通常以 DaemonSet 部署。如果某个节点没有对应 Pod,该节点上的 Pod 挂载卷会失败:
kubectl get pods -n kube-system -l app=csi-node -o wide
权限不足
kubectl get clusterrolebinding | grep csi
kubectl logs -n kube-system -l app=ebs-csi-controller -c ebs-plugin --tail=50
卷容量不匹配
某些存储后端不支持精确容量请求。请求 50Gi,后端可能创建 50.5Gi。CSI 规格允许实际容量略大于请求值。
排错清单:
# PVC 状态
kubectl describe pvc <name>
# PV 事件
kubectl describe pv <name>
# CSI 驱动日志
kubectl logs -n kube-system -l app=<csi-driver> -c <container>
# Kubelet 日志(节点上)
journalctl -u kubelet | grep -i csi
# 查看 CSI 驱动是否注册
kubectl get csidrivers.storage.k8s.io
第七章:对平台团队的影响
存储插件的架构变动也是平台工程治理的问题。
-
多租户隔离:CSI 的独立 RBAC 让不同租户可以用不同存储驱动,互不干扰。In-tree 时代所有插件共享 Kubelet 的权限。
-
升级灵活:CSI 驱动独立于 K8s 版本。升级 EBS CSI 驱动不需要升级整个集群。
-
可观测性:CSI 驱动作为独立进程,可以暴露自己的 Prometheus metrics,独立做日志采集。In-tree 的监控数据混在 Kubelet 日志里。
-
供应商锁定降低:CSI 是跨平台标准。如果从 K8s 迁移到其他编排平台,CSI 驱动可以复用。
-
开发者体验:平台团队封装 StorageClass,业务团队只要声明需要多少存储,不需要管底层是 EBS、Ceph 还是 NFS。
回顾
Kubernetes 存储插件走了这么一条路:
- In-tree:所有驱动写进核心仓库,快但臃肿
- FlexVolume:驱动拆到节点上,解决了独立开发但实现方式粗糙
- CSI:定义标准 gRPC 接口,驱动作为独立服务运行
In-tree 时期,K8s 核心团队试图覆盖所有存储场景。FlexVolume 先跑起来再说。CSI 则划清了边界,让生态在标准接口上各自发展。
现在用 kubectl create pvc 的时候,背后可能是 AWS、Ceph、Longhorn 或任何存储厂商的 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
更多推荐


所有评论(0)