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的代码看下:

简单先描述一下调用顺序:

  1. driver.go, 主要是Run()方法,启动RPC服务,注册到CSI.
  2. identity.go, 主要是GetPluginInfo()方法,获取插件的各类信息,如插件名字等,GetPluginCapabilities,获取CSI插件的各项功能,比如是否支持Attach之类的,还有个Probe()方法,提供给K8s的探针,用于健康检查CSI的状态。
  3. controller.go, CSI实现的具体方法,比如操作存储(CreateVolume 和 DeleteVolume),Attach存储(ControllerPublishVolume 和 ControllerUnpublishVolume方法)
  4. 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代码。从官网架构图的箭头可以看到整体的调用关系:

  1. daemonset在每台主机上运行了driver-registrar的container,和kubelet一一对应。
  2. kubelet调用driver-registrar,driver-registrar向CSI indentity(即代码里的driver.go和identity.go)进行了注册,并获取了CSI 插件的基本功能以及信息。
  3. External provisioner watch 了master的apiserver,监听pvc对象,一旦发现有创建, 则External provisioner会调用CSI Controller(调用了controller.go 里的CreateVolume/DeleteVolume方法)进行了存储端的创建,即自动生成了PV。
  4. External attacher,监听了VolumeAttachment对象,一个发现有挂载,则调用CSI Controller 和 CSI Node进行卷的Attach和mount操作(分别调用了controller.go和node.go)。

第二块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模式的部署方式进行部署。
简单画个部署图:
在这里插入图片描述

个人公众号, 分享一些日常开发,运维工作中的日常以及一些学习感悟,欢迎大家互相学习,交流

在这里插入图片描述

Logo

K8S/Kubernetes社区为您提供最前沿的新闻资讯和知识内容

更多推荐