K8S的自定义存储插件
和K8S的网络不太一样,K8S的网络只有CNI一种接口暴露方式,所有的网络实现基于第三方进行开发实现,但存储的内置实现就多达20多种,#K8S目前支持的插件类型#。但内置的往往不满足定制化的需求,所以和CNI 一样,K8S 也暴露了对外的存储接口,和CNI 一样,通过实现对应的接口方法,即可创建属于自己的存储插件,但和CNI 有点区别的是,K8S的存储插件的自定义实现方式,有FlexVolume 和 CSI 两种,两者的差别可以看做是新老功能的差异,但目前为止,FlexVolume 同样也有用武之地。
FlexVolume
熟悉CNI的编写方法的,对FlexVolume的编写一定不陌生,CNI编写完成后,是会拆分为2个二进制文件(CNI,IPAM)和一个配置文件,放在每个Node节点上,kubelet在创建Pod 时候,会调用对应的二进制文件进行网络的创建,同样也可以使用daemonset的方式容器化部署,FlexVolume 也一样,编写完成后一样以二进制的方式进行部署,和CNI 一样,FlexVolume需要实现类似CNI的cmdadd,cmddel的方法,具体需要实现以下几个方法:
- init:kubelet/kube-controller-manager 初始化存储插件时调用,插件需要返回是否需要attach 和 detach 操作
- attach:将存储卷挂载到 Node 上
- detach:将存储卷从 Node 上卸载
- waitforattach: 等待 attach 操作成功(超时时间为 10 分钟)
- isattached:检查存储卷是否已经挂载
- mountdevice:将设备挂载到指定目录中以便后续 bind mount 使用
- unmountdevice:将设备取消挂载
- mount:将存储卷挂载到指定目录中
- umount:将存储卷取消挂载
基本返回格式:
{
"status": "<Success/Failure/Not supported>",
"message": "<Reason for success/failure>",
"device": "<Path to the device attached. This field is valid only for attach & waitforattach call-outs>"
"volumeName": "<Cluster wide unique name of the volume. Valid only for getvolumename call-out>"
"attached": <True/False (Return true if volume is attached on the node. Valid only for isattached call-out)>
"capabilities": <Only included as part of the Init response>
{
"attach": <True/False (Return true if the driver implements attach and detach)>
}
}
那么kublet和他的调用关系是啥?看一下kubelet调用的FlexVolume的一段代码(pod mount dir):
// SetUpAt creates new directory.
func (f *flexVolumeMounter) SetUpAt(dir string, fsGroup *int64) error {
...
call := f.plugin.NewDriverCall(mountCmd)
// Interface parameters
call.Append(dir)
extraOptions := make(map[string]string)
// pod metadata
extraOptions[optionKeyPodName] = f.podName
extraOptions[optionKeyPodNamespace] = f.podNamespace
...
call.AppendSpec(f.spec, f.plugin.host, extraOptions)
_, err = call.Run()
...
return nil
}
再看下一个PV的yml的栗子对比一下:
apiVersion: v1
kind: PersistentVolume
metadata:
name: pv-flex-nfs
spec:
capacity:
storage: 1Gi
accessModes:
- ReadWriteMany
flexVolume:
driver: "k8s/nfs"
fsType: "nfs"
options:
server: "1.1.1.1"
share: "export"
先只看FlexVolume, dirver这里的k8s/nfs 就是FlexVolume的具体位置,注意k8s~nfs解析出来的就是k8s/nfs, FlexVolume的默认位置在/usr/libexec/kubernetes/kubelet-plugins/volume/exec/
,所以上述yml用的FlexVolume插件具体位置在/usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs
。
pv的yml 里面有对应的options, 就是kubelet代码里的extraOptions := make(map[string]string)
,是一个map类型,kubelet解析yml里面的option参数,传入该map变量,然后执行FlexVolume的具体方法,比如栗子里的mount,将options的参数传入,实现了如下的效果:
/usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs mount <mount dir> <json param>
git 上fork的几个demo,方便查询。
可以看到FlexVolume实现逻辑非常简单,但解决的问题也非常有限,用户还是需要手动创建PV,且每次调用插件执行的都是偏原子操作的,即单次只执行attach,mount,umount等动作,所以,其作用,只是一个针对不同存储自定义创建PV的插件,假设我需要动态生成PV 并动态绑定PVC,FlexVolume就不具备可操作性,所以CSI来了。
CSI
先说下CSI和FlexVolume的基本差异,FlexVolume实现了Attach(挂载存储到Node)和Mount(Node的目录挂载到Pod), 缺少了PV的动态生成(需要运维手动在存储上配置然后再创建手动创建PV),而CSI就是在FlexVolume的基础上,实现了PV的动态生成。
CSI的调用和FlexVolume不一样,在调用的时候,需要有一个注册的过程,找个CSI的代码看下:
- CSI-Code-Demo:
目录机构:
简单先描述一下调用顺序:
- driver.go, 主要是Run()方法,启动RPC服务,注册到CSI.
- identity.go, 主要是GetPluginInfo()方法,获取插件的各类信息,如插件名字等,GetPluginCapabilities,获取CSI插件的各项功能,比如是否支持Attach之类的,还有个Probe()方法,提供给K8s的探针,用于健康检查CSI的状态。
- controller.go, CSI实现的具体方法,比如操作存储(CreateVolume 和 DeleteVolume),Attach存储(ControllerPublishVolume 和 ControllerUnpublishVolume方法)
- node.go, 这一步主要实现的是mount的操作,即将目录挂载到Pod里,但是和简单的mount方式不一样,这里分为了MountDevice(预处理,挂载到一个临时目录进行格式化) 和 SetUp(最终绑定,将临时目录绑定到实际的目录), 分别对应了NodeStageVolume/NodeUnstageVolume 和 NodePublishVolume/NodeUnpublishVolume 方法。
上面只是描述了CSI插件的调用顺序,那么问题来了,每一步,分别是谁去调用的呢?
回过头先看下CSI的架构图:
发现CSI的架构实际分了3块,第一块K8S-Core,即K8S的核心组件,第二块Kubernetes External Component, 这是Kubernetes支持CSI的扩展组件,第三块External Component:传统意义上的CSI,即上面的那个demo代码。从官网架构图的箭头可以看到整体的调用关系:
- daemonset在每台主机上运行了driver-registrar的container,和kubelet一一对应。
- kubelet调用driver-registrar,driver-registrar向CSI indentity(即代码里的driver.go和identity.go)进行了注册,并获取了CSI 插件的基本功能以及信息。
- External provisioner watch 了master的apiserver,监听pvc对象,一旦发现有创建, 则External provisioner会调用CSI Controller(调用了controller.go 里的CreateVolume/DeleteVolume方法)进行了存储端的创建,即自动生成了PV。
- External attacher,监听了VolumeAttachment对象,一个发现有挂载,则调用CSI Controller 和 CSI Node进行卷的Attach和mount操作(分别调用了controller.go和node.go)。
从上面的调用逻辑可以看出,出了CSI本体(这里暂称为CSI Driver),还需要部署External Component里的三个container,且这3个container里只有driver-registrar需要和kubelet调用,所以在实际部署中,需要以daemonset的方式,将driver-registrar和CSI Driver作为side-car模式的部署方式进行部署,其他2个External provisioner和 External attacher以statefulset的方式,和CSI Driver一起作为side-car模式的部署方式进行部署。
简单画个部署图: