我们详细介绍了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阶段

发表评论

邮箱地址不会被公开。 必填项已用*标注