我们详细介绍了Kubernetes的持久化存储体系,讲解PV和PVC具体实现原理,然后我们分享下如何借助这些机制,来开发自己的存储插件
存储插件的开发方式有两种 FlexVolume和CSI
首先是一个FlexVolume的原理和使用方式
假设是一个根据NFS实现的FlexVolume插件
对于一个FlexVolume类型的PV,其YAML文件如下
apiVersion: v1
kind: PersistentVolume metadata: name: pv-flex-nfs spec: capacity: storage: 10Gi accessModes: – ReadWriteMany flexVolume: driver: “k8s/nfs” fsType: “nfs” options: server: “10.10.0.25” # 改成你自己的NFS服务器地址 share: “export” |
这个PV定义的Volume类型是flexVolume,并且制定了这个Volume的driver为k8s/nfs,这个driver的字段蛮重要的,在Volume的options字段中,是一个自定义的字段,类型是map[string]string是一个可以随意定义的部分
然后,在options字段指定了NFS服务器的地址 和 NFS的共享目录的名字 share export,当然这些定义的参数都会被FlexVolume拿到
这样的PV创建后,会和PVC绑定,绑定流程如下
还是Attach阶段和Mount阶段,主要作用是在Pod绑定的宿主机上,完成这个Volume目录的持久化过程,为虚拟机挂载磁盘,或者挂载一个NFS的共享目录
这样,具体的控制循环中,这两个操作实际上调用的,是Kubernetes的pkg/volume目录下的控制插件,就是pkg/volume/flexvolume目录中的代码,这个目录只是FlexVolume插件的入口,在FlexVolume目录中,处理流程很简单
// 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 } |
整体SetUpAt()函数中,就是利用了一条命令 NewDriverCall,让Kubectl在Mount阶段去执行,在这个例子中,Kubelet要通过插件在宿主机上执行的命令
/usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs mount <mount dir> <json param>
上面的路径,就是插件的可执行路径,名为nfs的文件,正是编写的插件实现,可以是二进制,也可以是脚本
路径中的k8s~nfs,就是插件中的名字,是从driver=”k8s/nfs”中解析出的
这个driver的格式是vendor/driver,一家存储插件的提供商是K8S,提供的存储驱动是nfs,那么Kubernetes就会使用k8s-nfs来作为插件名
当有了实现后,一定要将可执行文件放在每个节点的插件目录下
而跟在后面的mount参数,就定义的当前操作,可选的操作由 init mount unmount attach deattach对应着不同的Volume操作
后面的mount dir,是Kubelet调用SetUpAt()方法传递来的dir的值,代表的是Volume在宿主机上的目录
第二个执行参数<json params>是一个Json MAP格式的参数列表,就是PV中options字段定义的值,其中还包括了Pod的名字,Namesapce名字等元数据
然后我们就可以利用自定义的可执行文件来进行控制了
domount() {
MNTPATH=$1 NFS_SERVER=$(echo $2 | jq -r ‘.server’) SHARE=$(echo $2 | jq -r ‘.share’) … mkdir -p ${MNTPATH} &> /dev/null mount -t nfs ${NFS_SERVER}:/${SHARE} ${MNTPATH} &> /dev/null if [ $? -ne 0 ]; then err “{ \”status\”: \”Failure\”, \”message\”: \”Failed to mount ${NFS_SERVER}:${SHARE} at ${MNTPATH}\”}” exit 1 fi log ‘{“status”: “Success”}’ exit 0 } |
整体命令中,就是取出了传递的NFS的server和共享目录名字,以及mount dir在Volume宿主机上的目录,
然后进行相关的挂载,根据是否成功返回不同的数值,如果失败了抛出异常,成功了返回 log {status success} 这一步的解析,使用了jq命令
这个脚本最关键的一步,就是执行mount -t nfs ${NFS_SERVER}:/${SHARE} ${MNTPATH}
这样,一个NFS的数据卷就挂载到了MNTPATH,就是Volume所在的宿主机目录上
对于抛出的数据,也就是返回给调用者,让Kubelet判断这次调用是否成功的唯一依据
上面,就是kubelet在VolumeManagerReconcile控制循环中的一次调谐操作的执行流程
kubelet –> pkg/volume/flexvolume.SetUpAt() –> /usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs mount <mount dir> <json param>
而且,NFS这种文件存储系统,不需要在进行attach操作,也就不需要对应的attach和deattach操作
但是,像是这种FlexVolume的实现方式,虽然简单,但是局限很大
因为原生的FlexVolume插件,不支持Dynamic Provisioning,除非单独编写一个专门的External Provisioner
而且在执行mount操作的时候,会生成一些挂载信息,这些信息,在后面执行unmount操作可能会运动,而我们没法将这些信息保存在一个变量中,只能考虑保存在一个临时文件,等unmount读取
这就是Container Storage Interface CSI,这样更加完善的插件方式出现的原因
我们先来看下CSI插件体系的设计原理
根据之前的说法,晓得了Kubernetes通过存储话插件管理容器持久化存储的原理,可以如下的示意图描述
也就是,无论是使用了什么类型的存储插件,实际上担任的角色,只是Volume管理中Attach阶段和Mount阶段的具体执行者
还是利用了解耦的思想,将上层的流程抽象为了几个简单操作和阶段
那么实际的Attach Mount的操作,就是调用CSI插件完成的
这个设计思路的实现,如上所示
存储插件中多了三个独立的外部组件External Components Driver Registrar,External Provisioner,External Attacher,对应的Kubernetes关于Volume这部分管理的功能,这是交给Kubernetes社区去维护管理的管理功能(或者理解为这是抽象?Kubelet调用这些抽象)
然后最右边的部分,就是需要我们去实现的CSI插件,分别由三个服务组成 CSI Idenity CSI Controller CSI Node
这三个External Components,分别的功能为Driver Registrar组件,负责将插件注册到kubelet上,将可执行文件放在插件目录下,然后Driver Registrar需要请求CSI插件中的CSI Identity
然后是External Provisisoner,对应的功能是Provision,External 监听了APIServer中的PVC对象,当一个PVC被创建的时候,就会调用CSI Controller的CreateVolume方法,为其创建对应的PV
如果使用的是公有云的磁盘,这一部分使用的是公有云的API来创建这个PV描述的磁盘或者块设备
最后的一个External Attacher组件,正是Attach阶段,监听了APIServer的VolumeAttachment对象的变化
这个对象是表示一个Volume是否可以进入Attach阶段的重要标志
一旦出现了VolumeAttachment对象,External Attacher就会调用CSI Controller的ControllerPulish方法,完成对应的Attach操作
而Volume的Mount阶段,不属于External Components阶段,是由pkg/volume/csi包直接进行调用的
而实际部署上,我们将这三个External Components作为sidecar容器和CSI插件放在同一个Pod中
具体的这三个服务讲解如下
我们首先是CSI Identity服务,负责对外暴露这个插件自身的信息,如下所示
service Identity {
// return the version and name of the plugin rpc GetPluginInfo(GetPluginInfoRequest) returns (GetPluginInfoResponse) {} // reports whether the plugin has the ability of serving the Controller interface rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) returns (GetPluginCapabilitiesResponse) {} // called by the CO just to check whether the plugin is running or not rpc Probe (ProbeRequest) returns (ProbeResponse) {} } |
这里我们先不说具体的实现,简单看下即可
然后,我们看下CSI Contoller服务,对应的是CSI Volume的管理接口,插件和删除CSI Volumme,对CSI Volume进行Attach/Dettach,以及CSI Volume进行Snapsht等,对应的接口定义如下所示
service Controller {
// provisions a volume rpc CreateVolume (CreateVolumeRequest) returns (CreateVolumeResponse) {} // deletes a previously provisioned volume rpc DeleteVolume (DeleteVolumeRequest) returns (DeleteVolumeResponse) {} // make a volume available on some required node rpc ControllerPublishVolume (ControllerPublishVolumeRequest) returns (ControllerPublishVolumeResponse) {} // make a volume un-available on some required node rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest) returns (ControllerUnpublishVolumeResponse) {} … // make a snapshot rpc CreateSnapshot (CreateSnapshotRequest) returns (CreateSnapshotResponse) {} // Delete a given snapshot rpc DeleteSnapshot (DeleteSnapshotRequest) returns (DeleteSnapshotResponse) {} … } |
这些接口都是,直接交给了Kubernetes里的Volume Controller的逻辑,属于Master节点的一部分,而且CSI Controller服务的实际调用者,并不是Kubernetes,而是External Provisioner和External Attacher,这两个External Components,分别通过PVC和VolumeAttachement对象,来进行和Kubernetes的协作
而CSI Volume需要在宿主机上执行的操作,都在CSI Node服务里面,如下所示:
service Node {
// temporarily mount the volume to a staging path rpc NodeStageVolume (NodeStageVolumeRequest) returns (NodeStageVolumeResponse) {} // unmount the volume from staging path rpc NodeUnstageVolume (NodeUnstageVolumeRequest) returns (NodeUnstageVolumeResponse) {} // mount the volume from staging to target path rpc NodePublishVolume (NodePublishVolumeRequest) returns (NodePublishVolumeResponse) {} // unmount the volume from staging path rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest) returns (NodeUnpublishVolumeResponse) {} // stats for the volume rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest) returns (NodeGetVolumeStatsResponse) {} … // Similar to NodeGetId rpc NodeGetInfo (NodeGetInfoRequest) returns (NodeGetInfoResponse) {} } |
在Mount阶段,一共调用了NodeStageVolume和NodePublishVolume两个接口共同实现
那么,在本章中,我们详细讲解了FlexVolume和CSI这两个自定义存储插件的工作原理
在其中,将两阶段处理,扩展为了Provsion Attach Mount三个阶段,Provision等价于 创建磁盘,Attach等价于 挂载磁盘到虚拟机 ,Mount 等价于 将这个磁盘格式化后,挂载到Volume的宿主机目录上
有了CSI插件之后,Kubernetes本身就会进行绑定操作,不过,在进行Attach操作,实际上会执行到pkg/volume/csi 目录中,创建一个VolumeAttachment对象,从而触发External Attacher调用CSI Controller服务的Controller服务的ControllerPublishVolume
当VolumeManagerReconciler需要进行Mount操作时候,实际上会执行到pkg/volume/csi目录上,直接向CSI Node服务发起调用NodePublishVolume方法的请求
加入,宿主机是一个阿里云的虚拟机,需要实现持久化,是基于阿里云的云盘,那么在Provision Attach和Mount阶段,CSI需要什么操作?
1.首先是Register过程,csi插件类似daemonSet部署在每一个节点上,然后插件container挂载hostpath文件,将插件的可执行文件放在其中,启动rpc服务,这样Externam Component Driver Registrar 就可以利用kubelet plugin watcher 特性watch 到指定的文件夹来自动检测这个存储插件,然后调用identity rpc服务,获取driver信息,完成注册
2.Provision过程,部署External Provisioner,Provsioner会watch api Sever中关于PVC资源的创建,然后PVC指定的StorageClass的Provisioner就是上面启动的插件,那么External Provisioner会调用对应的Controller.createVolume()服务,通过阿里云的api,提交PVC描述的磁盘信息,创建对应的此岸
3.Attach过程,部署External Attacher,Attacher会监听apiServer中的VolumeAttachment对象的变化,一旦出现了,就说明需要进行绑定了,Attacher就会调用插件的controller.ControllerPublish()服务.主要工作调用阿里云api,将相对应的磁盘attach声明使用此PVC/PV的pod调度到对应的node
常见的挂载目录有/var/lib/kubelet/pods/<POD ID>/volumes/aliyun~netdisk/<name>
4.Mount过程:mount不可能在远程的container里完成,所以这个工作需要kubelet来做,kubelet的VolumeManagerReconciler控制循环,检测到需要执行Mount操作时候,通过调用pkg/volume/csi包,调用CSI Node服务,完成volume 的 Mount阶段