csi 是一个标准的容器存储接口,规定了如何实现一个容器的存储接口,CSI 本身的定义是基于 gRPC 的,所以有一套样例库可以使用,这里分析一下 kuberntes 实现 csi 的方式,为了兼容 CSI kubernete 其实搞得挺绕的,目前这个 CSI 还是定制中包括后期的 Snapshot 的接口怎么设计等等还在讨论中。kubernetes CSI 主要基于几个外部组件和内部功能的一些改动。
CSI-Driver
这里规定了 CSI 的标准,定义了三个 Service,也就是 RPC 的集合,但是没规定怎么写,目前看到的实现都是把这三个 service 都写在一起,比较方便,然后部署的时候有些区别将就可以。
1 | service Identity { |
比如说 GetPluginInfo
就是用来获取 driver 的 name 等信息的,NodePublishVolume
大部分情况下就是在节点上挂载文件系统,CreateVolume
这个如果对应的是 ebs 这种块存储可能就是在 API 里面建一个 ebs,如果对应的是 glusterfs 这种文件系统存储可能就是建一个 volume,然后 ControllerPublishVolume
对应 ebs 就是把 ebs 和 instance 绑定,然后调用节点的 NodePublishVolume
来挂载,如果是文件存储,可能就不需要 ``ControllerPublishVolume` 了,因为不需要绑定快设备到机器上,直接挂到网络接口就可以,这一套标准的目的一个是为了兼容现有的存储方案,一个是为了让一些私有的 provider 能够比较容易的实现一套方案,而不需要做过多的迁移,甚至厂商都不需要开源代码,如果是要实现 in-tree 的存储代码肯定是要开源的,因为 kubernetes 是开源的。
device-driver-registrar
kubernetes 实现 csi 的兼容,首先需要一个外部组件 devide-driver-registrar,初始化的时候通过 csi-sock 的 RPC 获取 driver name 和 node id。
主要功能给 node 打上下面类似的 annotations,dirver 对应的是 csi driver 的名字,name 对应的是 driver 的 NodeId 基本上就是 k8s 的 node name。这样可以让 ControllerPublishVolume
调用能够获取 nodeid 到 storage nodeid 的映射,理论上一样的就可以感觉。
1 | csi.volume.kubernetes.io/nodeid: "{ "driver1": "name1", "driver2": "name2" } |
他有两个模式,一个模式是自己给 node 打上这个 annotation,并且在退出的时候把这个 annotation 去掉。
另一个模式是交给 kubelet 的 pluginswatcher 来管理, kubelet 自己会根据 device-driver-registrar 提供的 unix domain socket 然后调用 gRPC 从 registrar 获取 NodeId 和 DriverName 自己把 annotation 打上。
搜索这条路径下的 socket/var/lib/kubelet/plugins/[SanitizedCSIDriverName]/csi.sock
,然后就可以自动连接 registrar 拿到 NodeId 和 DriverName。
所以 device-driver-registar 主要是注册 Node annotation 的。
external-attacher
监听 VolumeAttachments 和 PersistentVolumes 两个对象,这是和 kube-controller-manager 之间的桥梁。
实现中最后会调用 SyncNewOrUpdatedVolumeAttachment
来同步,调用 csi dirver 的 Attach 函数。
in-tree 的 attach/detach-controller
在 CSI 中扮演的角色是创建 VolumeAttachment,然后等待他的 VolumeAttachment 的 attached 的状态。
attach-controller 会创建 VolumeAttatchment.Spec.Attacher
指向的是 external-attacher
external-provisoner
Static Volume
和 Dynamic Volume
的区别是,有一个 PersistentVolumeClaim 这个会根据 claim 自动分配 PersistentVolume,不然就要自己手动创建,然后 pod 要指定这个手动创建的 volume。
external-provisoner 就是提供支持 PersistentVolumeClaim 的,一般的 provisioner 要实现 Provision 和 Delete 的接口。主要是根据 PVC 创建 PV,这是 Provisioner 的接口的定义了,不是 CSI spec 里的,这里顺带介绍一下。
external-provisoner 看到 pvc 调用 driver
的 CreateVolume,完成以后就会创建 PersistenVolume,并且绑定到 pvc。
kubelet volume manager
kubelet 有一个 volume manager 来管理 volume 的 mount/attach 操作。
desiredStateOfWorld
是从 podManager 同步的理想状态。
actualStateOfWorld
是目前 kubelet 的上运行的 pod 的状态。
每次 volume manager 需要把 actualStateOfWorld
中 volume 的状态同步到 desired 指定的状态。
volume Manager 有两个 goroutine 一个是同步状态,一个 reconciler.reconcile
rc.operationExecutor.MountVolume 会执行 MountVolume 的操作。
-> oe.operationGenerator.GenerateMountVolumeFunc
-> 首先根据 og.volumePluginMgr.FindPluginBySpec 找到对应的 VolumePlugin
-> 然后调用 volumePlugin.NewMounter
-> 然后拿到 og.volumePluginMgr.FindAttachablePluginBySpec attachableplugin
-> volumeMounter.SetUp(fsGroup) 做 mount
volume plugin
csi volume plugin 是一个 in-tree volume,以后应该会逐步迁移到都使用 csi,而不会再有 in-tree volume plugin 了。
1 | func (c *csiMountMgr) SetUp(fsGroup *int64) error { |
csi 的 mounter 调用了 NodePublish 函数。stagingTargetPath 和 targetPath 都是自动生成的。
SetUp
/TearDown
的调用会执行 in-tree CSI plugin 的接口(这又是 in-tree volume plugin 的定义,确实挺绕的),对应的是 NodePublishVolume
和 NodeUnpublishVolume
,这个会通过 unix domain socket 直接调用 csi driver。
总结一个简单的具体流程
首先管理员创建 StorageClass 指向 external-provisioner,然后用户创建指向这个 StorageClass 的 pvc,然后 kube-controller-manager 里的 persistent volume manager 会把这个 pvc 打上 volume.beta.kubernetes.io/storage-provisioner
的 annotation。
externla-provisioner 看到这个 pvc 带有自己的 annotation 以后,拿到 StorageClass 提供的一些参数,并且根据 StorageClass 和 pvc 调用 CSI driver 的 CreateVolume
,创建成功以后创建 pv,并且把这个 pv 绑定到对应的 pvc。
然后 kube-controller-manager 看到有 pod 含有对应的 pvc,就用调用 in-tree 的 CSI plugin 的 attach 方法。
in-tree 的 CSI plugin 实际上会创建一个 VolumeAttachment 的 object,等待这个 VolumeAttachment 被完成。
external-controller 看到 VolumeAttachment,开始 attach 一个 volume,实际上调用 CSI driver 的 ControllerPublish
,成功以后更新 VoluemAttachment
以后就知道这个 Volume Attach 成功了,然后让 attach/detach-controller (kube-controller-manager) 知道这个 attach 完成。
接下来就到 kubelet 了,kubelet 看到 volume in pod 以后就会调用 in-tree 的 csi plugin WaitForAttach,然后等待 Attach 成功,之后就会调用 daemonset 里面的 csi driver 的 NodePublishVolume
做挂载操作。
整体的流程是这样的,需要反复多看几遍 kubernetes-csi 的 document,加深理解。