《深入剖析Kubernetes》总结八:容器持久化存储

容器化一个应用中,对其“状态”的管理是比较麻烦的,而最常见的“状态”,则为存储状态。 所以剖析下 Kubernetes 项目处理容器持久化存 储的核心原理

PV/PVC/StorageClass

PV和PVC的关系

PV 描述的是持久化存储数据卷;
这个 API 对象主要定义的是一个持久化存储在宿主机上的目录,比如一个 NFS 的挂载目录;
通常情况下,PV 对象是由运维人员事先创建在 Kubernetes 集群里待用的。

PVC 描述的是 Pod 所希望使用的持久化存储的属性,比如,Volume 存储的大小、可读写权限等等;
PVC 对象通常由开发人员创建,或者以 PVC 模板的方式成为 StatefulSet 的一部分,然后由 StatefulSet 控制器负责创建带编号的 PVC。

用户创建的 PVC 要真正被容器使用起来,就必须先和某个符合条件的 PV 进行绑定;
这里要检查的条件,包括两部分:
1 PV 和 PVC 的 spec 字段。比如,PV 的存储(storage)大小,就必须 满足 PVC 的要求;
2 PV 和 PVC 的 storageClassName 字段必须一样

在成功地将 PVC 和 PV 进行绑定之后,Pod 就能够像使用 hostPath 等常规类型的 Volume 一 样,在自己的 YAML 文件里声明使用这个 PVC 了;
PV 与 PVC 进行“绑定”,其实就是将这个 PV 对象的名字,填在了 PVC 对象的 spec.volumeName 字段上,接下来 Kubernetes 只要获取到这个 PVC 对象,就一定能 够找到它所绑定的 PV。

如果在创建 Pod 的时候,系统里并没有合适的 PV 跟它定义的 PVC 绑定,也就是说此时容器想要使用的 Volume 不存在,这时候,Pod 的启动就会报错,而当创建了对应的PV出来后, Volume Controller的一个控制循环会次完成 PVC 和 PV 的绑定操作;
Volume Controller 维护着多个控制循环,完成该操作的循环叫PersistentVolumeController

PV对象如何变成容器里的一个持久化存储?

大多数情况下,持久化 Volume 的实现,往往依赖于一个远程存储服务,比如:远程文件存储(比如,NFS、GlusterFS)、远程块存储(比如,公有云提供的远程磁盘)等等;
Kubernetes 需要做的工作,就是使用这些存储服务,来为容器准备一个持久化的宿主机目 录,以供将来进行绑定挂载时使用;
而“持久化”,指的是容器在这个目录里写入的文件, 都会保存在远程存储中,从而使得这个目录具备了“持久性”

准备“持久化”宿主机目录的过程可以形象地称为“两阶段处理”

一阶段(Attach):将 Persistent Disk 挂载到 Pod 所在的宿主机上。

二阶段(Mount):格式化这个磁盘设备,然后将它挂载到宿主机指定的挂载点上,挂载点则是 Volume 的宿主机目录

Mount 阶段完成后,这个 Volume 的宿主机目录就是一个“持久化”的目录了,容器在它里面 写入的内容,会保存在远程磁盘中

经过了“两阶段处理”,就得到了一个“持久化”的 Volume 宿主机目录;
接下来,kubelet 只要把这个 Volume 目录通过 CRI 里的 Mounts 参数,传递给 Docker,然后就可以为 Pod 里的容器挂载这个“持久化”的 Volume 了

删除PV,则执行相应的Unmount 和 Dettach来处理即可

这个流程相对比较独立,所以控制循环也可以独立出来,kubernetes使用Volume Controller的AttachDetachController来维护第一阶段(不断地检查每一个 Pod 对应的 PV,和这个 Pod 所在宿主机之间挂载情况,从而决定是否需要对这个 PV 进行 Attach(或者 Dettach)操作),VolumeManagerReconciler来维护第二阶段

StorageClass

PV 这个对象的创建,是由运维人员完成的,一个大规模的 Kubernetes 集群里很可能有成千上万个 PVC,这就意味着运维人员必须得事先创建出成千上万个 PV;
更麻烦的是,随着新的 PVC 不断被提交,运维人员就不得不继续添加新的、能满足条件的 PV,否则新的 Pod 就会因为 PVC 绑定不到 PV 而失败;
在实 际操作中,这几乎没办法靠人工做到

所以Kubernetes 提供了一套可以自动创建 PV 的机制,即:Dynamic Provisioning(人工管理PV的方式叫作 Static Provisioning)

Dynamic Provisioning 机制工作的核心在于一个名叫 StorageClass 的 API 对象,该对象的作用就是创建PV模板;
StorageClass 对象会定义两个部分内容:
1 PV 的属性,比如,存储类型、Volume 的大小等等;
2 创建这种 PV 需要用到的存储插件。比如,Ceph 等等;
有了这两个信息之后,Kubernetes 就能够根据用户提交的 PVC,找到一个对应的 StorageClass ,然后,Kubernetes 就会调用该 StorageClass 声明的存储插件,创建出需要 的 PV;
应用开发者只需要在 PVC 里指定要使用的 StorageClass 名字即可

