30 | 编写自己的存储插件:FlexVolume与CSI
下载APP
关闭
渠道合作
推荐作者
30 | 编写自己的存储插件:FlexVolume与CSI
2018-10-31 张磊 来自北京
《深入剖析Kubernetes》
课程介绍
讲述:张磊
时长15:07大小6.92M
你好,我是张磊。今天我和你分享的主题是:编写自己的存储插件之 FlexVolume 与 CSI。
在上一篇文章中,我为你详细介绍了 Kubernetes 里的持久化存储体系,讲解了 PV 和 PVC 的具体实现原理,并提到了这样的设计实际上是出于对整个存储体系的可扩展性的考虑。
而在今天这篇文章中,我就和你分享一下如何借助这些机制,来开发自己的存储插件。
在 Kubernetes 中,存储插件的开发有两种方式:FlexVolume 和 CSI。
接下来,我就先为你剖析一下Flexvolume 的原理和使用方法。
举个例子,现在我们要编写的是一个使用 NFS 实现的 FlexVolume 插件。
对于一个 FlexVolume 类型的 PV 来说,它的 YAML 文件如下所示:
可以看到,这个 PV 定义的 Volume 类型是 flexVolume。并且,我们指定了这个 Volume 的 driver 叫作 k8s/nfs。这个名字很重要,我后面马上会为你解释它的含义。
而 Volume 的 options 字段,则是一个自定义字段。也就是说,它的类型,其实是 map[string]string。所以,你可以在这一部分自由地加上你想要定义的参数。
在我们这个例子里,options 字段指定了 NFS 服务器的地址(server: “10.10.0.25”),以及 NFS 共享目录的名字(share: “export”)。当然,你这里定义的所有参数,后面都会被 FlexVolume 拿到。
像这样的一个 PV 被创建后,一旦和某个 PVC 绑定起来,这个 FlexVolume 类型的 Volume 就会进入到我们前面讲解过的 Volume 处理流程。
你应该还记得,这个流程的名字叫作“两阶段处理”,即“Attach 阶段”和“Mount 阶段”。它们的主要作用,是在 Pod 所绑定的宿主机上,完成这个 Volume 目录的持久化过程,比如为虚拟机挂载磁盘(Attach),或者挂载一个 NFS 的共享目录(Mount)。
而在具体的控制循环中,这两个操作实际上调用的,正是 Kubernetes 的 pkg/volume 目录下的存储插件(Volume Plugin)。在我们这个例子里,就是 pkg/volume/flexvolume 这个目录里的代码。
当然了,这个目录其实只是 FlexVolume 插件的入口。以“Mount 阶段”为例,在 FlexVolume 目录里,它的处理过程非常简单,如下所示:
上面这个名叫 SetUpAt() 的方法,正是 FlexVolume 插件对“Mount 阶段”的实现位置。而 SetUpAt() 实际上只做了一件事,那就是封装出了一行命令(即:NewDriverCall),由 kubelet 在“Mount 阶段”去执行。
在我们这个例子中,kubelet 要通过插件在宿主机上执行的命令,如下所示:
其中,/usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs 就是插件的可执行文件的路径。这个名叫 nfs 的文件,正是你要编写的插件的实现。它可以是一个二进制文件,也可以是一个脚本。总之,只要能在宿主机上被执行起来即可。
而且这个路径里的 k8s~nfs 部分,正是这个插件在 Kubernetes 里的名字。它是从 driver="k8s/nfs"字段解析出来的。
这个 driver 字段的格式是:vendor/driver。比如,一家存储插件的提供商(vendor)的名字叫作 k8s,提供的存储驱动(driver)是 nfs,那么 Kubernetes 就会使用 k8s~nfs 来作为插件名。
所以说,当你编写完了 FlexVolume 的实现之后,一定要把它的可执行文件放在每个节点的插件目录下。
而紧跟在可执行文件后面的“mount”参数,定义的就是当前的操作。在 FlexVolume 里,这些操作参数的名字是固定的,比如 init、mount、unmount、attach,以及 dettach 等等,分别对应不同的 Volume 处理操作。
而跟在 mount 参数后面的两个字段:<mount dir>和<json params>,则是 FlexVolume 必须提供给这条命令的两个执行参数。
其中第一个执行参数<mount dir>,正是 kubelet 调用 SetUpAt() 方法传递来的 dir 的值。它代表的是当前正在处理的 Volume 在宿主机上的目录。在我们的例子里,这个路径如下所示:
其中,test 正是我们前面定义的 PV 的名字;而 k8s~nfs,则是插件的名字。可以看到,插件的名字正是从你声明的 driver="k8s/nfs"字段里解析出来的。
而第二个执行参数<json params>,则是一个 JSON Map 格式的参数列表。我们在前面 PV 里定义的 options 字段的值,都会被追加在这个参数里。此外,在 SetUpAt() 方法里可以看到,这个参数列表里还包括了 Pod 的名字、Namespace 等元数据(Metadata)。
在明白了存储插件的调用方式和参数列表之后,这个插件的可执行文件的实现部分就非常容易理解了。
在这个例子中,我直接编写了一个简单的 shell 脚本来作为插件的实现,它对“Mount 阶段”的处理过程,如下所示:
可以看到,当 kubelet 在宿主机上执行“nfs mount <mount dir> <json params>”的时候,这个名叫 nfs 的脚本,就可以直接从<mount dir>参数里拿到 Volume 在宿主机上的目录,即:MNTPATH=$1。而你在 PV 的 options 字段里定义的 NFS 的服务器地址(options.server)和共享目录名字(options.share),则可以从第二个<json params>参数里解析出来。这里,我们使用了 jq 命令,来进行解析工作。
有了这三个参数之后,这个脚本最关键的一步,当然就是执行:mount -t nfs ${NFS_SERVER}:/${SHARE} ${MNTPATH} 。这样,一个 NFS 的数据卷就被挂载到了 MNTPATH,也就是 Volume 所在的宿主机目录上,一个持久化的 Volume 目录就处理完了。
需要注意的是,当这个 mount -t nfs 操作完成后,你必须把一个 JOSN 格式的字符串,比如:{“status”: “Success”},返回给调用者,也就是 kubelet。这是 kubelet 判断这次调用是否成功的唯一依据。
综上所述,在“Mount 阶段”,kubelet 的 VolumeManagerReconcile 控制循环里的一次“调谐”操作的执行流程,如下所示:
当然,在前面文章中我也提到过,像 NFS 这样的文件系统存储,并不需要在宿主机上挂载磁盘或者块设备。所以,我们也就不需要实现 attach 和 dettach 操作了。
不过,像这样的 FlexVolume 实现方式,虽然简单,但局限性却很大。
比如,跟 Kubernetes 内置的 NFS 插件类似,这个 NFS FlexVolume 插件,也不能支持 Dynamic Provisioning(即:为每个 PVC 自动创建 PV 和对应的 Volume)。除非你再为它编写一个专门的 External Provisioner。
再比如,我的插件在执行 mount 操作的时候,可能会生成一些挂载信息。这些信息,在后面执行 unmount 操作的时候会被用到。可是,在上述 FlexVolume 的实现里,你没办法把这些信息保存在一个变量里,等到 unmount 的时候直接使用。
这个原因也很容易理解:FlexVolume 每一次对插件可执行文件的调用,都是一次完全独立的操作。所以,我们只能把这些信息写在一个宿主机上的临时文件里,等到 unmount 的时候再去读取。
这也是为什么,我们需要有 Container Storage Interface(CSI)这样更完善、更编程友好的插件方式。
接下来,我就来为你讲解一下开发存储插件的第二种方式 CSI。我们先来看一下CSI 插件体系的设计原理。
其实,通过前面对 FlexVolume 的讲述,你应该可以明白,默认情况下,Kubernetes 里通过存储插件管理容器持久化存储的原理,可以用如下所示的示意图来描述:
可以看到,在上述体系下,无论是 FlexVolume,还是 Kubernetes 内置的其他存储插件,它们实际上担任的角色,仅仅是 Volume 管理中的“Attach 阶段”和“Mount 阶段”的具体执行者。而像 Dynamic Provisioning 这样的功能,就不是存储插件的责任,而是 Kubernetes 本身存储管理功能的一部分。
相比之下,CSI 插件体系的设计思想,就是把这个 Provision 阶段,以及 Kubernetes 里的一部分存储管理功能,从主干代码里剥离出来,做成了几个单独的组件。这些组件会通过 Watch API 监听 Kubernetes 里与存储相关的事件变化,比如 PVC 的创建,来执行具体的存储管理动作。
而这些管理动作,比如“Attach 阶段”和“Mount 阶段”的具体操作,实际上就是通过调用 CSI 插件来完成的。
这种设计思路,我可以用如下所示的一幅示意图来表示:
可以看到,这套存储插件体系多了三个独立的外部组件(External Components),即:Driver Registrar、External Provisioner 和 External Attacher,对应的正是从 Kubernetes 项目里面剥离出来的那部分存储管理功能。
需要注意的是,External Components 虽然是外部组件,但依然由 Kubernetes 社区来开发和维护。
而图中最右侧的部分,就是需要我们编写代码来实现的 CSI 插件。一个 CSI 插件只有一个二进制文件,但它会以 gRPC 的方式对外提供三个服务(gRPC Service),分别叫作:CSI Identity、CSI Controller 和 CSI Node。
我先来为你讲解一下这三个 External Components。
其中,Driver Registrar 组件,负责将插件注册到 kubelet 里面(这可以类比为,将可执行文件放在插件目录下)。而在具体实现上,Driver Registrar 需要请求 CSI 插件的 Identity 服务来获取插件信息。
而 External Provisioner 组件,负责的正是 Provision 阶段。在具体实现上,External Provisioner 监听(Watch)了 APIServer 里的 PVC 对象。当一个 PVC 被创建时,它就会调用 CSI Controller 的 CreateVolume 方法,为你创建对应 PV。
此外,如果你使用的存储是公有云提供的磁盘(或者块设备)的话,这一步就需要调用公有云(或者块设备服务)的 API 来创建这个 PV 所描述的磁盘(或者块设备)了。
不过,由于 CSI 插件是独立于 Kubernetes 之外的,所以在 CSI 的 API 里不会直接使用 Kubernetes 定义的 PV 类型,而是会自己定义一个单独的 Volume 类型。
为了方便叙述,在本专栏里,我会把 Kubernetes 里的持久化卷类型叫作 PV,把 CSI 里的持久化卷类型叫作 CSI Volume,请你务必区分清楚。
最后一个 External Attacher 组件,负责的正是“Attach 阶段”。在具体实现上,它监听了 APIServer 里 VolumeAttachment 对象的变化。VolumeAttachment 对象是 Kubernetes 确认一个 Volume 可以进入“Attach 阶段”的重要标志,我会在下一篇文章里为你详细讲解。
一旦出现了 VolumeAttachment 对象,External Attacher 就会调用 CSI Controller 服务的 ControllerPublish 方法,完成它所对应的 Volume 的 Attach 阶段。
而 Volume 的“Mount 阶段”,并不属于 External Components 的职责。当 kubelet 的 VolumeManagerReconciler 控制循环检查到它需要执行 Mount 操作的时候,会通过 pkg/volume/csi 包,直接调用 CSI Node 服务完成 Volume 的“Mount 阶段”。
在实际使用 CSI 插件的时候,我们会将这三个 External Components 作为 sidecar 容器和 CSI 插件放置在同一个 Pod 中。由于 External Components 对 CSI 插件的调用非常频繁,所以这种 sidecar 的部署方式非常高效。
接下来,我再为你讲解一下 CSI 插件的里三个服务:CSI Identity、CSI Controller 和 CSI Node。
其中,CSI 插件的 CSI Identity 服务,负责对外暴露这个插件本身的信息,如下所示:
而 CSI Controller 服务,定义的则是对 CSI Volume(对应 Kubernetes 里的 PV)的管理接口,比如:创建和删除 CSI Volume、对 CSI Volume 进行 Attach/Dettach(在 CSI 里,这个操作被叫作 Publish/Unpublish),以及对 CSI Volume 进行 Snapshot 等,它们的接口定义如下所示:
不难发现,CSI Controller 服务里定义的这些操作有个共同特点,那就是它们都无需在宿主机上进行,而是属于 Kubernetes 里 Volume Controller 的逻辑,也就是属于 Master 节点的一部分。
需要注意的是,正如我在前面提到的那样,CSI Controller 服务的实际调用者,并不是 Kubernetes(即:通过 pkg/volume/csi 发起 CSI 请求),而是 External Provisioner 和 External Attacher。这两个 External Components,分别通过监听 PVC 和 VolumeAttachement 对象,来跟 Kubernetes 进行协作。
而 CSI Volume 需要在宿主机上执行的操作,都定义在了 CSI Node 服务里面,如下所示:
需要注意的是,“Mount 阶段”在 CSI Node 里的接口,是由 NodeStageVolume 和 NodePublishVolume 两个接口共同实现的。我会在下一篇文章中,为你详细介绍这个设计的目的和具体的实现方式。
总结
在本篇文章里,我为你详细讲解了 FlexVolume 和 CSI 这两种自定义存储插件的工作原理。
可以看到,相比于 FlexVolume,CSI 的设计思想,把插件的职责从“两阶段处理”,扩展成了 Provision、Attach 和 Mount 三个阶段。其中,Provision 等价于“创建磁盘”,Attach 等价于“挂载磁盘到虚拟机”,Mount 等价于“将该磁盘格式化后,挂载在 Volume 的宿主机目录上”。
当 AttachDetachController 需要进行“Attach”操作时(“Attach 阶段”),它实际上会执行到 pkg/volume/csi 目录中,创建一个 VolumeAttachment 对象,从而触发 External Attacher 调用 CSI Controller 服务的 ControllerPublishVolume 方法。
当 VolumeManagerReconciler 需要进行“Mount”操作时(“Mount 阶段”),它实际上也会执行到 pkg/volume/csi 目录中,直接向 CSI Node 服务发起调用 NodePublishVolume 方法的请求。
以上,就是 CSI 插件最基本的工作原理了。
在下一篇文章里,我会和你一起实践一个 CSI 存储插件的完整实现过程。
思考题
假设现在,你的宿主机是阿里云的一台虚拟机,你要实现的容器持久化存储,是基于阿里云提供的云盘。你能准确地描述出,在 Provision、Attach 和 Mount 阶段,CSI 插件都需要做哪些操作吗?
感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。
分享给需要的人,Ta购买本课程,你将得20元
生成海报并分享
赞 32
提建议
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
上一篇
29 | PV、PVC体系是不是多此一举?从本地持久化卷谈起
下一篇
31 | 容器存储实践:CSI插件编写指南
精选留言(15)
- 虎虎❤️2018-10-31思考题: 1. Register过程: csi 插件应该作为 daemonSet 部署到每个节点(node)。然后插件 container 挂载 hostpath 文件夹,把插件可执行文件放在其中,并启动rpc服务(identity, controller, node)。External component Driver Registrar 利用 kubelet plugin watcher 特性watch指定的文件夹路径来自动检测到这个存储插件。然后通过调用identity rpc服务,获得driver的信息,并完成注册。 2. Provision过程:部署External Provisioner。 Provisioner 将会 watch apiServer 中 PVC 资源的创建,并且PVC 所指定的 storageClass 的 provisioner是我们上面启动的插件。那么,External Provisioner 将会调用 插件的 controller.createVolume() 服务。其主要工作应该是通过阿里云的api 创建网络磁盘,并根据磁盘的信息创建相应的pv。 3. Attach过程:部署External Attacher。Attacher 将会监听 apiServer 中 VolumeAttachment 对象的变化。一旦出现新的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 阶段。具体工作是调用 CRI 启动带有 volume 参数的container,把上阶段准备好的磁盘 mount 到 container指定的目录。展开共 1 条评论141
- ch_ort2020-10-29PVC描述的,是Pod想要使用的持久化存储的属性,比如存储的大小、读写权限等 PV描述的,则是一个具体的Volume的属性,比如Volume的类型、挂载目录、远程存储服务器地址等 有两种管理PV的方式: 人工管理(Static Provisioning),自动创建(Dynamic Provisioning)。Dynamic Provisioning机制工作的核心,就在于一个名叫StorageClass的API对象。Kubernetes能够根据用户提交的PVC,找到一个对应的StorageClass了。然后,Kuberentes就会调用该StorageClass声明的存储插件,创建出需要的PV。 需要注意的是,StorageClass并不是专门为了Dynamic Provisioning而设计的。比如,我在PV和PVC里都声明了storageClassName=manual。而我的集群里,实际上并没有一个叫manual的StorageClass对象。这完全没有问题,这个时候Kubernetes进行的是Static Provisioning,但在做绑定决策的时候,它依然会考虑PV和PVC的StorageClass定义。而这么做的好处也很明显:这个PVC和PV的绑定关系,就完全在我自己的掌握之中。 存储插件的开发方式有两种:FlexVolume和CSI FlexVolume: kubelet --> pkg/volume/flexvolume.SetUpAt() --> /usr/libexec/kubernetes/kubelet-plugins/volume/exec/k8s~nfs/nfs mount <mount dir> <json param> FlexVolume的方式,需要手动先创建好PV,由FlexVolume来执行Attach和Mount操作。 相比于 FlexVolume,CSI 的设计思想是把插件的职责从“两阶段处理”,扩展成了 Provision、Attach 和 Mount 三个阶段。其中,Provision 等价于“创建磁盘”,Attach 等价于“挂载磁盘到虚拟机”,Mount 等价于“将该磁盘格式化后,挂载在 Volume 的宿主机目录上”。展开14
- silver2018-11-01'test 正是我们前面定义的 PV 的名字',这个是否是typo?PV的名字是pv-flex-nfs?10
- Geek_5baa012021-06-06Provision:调用阿里云 API Create 云盘 Attach: 调用阿里云 API 挂载云盘到 ECS Mount: 挂载云盘到对应的 pod volume 目录10
- kakj2019-07-10java程序员从入门到放弃到再入门到再放弃中共 4 条评论6
- leo2018-11-20厉害了 新的知识!2
- 虎虎❤️2018-10-31问题: 1. 既然csi的PV是自己定义的类型,那么volume controller应该不会做这个红娘吧?所以问题是,他们是怎么完成绑定的?绑定后的状态会改变为 bound 吗? 2. 按照我的理解 driver 插件应该安装到每个node上,那么适合使用 daemonSet 去部署插件和 Driver Registerar sidecar。而 External Provisioner/Attacher 则只需要一份部署就可以。为什么文中建议把三个 External components 都部署为sidecar?展开1
- BobToGo2021-11-12🐮🍺
- chenkai-12021-07-011.register(包含调用identity获取插件信息):将插件注册到kubelet里面,将可执行文件放在插件目录下 2.External Provisioner:处理 Provision 的阶段。External Provisioner 监听APIServer 里的 PVC 对象。当一个 PVC 被创建时,调用 CSI Controller 的 CreateVolume 方法,创建PV。 3.External Attacher :处理“Attach 阶段”。监听了 APIServer 里 VolumeAttachment 对象的变化。一旦出现了 VolumeAttachment 对象,External Attacher 就会调用 CSI Controller 服务的 ControllerPublish 方法,完成它所对应的 Volume 的 Attach 阶段。 4.mount:kubelet 的 VolumeManagerReconciler 控制循环检查到它需要执行 Mount 操作的时候,会通过 pkg/volume/csi 包,直接调用 CSI Node 服务完成 Volume 的“Mount 阶段”。展开
- 惘 闻2021-02-05脑壳疼... 没用过docker 没用过k8s,操作系统知识不扎实,导致我看到这里好累啊,还是基础差共 1 条评论
- vincent2020-03-29简单说是两阶段: attach + mount 细了说:1、创建卷资源 2、attach节点 3、node上创建设备格式化 4、挂在设备1
- 左氧佛沙星人2019-10-16思考题,应该参考local path storage provisioner 或者 local volume storage provisioner,需要新增的是,讲创建好的云盘,attach到主机上,这样对吗?
- 大星星2019-03-15有个问题,请教下,三个external组建为什么要独立出来。这个不需要吧。只要csi 三个服务起来了,自动注册插件。他们三个服务也负责watch api,每当有请求过来,provision attatch.mount动作时候分别找对应服务请求就行。不知道都一个个分出来有必要么?共 1 条评论
- 陆培尔2018-11-02老师的课讲得太好了,什么时候开始讲容器网络方面的内容?感觉这一块一直有很多地方搞不明白,service,ingress,lb,跨节点组网等等
作者回复: 很快
- Alery2018-10-31你好,自己实现的nfs flexvolume是否可以在pvc中指定呢?怎么指定呢?共 1 条评论