引言:Kubernetes 存储的演进之路
在 Kubernetes 的早期版本中,存储插件的集成方式非常原始——所有云厂商的存储驱动代码都硬编码在 Kubernetes 主仓库中,以一个庞大的 “in-tree” 插件列表存在。随着 AWS、GCP、Azure、vSphere 等云厂商的增多,这种模式带来了严重的维护灾难:每个存储驱动的 Bug 修复都需要等待 Kubernetes 发版,第三方存储厂商如果想接入 K8s,就必须向 Kubernetes 主仓库提交 PR,经过漫长的审查周期才能被合并。
这种紧耦合架构显然无法适应云原生生态的爆炸式增长。于是,Container Storage Interface(CSI) 作为行业标准应运而生。CSI 是一个统一的存储接口规范,让存储厂商可以编写完全独立于 Kubernetes 的驱动,以 DaemonSet / Deployment 的形式部署在集群中,Kubernetes 只需要通过 gRPC 调用即可与这些驱动交互。自 Kubernetes v1.13 起 CSI 进入 GA(正式可用)阶段,v1.23 起移除了所有 in-tree 存储驱动的代码路径,CSI 成为 K8s 中唯一推荐的存储扩展方式。
本文将深入剖析 CSI 的架构设计、核心组件、工作流程,并手把手教你如何部署 CSI 驱动、排查常见问题,最后用一个极简的自定义 CSI 驱动示例,帮你彻底理解这个”存储界的 OCI 标准”。