有了 Dynamic Provisioning 机制,运维人员只需要在 Kubernetes 集群里创建出数量有限的 StorageClass 对象就可以了;
相当于创建了各种各样的 PV 模板,当开发人员提交了包含 StorageClass 字段的 PVC 之后,Kubernetes 就会根据 StorageClass 创建出对应的 PV

总结

在这里插入图片描述
PVC 描述的是 Pod 想要使用的持久化存储的属性,比如存储的大小、读写权限等

PV 描述的是一个具体的 Volume 的属性,比如 Volume 的类型、挂载目录、远程存储 服务器地址等。

StorageClass 的作用是充当 PV 的模板,并且,只有同属于一个 StorageClass 的 PV 和 PVC,才可以绑定在一起

StorageClass 的另一个重要作用,是指定 PV 的 Provisioner(存储插件);
这时候,如果存储插件支持 Dynamic Provisioning 的话,Kubernetes 就可以自动为你创建 PV 了。

PV/PVC体系存在的必要性

用户希望 Kubernetes 能够直接使用宿主机上的本地磁盘目录,而不依赖于远程存储 服务,来提供“持久化”的容器 Volume;
因为这个 Volume 直接使用的是本地磁盘,尤其是 SSD 盘,它的读写性能相比于大多数远程存储来说,要好得多;
这个需求对本地物理服务器部署的私有 Kubernetes 集群来说非常常见;
所以Kubernetes依赖 PV、PVC 体系实现了这个特性,这个特性的名字叫作:Local Persistent Volume

Local Persistent Volume场景:高优先级的系统应用,需要在多个不同节点上存储数据,并且对 I/O 较为敏感,比如分布式数据存储比如 MongoDB、Cassandra 等,分布式文件系统GlusterFS、Ceph 等,以及需要在本地磁盘上进行大量数据缓存的分布式应用

相比于正常的 PV,一旦这些节点宕机且不能恢复时,Local Persistent Volume 的数据就可能丢失;
这要求使用 Local Persistent Volume 的应用必须具备数据备份和恢复的能力, 允许把这些数据定时备份在其他位置

Local Persistent Volume 的设计,主要面临两个难点:

1 如何把本地磁盘抽象成 PV


Local Persistent Volume如果设计成等同于 hostPath 加 NodeAffinity ,那么会面临这样的问题:
本地目录的存储行为 完全不可控,它所在的磁盘随时都可能被应用写满,甚至造成整个宿主机宕机,而且,不同的本地目录之间也缺乏哪怕最基础的 I/O 隔离机制;


所以不应该把一个宿主机上的目录当作 PV 使用,一个 Local Persistent Volume 对应的存储介质,一定是一块额外挂载在宿主机的磁盘或者块设备(“额外”的意思是,它不应该是宿主机根目录所使用的主硬盘);
这个原则可以称为“一个 PV 一块盘”


2 调度器如何保证 Pod 始终能被正确地调度到它所请求的 Local Persistent Volume 所在的节点上

对于常规的 PV 来说,Kubernetes 都是先调度 Pod 到某个节点 上,然后再通过“两阶段处理”来“持久化”这台机器上的 Volume 目录,进而完成 Volume 目录与容器的绑定挂载;


对于 Local PV 来说,节点上可供使用的磁盘(或者块设备),必须是运维人员提前准备好的,它们在不同节点上的挂载情况可以完全不同,甚至有的节点可以没这种磁盘。,所以调度器就必须能够知道所有节点与 Local Persistent Volume 对应的磁盘的关联关系,然后根据这个信息来调度 Pod


这个原则可以称为“在调度的时候考虑 Volume 分布”。在 Kubernetes 的调度器里, 有一个叫作 VolumeBindingChecker 的过滤条件专门负责这个事情

使用时,只需要PV定义local字段,path字段写上本地磁盘路径即可

提交了 PV 和 PVC 的 YAML 文件之后,Kubernetes 就会根据它们俩的属性, 以及它们指定的 StorageClass 来进行绑定;
只有绑定成功后,Pod 才能通过声明这个 PVC 来 使用对应的 PV;
但是如果使用的是 Local Persistent Volume 的话,这个流程就行不通,例子如下:

比如现在有一个 Pod,它声明使用的 PVC 叫作 pvc-1,并且规定这个 Pod 只能 运行在 node-2 上


此时在 Kubernetes 集群中,有两个属性(比如:大小、读写权限)相同的 Local 类型的 PV;
第一个 PV 的名字叫作 pv-1,它对应的磁盘所在的节点是 node-1;
第二个 PV 的名字 叫作 pv-2,它对应的磁盘所在的节点是 node-2


假设现在Kubernetes 的 Volume 控制循环里,首先检查到了 pvc-1 和 pv-1 的属性是匹配的,于是就将它们俩绑定在一起,然后用户用 kubectl create 创建了这个 Pod,这时候,问题就出现了:
调度器看到这个 Pod 所声明的 pvc-1 已经绑定了 pv-1,而 pv-1 所在的节点是 node-1,根 据“调度器必须在调度的时候考虑 Volume 分布”的原则,这个 Pod 自然会被调度到 node-1上;
可是前面已经规定过,这个 Pod 只能 运行在 node-2上,所以最后的结果就是这个 Pod 的调度必然会失败

因此在使用 Local Persistent Volume 的时候,必须想办法推迟这个“绑定”操作,推迟到调度的时候即可;
也就是说 Kubernetes 里的 Volume 控制循环发现了 StorageClass 关联 的 PVC 与 PV 可以绑定在一起,但不要现在就执行绑定操作(即设置 PVC 的 VolumeName 字段),要等到第一个声明使用该 PVC 的 Pod 出现在调度器之后,调度器再综合考虑所有的调度规 则,当然也包括每个 PV 所在的节点位置,来统一决定,这个 Pod 声明的 PVC,到底应该跟哪 个 PV 进行绑定;
该操作由StorageClass 里的 volumeBindingMode=WaitForFirstConsumer指定

由于需要使用“延迟绑定”这个特性,Local Persistent Volume 目前还不能支持 Dynamic Provisioning

通过PV/PVC体系,添加Local Persistent Volume对用户的影响可以忽略不计,因此该体系存在的意义,就是提高可扩展性,声明式API也是如此

开发存储插件(todo)

存储插件的开发有两种方式:FlexVolume 和 CSI

CSI是相对更完善的方式

默认情况下,Kubernetes 里通过存储插件管理容器持久化存储的原理如下图:
在这里插入图片描述
FlexVolume的角色是 Volume 管理中的“Attach 阶段”和“Mount 阶段”的具体执行 者

像 Dynamic Provisioning 这样的功能,就不是存储插件的责任,而是 Kubernetes 自身存储管理功能的一部分

因此出现了CSI,CSI 插件体系的设计思想,就是把这个 Provision 阶段,以及 Kubernetes 里的一部 分存储管理功能,从主干代码里剥离出来,做成了几个单独的组件;
这些组件会通过 Watch API 监听 Kubernetes 里与存储相关的事件变化,比如 PVC 的创建,来执行具体的存储管理动作;
而这些管理动作,比如“Attach 阶段”和“Mount 阶段”的具体操作,实际上就是通过调用 CSI 插件来完成的
在这里插入图片描述
多了三个独立的外部组件(External Components),即: Driver Registrar、External Provisioner 和 External Attacher,对应的正是从 Kubernetes 项 目里面剥离出来的那部分存储管理功能

Driver Registrar 组件负责将插件注册到 kubelet 里面

External Provisioner 组件负责Provision 阶段

External Attacher 组件,负责Attach 阶段

Volume 的“Mount 阶段”则不属于 External Components 的职责;
当 kubelet 的 VolumeManagerReconciler 控制循环检查到它需要执行 Mount 操作的时候,会通过 pkg/volume/csi 包,直接调用 CSI Node 服务完成 Volume 的“Mount 阶段”。

CSI Identity 服务负责对外暴露这个插件本身的信息

CSI Controller 服务,定义的是对 CSI Volume(对应 Kubernetes 里的 PV)的管理接口,比如:创建和删除 CSI Volume、对 CSI Volume 进行 Attach/Dettach(在 CSI 里,这个 操作被叫作 Publish/Unpublish),以及对 CSI Volume 进行 Snapshot 等

CSI Node 服务定义了 CSI Volume 需要在宿主机上执行的操作,“Mount 阶段”在 CSI Node 里的接口,是由 NodeStageVolume 和 NodePublishVolume 两个接口共同实现的

总结来看,相比于 FlexVolume,CSI 的设计思想,把插件的职责从“两阶段处理”,扩展成了 Provision、Attach 和 Mount 三个阶段;
Provision 等价于“创建磁盘”,Attach 等价 于“挂载磁盘到虚拟机”,Mount 等价于“将该磁盘格式化后,挂载在 Volume 的宿主机目录上”

有了CSI后,工作方式多了如下区别:

1 当 AttachDetachController 需要进行“Attach”操作时(“Attach 阶段”),它实际上会执行到 pkg/volume/csi 目录中,创建一个 VolumeAttachment 对象,从而触发 External Attacher 调用 CSI Controller 服务的 ControllerPublishVolume 方法;
2 当 VolumeManagerReconciler 需要进行“Mount”操作时(“Mount 阶段”),它实际上也会执行到 pkg/volume/csi 目录中,直接向 CSI Node 服务发起调用 NodePublishVolume 方法的请求。

猜你喜欢

转载自blog.csdn.net/qq_41594698/article/details/107234958