和K8S的网络不太一样,K8S的网络只有CNI一种接口暴露方式,所有的网络实现基于第三方进行开发实现,但存储的内置实现就多达20多种,#K8S目前支持的插件类型#。但内置的往往不满足定制化的需求,所以和CNI 一样,K8S 也暴露了对外的存储接口,和CNI 一样,通过实现对应的接口方法,即可创建属于自己的存储插件,但和CNI 有点区别的是,K8S的存储插件的自定义实现方式,有FlexVolume 和 CSI 两种,两者的差别可以看做是新老功能的差异,但目前为止,FlexVolume 同样也有用武之地。
熟悉CNI的编写方法的,对FlexVolume的编写一定不陌生,CNI编写完成后,是会拆分为2个二进制文件(CNI,IPAM)和一个配置文件,放在每个Node节点上,kubelet在创建Pod 时候,会调用对应的二进制文件进行网络的创建,同样也可以使用daemonset的方式容器化部署,FlexVolume 也一样,编写完成后一样以二进制的方式进行部署,和CNI 一样,FlexVolume需要实现类似CNI的cmdadd,cmddel的方法,具体需要实现以下几个方法:
基本返回格式:
{ "status": "", "message": "", "device": "" "volumeName": "" "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: v1kind: PersistentVolumemetadata: name: pv-flex-nfsspec: 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和FlexVolume的基本差异,FlexVolume实现了Attach(挂载存储到Node)和Mount(Node的目录挂载到Pod), 缺少了PV的动态生成(需要运维手动在存储上配置然后再创建手动创建PV),而CSI就是在FlexVolume的基础上,实现了PV的动态生成。
CSI的调用和FlexVolume不一样,在调用的时候,需要有一个注册的过程,找个CSI的代码看下:
简单先描述一下调用顺序:
上面只是描述了CSI插件的调用顺序,那么问题来了,每一步,分别是谁去调用的呢?
回过头先看下CSI的架构图:
发现CSI的架构实际分了3块,第一块K8S-Core,即K8S的核心组件,第二块Kubernetes External Component, 这是Kubernetes支持CSI的扩展组件,第三块External Component:传统意义上的CSI,即上面的那个demo代码。从官网架构图的箭头可以看到整体的调用关系:
第二块External Component的下载地址
从上面的调用逻辑可以看出,出了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模式的部署方式进行部署。
简单画个部署图: