CSI 即 Container Storage Interface(容器存储接口),向 Kubernetes 这样的 Container Orchestration Systems(容器编排系统)上的容器化工作负载暴露裸存储块或文件系统。第三方 SP(存储提供方)遵循 CSI 编写和部署插件为 Kubernetes 集群提供持久化的存储系统,但无需接触核心 Kubernetes 代码。
几个经典的 CSI 项目:
专业术语
术语 | 定义 |
---|---|
in-tree | 代码在核心 Kubernetes 仓库中,将被编译、链接内置在核心的 Kubernetes 二进制可执行文件中 |
out-of-tree | 代码在其他仓库中 |
CSI Volume Plugin | in-tree 的 CSI 插件作为适配器,使得 out-of-tree 的第三方 CSI 驱动能够被 Kubernetes 使用 |
CSI Volume Driver | out-of-tree 的兼容 CSI 的插件实现,通过 CSI 插件被 Kubernetes 使用 |
因为 Kubernetes 已经成为了容器编排系统的事实标准,所以下文中的 Kubernetes 就是容器编排系统,反之亦然。
CSI 规范的背景
因为 Volume Plugin(数据卷插件)是要编译进 Kubernetes 二进制执行文件中的,添加一个新的存储系统需要修改核心代码仓库中的代码,非常麻烦:
- 因为处于同一个代码仓库中,数据卷插件的开发与 Kubernetes 紧密耦合。
- Kubernetes 开发者和社区得负责测试和维护所有数据卷插件,这是相当大的额外工作量。
- 因为在同一个二进制中,数据卷插件的 bug 可能会造成整个 Kubernetes 组件崩溃。
- 数据卷插件有 kubernetes 组件(kubelet 和 kube-controller-manager)所有的权限。
- 插件被迫同 kubernetes 开源,不能选择只发布二进制文件。
- 数据卷插件通常需要许多额外挂载和文件系统工具,可能要在宿主机操作系统上另外安装。
要解决上述痛点:
- 数据卷插件代码从 Kubernetes 核心代码仓库分离出来,在各自的代码仓库中维护。
- 插件以独立进程的形式运行,不再内置于 Kubernetes 组件二进制执行文件中。
- 将额外的挂载和文件系统工具构建进容器镜像中,插件以容器形式由 Kubernetes 托管。
数据卷插件下面改称为存储插件。
进而又引出了一个大问题——Kubernetes 组件与存储插件如何通信:
- 因为 Kubernetes 组件与插件都是独立执行的进程,有可能运行在不同的宿主机上,通信毫无疑问要走网络。
- Kubernetes 组件与插件往往隐藏在系统的背后,不面向消费者,用 gRPC 协议(字节码传输)比传统的 HTTP 更高效;而且有丰富的代码生成工具,开发者只需关注协议本身。
- Kubernetes 组件与存储系统解耦,无需知道正在使用的存储系统;无论与哪个存储插件通信,只要使用统一的接口发送创建、使用磁盘请求,这个接口就是 CSI 规范(协议)。
所以 CSI 规范主要关注于 Kubernetes 和存储插件之间的协议。
CSI Volume Driver 存储插件架构
我们按存储插件的功能来看其架构:
- 在存储系统中创建/销毁存储卷
- attach/detach 存储卷至集群节点
- mount/unmount 存储卷至操作系统
两类功能对应存储插件的两个不同组件,插件容器全都由 Kubernetes 托管:
- controller 插件:关注于存储系统 API 调用
- node 插件:关注于主机操作系统使用存储卷;有些存储系统也会由 node 插件来插拔存储卷
Node 插件
由于 Kubernetes 集群中的所有节点都有可能使用存储系统的存储卷,所以每个节点都要部署一个存储插件守护进程,kubelet 只与同节点上存储插件通过 CSI 协议通信;同宿主机上的网络通信最好使用 Unix Domain Socket。
因此 CSI 存储插件要在宿主机上创建 /var/lib/kubelet/plugins/[SanitizedCSIDriverName]/csi.sock 文件。
$ kubectl get CSIDriver
kubectl get CSIDriver
NAME ATTACHREQUIRED PODINFOONMOUNT STORAGECAPACITY TOKENREQUESTS REQUIRESREPUBLISH MODES AGE
local.csi.openebs.io false true true <unset> false Persistent 64d
rook-ceph.cephfs.csi.ceph.com true false false <unset> false Persistent 64d
rook-ceph.rbd.csi.ceph.com true false false <unset> false Persistent 64d
$ tree /var/lib/kubelet/plugins/
/var/lib/kubelet/plugins/
├── kubernetes.io
│ └── csi
│ ├── pv
│ └── volumeDevices
│ ├── publish
│ └── staging
├── lvm-localpv
│ └── csi.sock
├── rook-ceph.cephfs.csi.ceph.com
│ └── csi.sock
└── rook-ceph.rbd.csi.ceph.com
└── csi.sock
该环境上有三种 CSI Volume Driver,每种插件都会创建其 usock 文件。
因此每一种 CSI 存储插件,都要在集群中的每个节点上(跟随 kubelet)部署一个存储插件守护进程(DaemonSet):
$ kubectl get ds -n rook-ceph
NAME DESIRED CURRENT READY UP-TO-DATE AVAILABLE NODE SELECTOR AGE
csi-cephfsplugin 3 3 3 3 3 <none> 65d
csi-rbdplugin 3 3 3 3 3 <none> 65d
$ kubectl get ds -n kube-system | grep openebs
openebs-lvm-node 3 3 3 3 3 <none> 64d
$ kubectl get po -n rook-ceph
NAME READY STATUS RESTARTS AGE
csi-cephfsplugin-bnhjd 2/2 Running 10 (3d3h ago) 65d
csi-cephfsplugin-q4jbz 2/2 Running 6 (3d3h ago) 65d
csi-cephfsplugin-qgq5q 2/2 Running 8 (47d ago) 65d
csi-rbdplugin-4pbx9 2/2 Running 10 (3d3h ago) 65d
csi-rbdplugin-4x2j8 2/2 Running 6 (3d3h ago) 65d
csi-rbdplugin-j8p4p 2/2 Running 8 (47d ago) 65d
$ kubectl get po -n kube-system | grep openebs
kubectl get po -n kube-system | grep openebs
openebs-lvm-node-2dntx 2/2 Running 8 (3d3h ago) 64d
openebs-lvm-node-8j4cg 2/2 Running 0 12d
openebs-lvm-node-rbmpw 2/2 Running 2 (3d3h ago) 52d
我们注意到每个存储插件 Pod 都有两个容器,这是因为除存储插件本身外,Kubernetes 还提供了一个 sidecar 容器用于帮助将存储插件的 usock 注册至 kubelet:
$ ps -ef | grep csi-node-driver-registrar
root 4262 3699 0 2022 ? 00:01:44 /csi-node-driver-registrar --v=0 --csi-address=/csi/csi.sock --kubelet-registration-path=/var/lib/kubelet/plugins/rook-ceph.rbd.csi.ceph.com/csi.sock
root 5961 3489 0 2022 ? 00:03:24 /csi-node-driver-registrar --v=0 --csi-address=/csi/csi.sock --kubelet-registration-path=/var/lib/kubelet/plugins/rook-ceph.cephfs.csi.ceph.com/csi.sock
root 571470 571420 0 Jan25 ? 00:00:58 /csi-node-driver-registrar --v=5 --csi-address=/plugin/csi.sock --kubelet-registration-path=/var/lib/kubelet/plugins/lvm-localpv/csi.sock
kubelet 在存储卷插到主机上后容器启动前,向同节点上的存储插件发送 NodePublishVolume
CSI 请求,由 Node 存储插件将存储卷挂载至指定挂载点。有些存储系统的 Node 存储插件还会实现收到 NodeStageVolume
CSI 请求后将存储卷插到主机上(Ceph RBD CSI)。
Controller 插件
controller 插件负责创建(provision)和删除存储卷还有 attach/detach 存储卷至主机。
因为 CSI Volume Driver 的代码可能不安全,一般不运行在控制面节点上(主节点),也就意味着主节点上的 Kube controller manager 无法通过 Unix Domain Socket 与 controller 存储插件通信。同 node 存储插件一样,Kubernetes 还提供了几个 sidecar 容器来做中间人,而非 Kube controller manager 直接去调用 CSI 接口。
$ kubectl get deployment -n rook-ceph
NAME READY UP-TO-DATE AVAILABLE AGE
csi-cephfsplugin-provisioner 2/2 2 2 65d
csi-rbdplugin-provisioner 2/2 2 2 65d
$ kubectl get po -n rook-ceph | grep provisioner
csi-cephfsplugin-provisioner-77cd78d94-n5hpr 5/5 Running 0 3d15h
csi-cephfsplugin-provisioner-77cd78d94-rwj54 5/5 Running 8 (3d15h ago) 48d
csi-rbdplugin-provisioner-64b876d6fd-2nmcz 5/5 Running 6 (3d15h ago) 48d
csi-rbdplugin-provisioner-64b876d6fd-kfvp6 5/5 Running 0 3d15h
-
创建/删除存储卷
$ kubectl get deployment csi-rbdplugin-provisioner -n rook-ceph -o=jsonpath='{.spec.template.spec.containers}' | jq -r '.[] | select(.name=="csi-provisioner")' { "args": [ "--csi-address=$(ADDRESS)", "--v=0", "--timeout=2m30s", "--retry-interval-start=500ms", "--leader-election=true", "--leader-election-namespace=rook-ceph", "--leader-election-lease-duration=137s", "--leader-election-renew-deadline=107s", "--leader-election-retry-period=26s", "--default-fstype=ext4", "--extra-create-metadata=true" ], "env": [ { "name": "ADDRESS", "value": "unix:///csi/csi-provisioner.sock" } ], "image": "registry-1.ict-mec.net:18443/mececs/csi-provisioner:release-3.1-1e3344f23-20220913113748", "imagePullPolicy": "IfNotPresent", "name": "csi-provisioner", "resources": {}, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File", "volumeMounts": [ { "mountPath": "/csi", "name": "socket-dir" } ] } $ kubectl get deployment csi-cephfsplugin-provisioner -n rook-ceph -o=jsonpath='{.spec.template.spec.containers}' | jq -r '.[] | select(.name=="csi-provisioner")' { "args": [ "--csi-address=$(ADDRESS)", "--v=0", "--timeout=2m30s", "--retry-interval-start=500ms", "--leader-election=true", "--leader-election-namespace=rook-ceph", "--leader-election-lease-duration=137s", "--leader-election-renew-deadline=107s", "--leader-election-retry-period=26s", "--extra-create-metadata=true" ], "env": [ { "name": "ADDRESS", "value": "unix:///csi/csi-provisioner.sock" } ], "image": "registry-1.ict-mec.net:18443/mececs/csi-provisioner:release-3.1-1e3344f23-20220913113748", "imagePullPolicy": "IfNotPresent", "name": "csi-provisioner", "resources": {}, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File", "volumeMounts": [ { "mountPath": "/csi", "name": "socket-dir" } ] }
controller 存储插件同 Pod 中有一个 sidecar 容器叫 csi-provisioner。它是一个 external provisioner,由它发送
CreateVolume
CSI 请求至 controller 存储插件,去存储系统那创建一个新的存储卷。 -
Attach 和 Detach
$ kubectl get deployment csi-rbdplugin-provisioner -n rook-ceph -o=jsonpath='{.spec.template.spec.containers}' | jq -r '.[] | select(.name=="csi-attacher")' { "args": [ "--v=0", "--timeout=2m30s", "--csi-address=$(ADDRESS)", "--leader-election=true", "--leader-election-namespace=rook-ceph", "--leader-election-lease-duration=137s", "--leader-election-renew-deadline=107s", "--leader-election-retry-period=26s" ], "env": [ { "name": "ADDRESS", "value": "/csi/csi-provisioner.sock" } ], "image": "registry-1.ict-mec.net:18443/google_containers/csi-attacher:v3.4.0", "imagePullPolicy": "IfNotPresent", "name": "csi-attacher", "resources": {}, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File", "volumeMounts": [ { "mountPath": "/csi", "name": "socket-dir" } ] } $ kubectl get deployment csi-cephfsplugin-provisioner -n rook-ceph -o=jsonpath='{.spec.template.spec.containers}' | jq -r '.[] | select(.name=="csi-attacher")' { "args": [ "--v=0", "--csi-address=$(ADDRESS)", "--leader-election=true", "--timeout=2m30s", "--leader-election-namespace=rook-ceph", "--leader-election-lease-duration=137s", "--leader-election-renew-deadline=107s", "--leader-election-retry-period=26s" ], "env": [ { "name": "ADDRESS", "value": "/csi/csi-provisioner.sock" } ], "image": "registry-1.ict-mec.net:18443/google_containers/csi-attacher:v3.4.0", "imagePullPolicy": "IfNotPresent", "name": "csi-attacher", "resources": {}, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File", "volumeMounts": [ { "mountPath": "/csi", "name": "socket-dir" } ] }
controller 存储插件所在 Pod 中还有一个 sidecar 容器叫 csi-attacher。这是一个 external attacher,监视(watch)
VolumeAttachment
对象并发送ControllerPublishVolume
CSI 请求至 controller 存储插件,将新的存储卷插到指定节点。而VolumeAttachment
对象是由 kube controller manager 中的 attach/detach 控制器创建的。如果VolumeAttachment
对象被删除,attacher 则发送ControllerUnpublishVolume
CSI 请求至 controller 存储插件,存储卷会被拔出。至于怎么插拔的,Kubernetes 并不关心。并非所有存储系统都由接收到
ControllerPublishVolume
CSI 请求的 controller 插件去插拔磁盘,有些则是由 node 插件接收到NodeStageVolume
CSI 请求后去完成。

上图就是 Kubernetes 官方推荐的存储插件(CSI Driver)部署架构。
- StatefulSet 或 Deployment
- 存储供应商自己的 CSI controller 插件容器
- Kubernetes 提供的 sidecars:
- external-provisioner:provision/delete 操作
- external-attacher:attach/detach 操作
- external-resizer:扩容操作
- external-snapshotter:快照操作
- livenessprobe:可选
- DaemonSet
- 存储供应商自己的 CSI node 插件容器
- Kubernetes 提供的 sidecars:
- node-driver-registrar:将 node 插件的 usock 注册至 kubelet
- livenessprobe:可选
CSI 协议与 API
存储插件的部署架构确定后,剩下的就是 Kubernetes 组件(通常是存储插件的 sidecar 容器)与存储插件通信的协议了。CSI 规范定义了通信的格式(gRPC 协议)及方法签名。存储系统五花八门千差万别,但只要存储供应商编写存储插件自行实现 CSI API 中的方法,Kubernetes 集群就可以使用这些存储系统为容器提供持久化数据卷,无需关心对接的到底是哪种存储系统。
存储供应商必须提供 Node 和 Controller 两个插件,分别实现:
-
Node 服务:
service Node { rpc NodeStageVolume (NodeStageVolumeRequest) returns (NodeStageVolumeResponse) {} rpc NodeUnstageVolume (NodeUnstageVolumeRequest) returns (NodeUnstageVolumeResponse) {} rpc NodePublishVolume (NodePublishVolumeRequest) returns (NodePublishVolumeResponse) {} rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest) returns (NodeUnpublishVolumeResponse) {} rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest) returns (NodeGetVolumeStatsResponse) {} rpc NodeExpandVolume(NodeExpandVolumeRequest) returns (NodeExpandVolumeResponse) {} rpc NodeGetCapabilities (NodeGetCapabilitiesRequest) returns (NodeGetCapabilitiesResponse) {} rpc NodeGetInfo (NodeGetInfoRequest) returns (NodeGetInfoResponse) {} }
-
Controller 服务:
service Controller { rpc CreateVolume (CreateVolumeRequest) returns (CreateVolumeResponse) {} rpc DeleteVolume (DeleteVolumeRequest) returns (DeleteVolumeResponse) {} rpc ControllerPublishVolume (ControllerPublishVolumeRequest) returns (ControllerPublishVolumeResponse) {} rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest) returns (ControllerUnpublishVolumeResponse) {} rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest) returns (ValidateVolumeCapabilitiesResponse) {} rpc ListVolumes (ListVolumesRequest) returns (ListVolumesResponse) {} rpc GetCapacity (GetCapacityRequest) returns (GetCapacityResponse) {} rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest) returns (ControllerGetCapabilitiesResponse) {} rpc CreateSnapshot (CreateSnapshotRequest) returns (CreateSnapshotResponse) {} rpc DeleteSnapshot (DeleteSnapshotRequest) returns (DeleteSnapshotResponse) {} rpc ListSnapshots (ListSnapshotsRequest) returns (ListSnapshotsResponse) {} rpc ControllerExpandVolume (ControllerExpandVolumeRequest) returns (ControllerExpandVolumeResponse) {} rpc ControllerGetVolume (ControllerGetVolumeRequest) returns (ControllerGetVolumeResponse) { option (alpha_method) = true; } }
-
Identity 服务则两个插件都要实现:
service Identity { rpc GetPluginInfo(GetPluginInfoRequest) returns (GetPluginInfoResponse) {} rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) returns (GetPluginCapabilitiesResponse) {} rpc Probe (ProbeRequest) returns (ProbeResponse) {} }
我们有时排查 Kubernetes 集群存储相关问题,在查看了 controller 和 node 插件日志初步定位后,找到存储插件源码,搜索相关方法查看具体实现进一步排查。
参考文档
- https://github.com/container-storage-interface/spec/blob/master/spec.md
- https://kubernetes.io/docs/concepts/storage/volumes/#csi
- https://github.com/kubernetes/design-proposals-archive/blob/main/storage/container-storage-interface.md