无需二次开发,Cloud Alert 快速衔接您的IT事件和钉钉通知
889
2022-11-03
CSI系列二:如何用CSI为Kubernetes开发volume plugin
上一篇文章介绍了为什么要用CSI为Kubernetes开发volume plugin,这篇文章则是介绍如何用CSI来为Kubernetes开发volume plugin。
概述
对于开发者来说,用CSI为Kubernetes开发volume plugin,主要是要考虑两个问题。第一问题是要理解CSI的规范(spec),实现CSI spec中规定的接口。第二个问题是如何将自己的CSI plugin部署到Kubernetes cluster中,也就是如何与Kubernetes集成。
有童鞋可能会问,既然CSI是一个通用的标准,所有容器编排系统(包括Kubernetes)都支持。那么开发CSI plugin只需要关心CSI spec中定义的接口即可,为什么要关心Kubernetes呢?理想情况下确实只需要关心CSI spec,关键问题是CSI spec只是定义所有CO(Container Orchestration)都支持的接口,但是并没有定义CSI plugin与CO之间标准的集成解决方案。所以各个CO都有自己独特的集成解决方案,Kubernetes也不例外,作为开发者,必须要理解Kubernetes提供的集成方案。
CSI spec
在写作本文时,最新的CSI spec版本是1.1.0,在1.0.0的基础上增加了VolumeExpansion的功能。零君最近实现的一个CSI plugin就是基于1.0.0开发的,所以本文主要是基于1.0.0来说明。CSI spec 1.0.0的详细链接如下:
https://github.com/container-storage-interface/spec/blob/v1.0.0/spec.md
Kubernetes与CSI plugin之间通过gRPC通信。CSI spec主要定义了三组RPC接口:
Identity ServiceController ServiceNode Service
Identity Service定义了下面三个接口函数,如下所示。GetPluginInfo用于Kubernetes获取CSI plugin的信息,比如plugin的名称和版本。GetPluginCapabilities则是用于获取plugin所支持的能力,比如是否支持Controller service,是否支持topology。Probe则是用于health check,看plugin是否还在正常运行。
service Identity { rpc GetPluginInfo(GetPluginInfoRequest) returns (GetPluginInfoResponse) {} rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) returns (GetPluginCapabilitiesResponse) {} rpc Probe (ProbeRequest) returns (ProbeResponse) {}}
Controller Service定义了下面的接口函数。CSI Controller Plugin要实现这组接口,但是并不是必须要实现这里定义的所有接口函数。那么Kubernetes如何知道plugin实现了哪些,没实现哪些接口呢?这就是接口ControllerGetCapabilities要实现的功能,Kubernetes调用这个接口就可以获取Controller Plugin实现了哪些接口函数。
这里主要对CreateVolume和DeleteVolume以及ControllerPublishVolume和ControllerUnpublishVolume这几个重要的接口函数简要说明。CreateVolume用于动态创建Volume,也就是用户事先无需手动创建Volume,这点后文还会详细说明,先不用着急。DeleteVolume则是CreateVolume的逆向操作,也就是动态删除Volume。ControllerPublishVolume通常用于将一个Block设备动态attach到一个node上,而ControllerUnpublishVolume则是动态地做Detach操作。
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) {}}
Node Service定义了下面的接口函数。同样的,CSI Node Plugin要实现这组接口,但是不是必须要实现这里定义的所有接口函数。Kubernetes根据NodeGetCapabilities的返回结果就可以知道CSI Node Plugin实现了哪些接口。NodeStageVolume和NodePublishVolume都用于对volume进行format和mount操作,但是区别在于前者是将volume挂载(mount)到一个全局的staging路径,而后者是直接将volume挂载(mount)到pod内。当同一个node上运行的多个pod需要共享访问同一个volume时,NodeStageVolume就派上用场了,不过这种场景并不常见。NodeUnstageVolume和NodeUnpublishVolume则是对应的逆向操作。
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 NodeGetCapabilities (NodeGetCapabilitiesRequest) returns (NodeGetCapabilitiesResponse) {} rpc NodeGetInfo (NodeGetInfoRequest) returns (NodeGetInfoResponse) {}}
从功能模块的角度来看,每个CSI plugin又可以分为两个Plugin:
Node PluginController Plugin
对于Node Plugin来说,必须要实现上面的Identify Service和Node Service两组接口。而Controller Plugin则必须要实现Identify Service和Controller Service两组接口。不难看出,Identify Service是两种Plugin都必须实现的接口。
可以为Node Plugin和Controller Plugin各生成一个docker image,也可以将它们的功能包含在一个docker image中。为了便于维护,通常是将它们包含一个docker image中。
有人可能会疑惑,如果包含在一个docker image中,那么实际运行时如何区分呢?不用着急,看到后面就会明白!
Kubernetes与CSI plugin的集成
Kubernetes提供了完善的解决方案用于集成CSI plugin。其实就是Kubernetes提供了一些sidecar containers。先给出一个典型的部署图:
Controller Plugin与external-provisioner和external-attacher这两个sidecar部署在一起,作为一个StatefulSets运行在master上。external-provisioner监控k8s cluster内PVC的变化,当有一个新的PVC产生,并满足下面两个条件时,external-provisioner就会通过gRPC调用CSI Controller Plugin的CreateVolume接口函数,动态创建一个Volume。
该PVC的storageClassName设置的就是我们开发的CSI Plugin;没有满足的PV存在。
当上面那个PVC被删除并且对应的StorageClass的reclaimPolicy是Delete时,external-provisioner就会通过gRPC调用Controller Plugin的DeleteVolume接口函数,动态删除之前创建的Volume。
接下来,当某个POD使用了上面的PVC,并且该POD被调度到某个node上时,external-attacher就会调用Controller Plugin的ControllerPublishVolune接口函数,动态将之间创建的Volulme attach到POD所在的node上。反之,当POD被删除时,external-attacher就会调用Controller Plugin的ControllerUnpublishVolume将之前attach的volume进行detach。
Node Plugin只与一个sidecar container部署在一起,也就是与node-driver-registrar部署在一起,作为Daemonset运行在每一个node上。node-driver-registrar的作用是将Node Plugin向Kubelet注册。在external-attacher将Volume attach到POD所在的node之后,Kubelet就会调用Node Plugin的NodePublishVolume将volume挂载到POD内。当该POD被删除时,Kubelet会调用Node Plugin的NodeUnpublishVolume来做unmount操作。
最终用户使用举例
这里的用户是指使用我们开发的CSI Plugin的用户。这里只举一个典型的动态create/attach/mount的例子。
首先,用户需要创建一个StorageClass(假设文件名为my-sc.yaml),其中的provisioner必须设置成我们开发的CSI plugin的名称。注意这里的名称必须与Identity Service中的GetPluginInfo返回的名称匹配。假设你的CSI Plugin是“csi-excellent-plugin”,那么就可以定义下面的StorageClass。当然我们也可以提供默认的StorageClass给用户使用。
apiVersion: storage.k8s.io/v1kind: StorageClassmetadata: name: csi-my-scprovisioner: csi-excellent-pluginreclaimPolicy: DeletevolumeBindingMode: Immediate
使用如下命令创建上面的StorageClass.yaml:
kubectl create -f my-sc.yaml
接着,用户可以定义如下PVC(假设文件名为my-pvc.yaml)。其中的storageClassName就是上面的定义的StorageClass的名称。
apiVersion: v1kind: PersistentVolumeClaimmetadata: name: csi-pvcspec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi storageClassName: csi-my-sc
当使用下面的命令创建上面的PVC时,就会自动创建一个Volume,
kubectl create -f my-pvc.yaml
最后在POD中就可以直接用上面的PVC,假设文件名为my-pod.yaml。
kind: PodapiVersion: v1metadata: name: my-csi-appspec: containers: - name: my-fancy-app image: busybox volumeMounts: - mountPath: "/data" name: my-csi-volume command: [ "sleep", "1000000" ] volumes: - name: my-csi-volume persistentVolumeClaim: claimName: csi-pvc
当使用下面的命令创建上面的POD时,之前创建的volume就会自动被attach到POD所在的node上,并且被mount到POD内,
kubectl create -f my-pod.yaml
深入探讨
零君会在此系列的后续文章中单独详细介绍。
--END--
发表评论
暂时没有评论,来抢沙发吧~