CSI 架构全景:三个核心组件
CSI 定义了一套标准的 gRPC 接口,整个实现被拆分为三个独立的 Sidecar 容器(通常运行在同一个 Pod 中),职责清晰分离:
| 组件 | 运行位置 | 核心职责 | 实现的 CSI 接口 | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|
| CSI Controller | Controller(Deployment) | 卷的创建/删除/快照/克隆等控制面操作 |
,
,
,
|
||||||||
| CSI Node | Node(DaemonSet) | 卷的挂载/卸载/格式化、设备发现 |
,
,
|
||||||||
| CSI Identity | Controller + Node 均有 | 插件能力声明、探针健康检查 |
,
|
CSI Controller(控制面组件)
CSI Controller 以 Deployment 的形式在集群中运行,副本数通常为 1 或者 2(主备模式)。它负责处理所有”不依赖于具体节点”的存储操作:创建卷、删除卷、创建快照、扩容卷大小等。Kubernetes 中的
1 | PersistentVolumeClaim |
触发卷创建时,由
1 | external-provisioner |
这个 Sidecar 容器监听到 PVC 事件,然后通过 gRPC 调用 CSI Controller 的
1 | CreateVolume |
接口,最终在底层存储系统(比如 Ceph RBD、NFS 服务器、云厂商的 EBS 卷)上真正分配空间。
关键接口解析(gRPC proto 定义):
1
2
3
4
5
6
7
8
9
10
11
12 service Controller {
rpc CreateVolume(CreateVolumeRequest) returns (CreateVolumeResponse);
rpc DeleteVolume(DeleteVolumeRequest) returns (DeleteVolumeResponse);
rpc ControllerPublishVolume(ControllerPublishVolumeRequest)
returns (ControllerPublishVolumeResponse);
rpc ControllerUnpublishVolume(ControllerUnpublishVolumeRequest)
returns (ControllerUnpublishVolumeResponse);
rpc CreateSnapshot(CreateSnapshotRequest) returns (CreateSnapshotResponse);
rpc DeleteSnapshot(DeleteSnapshotRequest) returns (DeleteSnapshotResponse);
rpc ControllerExpandVolume(ControllerExpandVolumeRequest)
returns (ControllerExpandVolumeResponse);
}
CSI Node(节点面组件)
CSI Node 以 DaemonSet 的形式在每个工作节点上运行。它负责实际的数据面操作:将卷格式化、挂载到 Pod 可以访问的路径。
当 Kubernetes 调度器决定将某个 Pod 调度到 Node A 上,并且该 Pod 声明了一个 CSI 卷时,
1 | external-attacher |
Sidecar 会先调用 Controller 的
1 | ControllerPublishVolume |
(对于块存储而言,相当于将云硬盘挂载到宿主机),然后
1 | node-driver-registrar |
会通知 kubelet 调用 CSI Node 的
1 | NodePublishVolume |
,完成最终的 mount 操作。
1
2
3
4
5
6
7
8
9
10
11 service Node {
rpc NodePublishVolume(NodePublishVolumeRequest)
returns (NodePublishVolumeResponse);
rpc NodeUnpublishVolume(NodeUnpublishVolumeRequest)
returns (NodeUnpublishVolumeResponse);
rpc NodeGetVolumeStats(NodeGetVolumeStatsRequest)
returns (NodeGetVolumeStatsResponse);
rpc NodeExpandVolume(NodeExpandVolumeRequest)
returns (NodeExpandVolumeResponse);
rpc NodeGetInfo(NodeGetInfoRequest) returns (NodeGetInfoResponse);
}
Kubernetes CSI Sidecar 容器
CSI 驱动本身通常只实现了上述三个 gRPC 服务。为了将 CSI 驱动与 Kubernetes 的资源体系对接,社区提供了几个标准的 Sidecar 容器,与 CSI 驱动容器部署在同一个 Pod 中:
- external-provisioner:监听 PVC 对象,调用 CSI Controller 的
1CreateVolume
/
1DeleteVolume来动态创建和删除卷,并自动创建对应的 PV 对象。
- external-attacher:监听
1VolumeAttachment
对象,调用 CSI Controller 的
1ControllerPublishVolume/
1ControllerUnpublishVolume完成卷与节点的绑定/解绑。
- external-resizer:监听 PVC 大小变更请求,调用 CSI Controller 的
1ControllerExpandVolume
实现在线扩容。
- external-snapshotter:监听
1VolumeSnapshot
/
1VolumeSnapshotContent对象,调用 CSI Controller 的
1CreateSnapshot/
1DeleteSnapshot。
- node-driver-registrar:在节点上向 kubelet 注册 CSI 驱动的 Unix Socket 路径,并上报驱动信息。
- livenessprobe:定期对 CSI 驱动执行
1Probe()
gRPC 调用,确保驱动健康,配置 Pod 的 liveness probe。
CSI 卷的完整生命周期
理解了组件之后,让我们追踪一个 PVC 从创建到挂载到 Pod 使用的完整流程:
- 用户创建 PVC:用户提交
1PersistentVolumeClaim
YAML,指定
1storageClassName指向某个 CSI 驱动的 StorageClass。
- external-provisioner 感知:Sidecar 监听到 PVC 事件,读取其 StorageClass 中的
1provisioner
字段(如
1rbd.csi.ceph.com),确认由哪个 CSI 驱动处理。
- 调用 CreateVolume:external-provisioner 通过 Unix Socket 调用 CSI Controller 的
1CreateVolume
,传入容量、参数(如
1pool、
1fsType)等。
- 创建 PV:底层存储卷创建成功后,external-provisioner 自动创建一个
1PersistentVolume
对象与 PVC 绑定。
- Pod 调度 & 创建 VolumeAttachment:当使用了该 PVC 的 Pod 被调度到某个节点上时,K8s 创建
1VolumeAttachment
对象,表明该卷需要被挂载到特定节点。
- external-attacher 调用 ControllerPublishVolume:Attacher Sidecar 调用 CSI Controller,将底层卷挂载到目标节点(对块存储而言,相当于将 EBS 卷 attach 到 EC2 实例)。
- kubelet 调用 NodePublishVolume:节点上的 kubelet(通过 Unix Socket)调用 CSI Node 的
1NodePublishVolume
,完成格式化(如果需要)和最终的 mount,将卷暴露到宿主机的 staging 路径。
- CSI Node 执行 mount:驱动内部调用
1mount
、
1mkfs等系统命令,完成后 kubelet 将卷绑定到 Pod 的容器文件系统中。
至此,Pod 中的应用就可以通过挂载路径正常读写持久化数据了。整个过程对用户完全透明——用户只看到了 PVC 和 Pod,中间所有的 orchestration 由 Sidecar 们自动完成。
实战部署:用 Ceph CSI(RBD)搭建持久化存储
理论讲再多也不如动手部署一次。下面我们以 Ceph RBD 为例,完整演示如何部署 CSI 驱动并使用动态供给创建 PV。
前提条件
你需要一个运行中的 Ceph 集群(至少一个 mon 和一个 OSD),以及
1 | ceph-csi |
的 Helm Chart:
1
2
3
4
5
6
7 # 添加 Helm 仓库
helm repo add ceph-csi https://ceph.github.io/csi-charts
helm repo update
# 获取 Ceph 集群的 mon 地址和 keyring
ceph mon dump | grep "mon." | awk '{print $1}' | cut -d' ' -f1
ceph auth get-key client.admin | base64
创建一个 Ceph Secret,用于 CSI 驱动访问 Ceph 集群:
1
2
3
4
5
6
7
8 apiVersion: v1
kind: Secret
metadata:
name: ceph-csi-secret
namespace: ceph-csi
stringData:
userID: admin
userKey: QVFDVnd...base64-encoded-key=
部署 CSI 驱动
1
2
3
4
5
6
7 helm install ceph-csi-rbd ceph-csi/ceph-csi-rbd \
--namespace ceph-csi \
--create-namespace \
--set csiConfig[0].clusterID=ceph-cluster-1 \
--set csiConfig[0].monitors="192.168.1.10:6789,192.168.1.11:6789" \
--set secret.create=false \
--set secret.name=ceph-csi-secret
验证 CSI 驱动 Pod 是否正常运行:
1
2
3
4
5 kubectl -n ceph-csi get pods
NAME READY STATUS RESTARTS
csi-rbdplugin-controller-749f6f547-pq8cr 5/5 Running 0
csi-rbdplugin-8rt6d 3/3 Running 0
csi-rbdplugin-hq45x 3/3 Running 0
这里 Controller Pod 有 5 个容器(CSI 驱动 + 4 个 Sidecar),Node Pod 有 3 个容器(CSI 驱动 + node-driver-registrar + livenessprobe)。
创建 StorageClass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: csi-rbd-sc
provisioner: rbd.csi.ceph.com
parameters:
clusterID: ceph-cluster-1
pool: kubernetes
imageFeatures: layering
csi.storage.k8s.io/provisioner-secret-name: ceph-csi-secret
csi.storage.k8s.io/controller-expand-secret-name: ceph-csi-secret
csi.storage.k8s.io/node-stage-secret-name: ceph-csi-secret
reclaimPolicy: Delete
allowVolumeExpansion: true
volumeBindingMode: Immediate
测试动态供给
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: csi-rbd-pvc
spec:
accessModes:
- ReadWriteOnce
storageClassName: csi-rbd-sc
resources:
requests:
storage: 10Gi
---
apiVersion: v1
kind: Pod
metadata:
name: csi-rbd-test
spec:
containers:
- name: app
image: busybox
command: ["sleep", "3600"]
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: csi-rbd-pvc
创建后验证:
1
2
3
4
5
6
7 kubectl get pv
NAME CAPACITY ACCESS MODES STATUS CLAIM
pvc-8a3b2c1d-... 10Gi RWO Bound default/csi-rbd-pvc
kubectl exec csi-rbd-test -- df -h /data
Filesystem Size Used Avail Use% Mounted on
/dev/rbd0 9.8G 44M 9.8G 1% /data
CSI 驱动开发速览:构建一个极简的 Demo 驱动
为了更好地理解 CSI 的内部机制,我们来动手写一个最简单的 CSI 驱动——”Memory CSI Driver”,它不真正管理存储,只是在内存中模拟卷的创建和挂载,但完整实现了 Identity、Controller 和 Node 三个服务。
项目结构
1
2
3
4
5
6
7
8
9
10 memory-csi/
├── cmd/
│ └── main.go # 入口,启动 gRPC Server
├── pkg/
│ ├── identity.go # Identity 服务
│ ├── controller.go # Controller 服务
│ └── node.go # Node 服务
├── deploy/
│ └── csi-memory.yaml # Kubernetes 部署清单
└── go.mod / go.sum
核心代码示例:Controller 的 CreateVolume
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 // pkg/controller.go
func (cs *controllerServer) CreateVolume(
ctx context.Context,
req *csi.CreateVolumeRequest,
) (*csi.CreateVolumeResponse, error) {
name := req.GetName()
caps := req.GetVolumeCapabilities()
reqBytes := req.GetCapacityRange().GetRequiredBytes()
log.Printf("CreateVolume called: name=%s, size=%d bytes", name, reqBytes)
// 在实际驱动中,这里会调用云 API 或存储后端
// 在 Demo 中,我们只是在内存中记录一下
volume := &csi.Volume{
VolumeId: uuid.New().String(),
CapacityBytes: reqBytes,
VolumeContext: map[string]string{
"driver": "memory-csi",
"name": name,
},
}
return &csi.CreateVolumeResponse{Volume: volume}, nil
}
核心代码示例:Node 的 NodePublishVolume
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 // pkg/node.go
func (ns *nodeServer) NodePublishVolume(
ctx context.Context,
req *csi.NodePublishVolumeRequest,
) (*csi.NodePublishVolumeResponse, error) {
targetPath := req.GetTargetPath()
volumeID := req.GetVolumeId()
log.Printf("NodePublishVolume: volume=%s, target=%s", volumeID, targetPath)
// 保证目标目录存在
if err := os.MkdirAll(targetPath, 0755); err != nil {
return nil, status.Errorf(codes.Internal,
"mkdir %s failed: %v", targetPath, err)
}
// 挂载 tmpfs(实际生产驱动这里会 mount 真实的存储设备)
if err := syscall.Mount("tmpfs", targetPath, "tmpfs", 0, ""); err != nil {
return nil, status.Errorf(codes.Internal,
"mount tmpfs at %s failed: %v", targetPath, err)
}
log.Printf("Successfully mounted volume %s at %s", volumeID, targetPath)
return &csi.NodePublishVolumeResponse{}, nil
}
完整的 Demo 驱动需要处理
1 | NodeGetCapabilities |
、
1 | ControllerGetCapabilities |
等能力声明方法,以及错误处理和资源回收逻辑。你可以在 csi-test 项目中找到更多的开发参考和测试框架。
常见问题与排障指南
1. PVC 一直处于 Pending 状态
这是 CSI 使用中最常见的问题。排查步骤:
1
2
3
4
5
6
7
8 # 查看 PVC 事件
kubectl describe pvc my-pvc
# 检查 CSI Controller Pod 日志
kubectl -n ceph-csi logs -l app=csi-rbdplugin-controller -c csi-provisioner
# 检查 CSI 驱动自身的 Controller 日志
kubectl -n ceph-csi logs -l app=csi-rbdplugin-controller -c csi-rbdplugin
常见原因包括:
- StorageClass 中的
1provisioner
名称写错,与 CSI 驱动的注册名称不匹配
- CSI 驱动的 Secret 配置错误,无法通过存储后端的认证
- Ceph pool 不存在或已满(针对 Ceph 场景)
- CSI Controller 未正确启动或被 OOMKill
2. Pod 创建失败:Failed to mount volume
挂载阶段的错误通常定位到 Node 侧的日志:
1
2
3
4
5
6
7
8
9
10 # 查看 kubelet 日志(需要 SSH 到工作节点)
journalctl -u kubelet -n 50 | grep -i csi
# 查看 CSI Node 插件日志
kubectl -n ceph-csi logs -l app=csi-rbdplugin -c csi-rbdplugin
# 检查节点是否有需要的工具(如 mkfs.ext4, mount, rbd)
which mount
which mkfs.ext4
which rbd
一个容易被忽略的点:CSI Node 驱动的容器镜像中可能不包含
1 | mkfs.ext4 |
、
1 | xfs_repair |
等文件系统工具。你需要在 DaemonSet 中挂载宿主机的
1 | /sbin/mkfs.* |
或者在驱动镜像中预装这些工具。
3. 在线扩容不生效
要支持在线扩容,需要同时满足三个条件:
- StorageClass 中设置了
1allowVolumeExpansion: true
- CSI 驱动实现了
1ControllerExpandVolume
和
1NodeExpandVolume接口
- 部署了
1external-resizer
Sidecar
1
2
3 # 检查 CSI 驱动是否声明了 EXPAND_VOLUME 能力
kubectl -n ceph-csi exec -it deploy/csi-rbdplugin-controller \
-c csi-rbdplugin -- csc --endpoint /csi/csi.sock controller-get-capabilities
CSI 最佳实践与性能调优
资源限制与调优
CSI 驱动的资源规划直接影响存储操作的并发性能和稳定性:
| 组件 | 推荐 CPU 限制 | 推荐内存限制 | 关键调优参数 | ||
|---|---|---|---|---|---|
| CSI Controller | 200m – 500m | 256Mi – 512Mi |
(默认 10) |
||
| CSI Node | 100m – 300m | 128Mi – 256Mi | 影响 Pod 启动速度 | ||
| external-provisioner | 100m – 200m | 128Mi |
重试间隔 |
||
| external-attacher | 100m – 200m | 128Mi |
并行数 |
安全加固建议
- 最小权限原则:CSI 驱动的 ServiceAccount 只需要对 PV、PVC、VolumeAttachment 等资源的必要操作权限,不要使用
1cluster-admin
。
- Secret 隔离:存储后端凭据必须单独放在 CSI 驱动所在的 Namespace 中,不要混入业务 Namespace。
- gRPC Socket 权限:CSI 驱动的 Unix Socket 文件应设置
10600
权限,确保只有 kubelet 和同 Pod 的 Sidecar 可以访问。
- 加密传输:对于敏感数据场景,建议启用存储后端(如 Ceph 的
1rbd
加密或云厂商的 EBS 加密)的静态数据加密功能。
总结
CSI 的出现彻底改变了 Kubernetes 存储生态的面貌。它将存储驱动的开发从 Kubernetes 主仓库中解耦出来,让每一个存储厂商都可以独立迭代自己的驱动版本。作为 K8s 用户和运维人员,理解 CSI 的架构和工作流程至关重要——当你遇到 PVC 挂载失败、扩容异常或性能瓶颈时,知道应该查看 Controller 还是 Node 的日志、理解每个 Sidecar 的角色,能帮你将排障时间从小时级缩短到分钟级。
如果你正在考虑为自建存储系统接入 Kubernetes,推荐从社区已有的 CSI 驱动开始(Ceph RBD/NFS、Longhorn、Rook、OpenEBS 等都有成熟的实现),只有在特殊需求确实无法被现有驱动满足时,才需要自行开发 CSI 驱动。CSI Spec 目前版本为 v1.9+,接口已经非常稳定,官方提供了完整的 csi-test 测试套件来保证实现的合规性。
最后,强烈建议在生产环境中启用 CSI 的 Topology-Aware Provisioning(拓扑感知调度),让你的 PV 创建在离 Pod 最近的可用区,减少跨 AZ 的网络延迟和流量费用。通过配置 StorageClass 的
1 | volumeBindingMode: WaitForFirstConsumer |
,可以让卷的创建延迟到 Pod 被调度之后再做,确保存储位置与计算位置一致。
汤不热吧