Operator
Operator 的工作原理,实际上是利用了 Kubernetes 的自定义 API 资源(CRD),来描述我们想要部署的“有状态应用”;然后在自定义控制器里,根据自定义 API 对象的变化,来完成具体的部署和运维工作。
Etcd Operator 部署 Etcd 集群,采用的是静态集群(Static)的方式。
静态集群的好处是,它不必依赖于一个额外的服务发现机制来组建集群,非常适合本地容器化部署。而它的难点,则在于你必须在部署的时候,就规划好这个集群的拓扑结构,并且能够知道这些节点固定的 IP 地址。
首先,Etcd Operator 会创建一个“种子节点”;然后,Etcd Operator 会不断创建新的 Etcd 节点,然后将它们逐一加入到这个集群当中,直到集群的节点数等于 size。这就意味着,在生成不同角色的 Etcd Pod 时,Operator 需要能够区分种子节点与普通节点。–initial-cluster-state值设为 new 时,就代表了该节点是种子节点;值设为 existing,那就是说明这个节点是一个普通节点
Etcd Operator 启动要做的第一件事( c.initResource),是创建 EtcdCluster 对象所需要的 CRD,即:前面提到的etcdclusters.etcd.database.coreos.com
。这样 Kubernetes 就能够“认识”EtcdCluster 这个自定义 API 资源了。
而接下来,Etcd Operator 会定义一个 EtcdCluster 对象的 Informer。
当etcd的yaml文件提交到k8s之后,Etcd Operator 的 Informer,就会立刻“感知”到一个新的 EtcdCluster 对象被创建了出来。所以,EventHandler 里的“添加”事件会被触发——在 Etcd Operator 内部创建一个对应的 Cluster 对象
cluster两个工作:
1. **Bootstrap,即:创建一个单节点的种子集群。**
2. **启动该集群所对应的控制循环。**
以 addOneMember 方法为例,它执行的流程如下所示:
- 生成一个新节点的 Pod 的名字,比如:example-etcd-cluster-v6v6s6stxd;
- 调用 Etcd Client,执行前面提到过的 etcdctl member add example-etcd-cluster-v6v6s6stxd 命令;
- 使用这个 Pod 名字,和已经存在的所有节点列表,组合成一个新的 initial-cluster 字段的值;
- 使用这个 initial-cluster 的值,生成这个 Pod 里 Etcd 容器的启动命令。
PV、PVC、StorageClass
PV:持久化存储数据卷
这个 API 对象主要定义的是一个持久化存储在宿主机上的目录,比如一个 NFS 的挂载目录。由运维人员事先创建在 Kubernetes 集群里待用的
PVC:是 Pod 所希望使用的持久化存储的属性。
比如,Volume 存储的大小、可读写权限等等。PVC 对象通常由开发人员创建;或者以 PVC 模板的方式成为 StatefulSet 的一部分,然后由 StatefulSet 控制器负责创建带编号的 PVC。
而用户创建的 PVC 要真正被容器使用起来,就必须先和某个符合条件的 PV 进行绑定。这里要检查的条件,包括两部分:
- 第一个条件,当然是 PV 和 PVC 的 spec 字段。比如,PV 的存储(storage)大小,就必须满足 PVC 的要求。
- 而第二个条件,则是 PV 和 PVC 的 storageClassName 字段必须一样。这个机制我会在本篇文章的最后一部分专门介绍。
在成功地将 PVC 和 PV 进行绑定之后,Pod 就能够像使用 hostPath 等常规类型的 Volume 一样,在自己的 YAML 文件里声明使用这个 PVC 了
PVC 可以理解为持久化存储的“接口”,它提供了对某种持久化存储的描述,但不提供具体的实现;而这个持久化存储的实现部分则由 PV 负责完成。
Volume Controller 维护着多个控制循环,其中有一个循环,扮演的就是撮合 PV 和 PVC 的“红娘”的角色。它的名字叫作 PersistentVolumeController。他会不断地查看当前每一个 PVC,是不是已经处于 Bound(已绑定)状态。如果不是,那它就会遍历所有的、可用的 PV,并尝试将其与这个“单身”的 PVC 进行绑定。
PV 对象,是如何变成容器里的一个持久化存储的
所谓容器的 Volume,其实就是将一个宿主机上的目录,跟一个容器里的目录绑定挂载在了一起。而所谓的“持久化 Volume”,指的就是这个宿主机上的目录,具备“持久性”。
Kubernetes 需要做的工作,就是使用这些存储服务,来为容器准备一个持久化的宿主机目录,以供将来进行绑定挂载时使用。而所谓“持久化”,指的是容器在这个目录里写入的文件,都会保存在远程存储中,从而使得这个目录具备了“持久性”。
这个准备“持久化”宿主机目录的过程,我们可以形象地称为“两阶段处理”。
当一个 Pod 调度到一个节点上之后,kubelet 就要负责为这个 Pod 创建它的 Volume 目录。默认情况下,kubelet 为 Volume 创建的目录是如下所示的一个宿主机上的路径:
/var/lib/kubelet/pods/
这一步为虚拟机挂载远程磁盘的操作,对应的正是“两阶段处理”的第一阶段。在 Kubernetes 中,我们把这个阶段称为 Attach。
第二个操作,即:格式化这个磁盘设备,然后将它挂载到宿主机指定的挂载点上。这个将磁盘设备格式化并挂载到 Volume 宿主机目录的操作,对应的正是“两阶段处理”的第二个阶段,我们一般称为:Mount。
经过了“两阶段处理”,我们就得到了一个“持久化”的 Volume 宿主机目录。所以,接下来,kubelet 只要把这个 Volume 目录通过 CRI 里的 Mounts 参数,传递给 Docker,然后就可以为 Pod 里的容器挂载这个“持久化”的 Volume 了。
StorageClass
人工管理 PV 的方式: Static Provisioning;
自动创建 PV 的机制:Dynamic Provisioning。
Dynamic Provisioning 机制工作的核心,在于一个名叫 StorageClass 的 API 对象。
而 StorageClass 对象的作用,其实就是创建 PV 的模板。
具体地说,StorageClass 对象会定义如下两个部分内容:
- 第一,PV 的属性。比如,存储类型、Volume 的大小等等。
- 第二,创建这种 PV 需要用到的存储插件。比如,Ceph 等等。
有了这样两个信息之后,Kubernetes 就能够根据用户提交的 PVC,找到一个对应的 StorageClass 了。然后,Kubernetes 就会调用该 StorageClass 声明的存储插件,创建出需要的 PV。
- PVC 描述的,是 Pod 想要使用的持久化存储的属性,比如存储的大小、读写权限等。
- PV 描述的,则是一个具体的 Volume 的属性,比如 Volume 的类型、挂载目录、远程存储服务器地址等。
- 而 StorageClass 的作用,则是充当 PV 的模板。并且,只有同属于一个 StorageClass 的 PV 和 PVC,才可以绑定在一起。
总结:
用户提交请求创建pod,Kubernetes发现这个pod声明使用了PVC,那就靠PersistentVolumeController帮它找一个PV配对。没有现成的PV,就去找对应的StorageClass,帮它新创建一个PV,然后和PVC完成绑定。新创建的PV,还只是一个API 对象,需要经过“两阶段处理”变成宿主机上的“持久化 Volume”才真正有用:
第一阶段由运行在master上的AttachDetachController负责,为这个PV完成 Attach 操作,为宿主机挂载远程磁盘;
第二阶段是运行在每个节点上kubelet组件的内部,把第一步attach的远程磁盘 mount 到宿主机目录。这个控制循环叫VolumeManagerReconciler,运行在独立的Goroutine,不会阻塞kubelet主循环。
完成这两步,PV对应的“持久化 Volume”就准备好了,POD可以正常启动,将“持久化 Volume”挂载在容器内指定的路径。
Local Persistent Volume
本地持久化存储,必须具备数据备份和恢复的能力。在开始使用 Local Persistent Volume 之前,首先需要在集群里配置好磁盘或者块设备。
- 第一种,当然就是给宿主机挂载并格式化一个可用的本地磁盘,这也是最常规的操作;
- 第二种,对于实验环境,可以在宿主机上挂载几个 RAM Disk(内存盘)来模拟本地磁盘。
hostPath volume存在的问题
过去我们经常会通过hostPath volume
让Pod能够使用本地存储,将Node文件系统中的文件或者目录挂载到容器内,但是hostPath volume
的使用是很难受的,并不适合在生产环境中使用。
- 由于集群内每个节点的差异化,要使用hostPath Volume,我们需要通过NodeSelector等方式进行精确调度,这种事情多了,你就会不耐烦了。
- 注意DirectoryOrCreate和FileOrCreate两种类型的hostPath,当Node上没有对应的File/Directory时,你需要保证kubelet有在Node上Create File/Directory的权限。
- 另外,如果Node上的文件或目录是由root创建的,挂载到容器内之后,你通常还要保证容器内进程有权限对该文件或者目录进行写入,比如你需要以root用户启动进程并运行于privileged容器,或者你需要事先修改好Node上的文件权限配置。
- Scheduler并不会考虑hostPath volume的大小,hostPath也不能申明需要的storage size,这样调度时存储的考虑,就需要人为检查并保证。
- StatefulSet无法使用hostPath volume,已经写好的使用共享存储的Helm Chart不能兼容hostPath volume,需要修改的地方还不少,这也挺难受的。
local persistent volume工作机制
通常什么情况会使用Local PV呢?
- 比如节点上的目录数据是从远程的网络存储上挂载或者预先读取到本地的,为了能加速Pod读取这些数据的速度,相当于起Cache作用,这种情况下因为只读,不存在惧怕数据丢失。这种AI训练中存在需要重复利用并且训练数据巨大的时候可能会采取的方式。
- 如果本地节点上目录/磁盘实际是具有副本/分片机制的分布式存储(比如gluster, ceph等)挂载过来的,这种情况也可以使用local pv。
和HostPath Volume的区别
Local PV出现之前,使用本地磁盘的方法是HostPath Volume,同为使用本地磁盘,区别在哪呢?
- 最重要的区别,就是Local PV和具体节点是有关联的,这意味着使用了Local PV的pod,重启多次都会被Kubernetes scheduler调度到同一节点,而如果用的是HostPath Volume,每次重启都可能被Kubernetes scheduler调度到新的节点,然后使用同样的本地路径;
- 当我们要用HostPath Volume的时候,既可以在PVC声明,又可以直接写到Pod的配置中,但是Local PV只能在PVC声明,对于PV资源,通常都有专人管理,这样就避免了Pod开发者擅自使用本地磁盘带来的冲突和风险;
- 另外要注意的是,HostPath Volume和Local PV都是在使用本地磁盘,和常见的分布式文件系统相比,本地磁盘故障会导致数据丢失,保存重要数据请勿使用HostPath Volume和Local PV;
StorageClass 里的 volumeBindingMode=WaitForFirstConsumer 的含义,就是告诉 Kubernetes 里的 Volume 控制循环(“红娘”):虽然你已经发现这个 StorageClass 关联的 PVC 与 PV 可以绑定在一起,但请不要现在就执行绑定操作(即:设置 PVC 的 VolumeName 字段)。而要等到第一个声明使用该 PVC 的 Pod 出现在调度器之后,调度器再综合考虑所有的调度规则,当然也包括每个 PV 所在的节点位置,来统一决定,这个 Pod 声明的 PVC,到底应该跟哪个 PV 进行绑定。
所以,通过这个延迟绑定机制,原本实时发生的 PVC 和 PV 的绑定过程,就被延迟到了 Pod 第一次调度的时候在调度器中进行,从而保证了这个绑定结果不会影响 Pod 的正常调度。
Flexvolume 与CSI
无论是 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 项目里面剥离出来的那部分存储管理功能。
最右侧的部分,就是需要我们编写代码来实现的 CSI 插件。一个 CSI 插件只有一个二进制文件,但它会以 gRPC 的方式对外提供三个服务(gRPC Service),分别叫作:CSI Identity、CSI Controller 和 CSI Node。
其中,Driver Registrar 组件,负责将插件注册到 kubelet 里面(这可以类比为,将可执行文件放在插件目录下)。而在具体实现上,Driver Registrar 需要请求 CSI 插件的 Identity 服务来获取插件信息。
-
而External Provisioner 组件,负责的正是 Provision 阶段。在具体实现上,External Provisioner 监听(Watch)了 APIServer 里的 PVC 对象。当一个 PVC 被创建时,它就会调用 CSI Controller 的 CreateVolume 方法,为你创建对应 PV。
此外,如果你使用的存储是公有云提供的磁盘(或者块设备)的话,这一步就需要调用公有云(或者块设备服务)的 API 来创建这个 PV 所描述的磁盘(或者块设备)了。
最后一个External Attacher 组件,负责的正是“Attach 阶段”。在具体实现上,它监听了 APIServer 里 VolumeAttachment 对象的变化。VolumeAttachment 对象是 Kubernetes 确认一个 Volume 可以进入“Attach 阶段”的重要标志
- CSI 插件的 CSI Identity 服务,负责对外暴露这个插件本身的信息
- CSI Controller 服务,定义的则是对 CSI Volume(对应 Kubernetes 里的 PV)的管理接口,比如:创建和删除 CSI Volume、对 CSI Volume 进行 Attach/Dettach(在 CSI 里,这个操作被叫作 Publish/Unpublish),以及对 CSI Volume 进行 Snapshot 等
- CSI Volume 需要在宿主机上执行的操作,都定义在了 CSI Node 服务里面。
总结:相比于 FlexVolume,CSI 的设计思想,把插件的职责从“两阶段处理”,扩展成了 Provision、Attach 和 Mount 三个阶段。其中,Provision 等价于“创建磁盘”,Attach 等价于“挂载磁盘到虚拟机”,Mount 等价于“将该磁盘格式化后,挂载在 Volume 的宿主机目录上”。
有了 StorageClass,External Provisoner 就会为集群中新出现的 PVC 自动创建出 PV,然后调用 CSI 插件创建出这个 PV 对应的 Volume,这正是 CSI 体系中 Dynamic Provisioning 的实现方式。
部署 CSI 插件的常用原则是:
- 通过 DaemonSet 在每个节点上都启动一个 CSI 插件,来为 kubelet 提供 CSI Node 服务**。
- 通过 StatefulSet 在任意一个节点上再启动一个 CSI 插件,为 External Components 提供 CSI Controller 服务。
当用户创建了一个 PVC 之后,你前面部署的 StatefulSet 里的 External Provisioner 容器,就会监听到这个 PVC 的诞生,然后调用同一个 Pod 里的 CSI 插件的 CSI Controller 服务的 CreateVolume 方法,为你创建出对应的 PV。
这时候,运行在 Kubernetes Master 节点上的 Volume Controller,就会通过 PersistentVolumeController 控制循环,发现这对新创建出来的 PV 和 PVC,并且看到它们声明的是同一个 StorageClass。所以,它会把这一对 PV 和 PVC 绑定起来,使 PVC 进入 Bound 状态。
然后,用户创建了一个声明使用上述 PVC 的 Pod,并且这个 Pod 被调度器调度到了宿主机 A 上。这时候,Volume Controller 的 AttachDetachController 控制循环就会发现,上述 PVC 对应的 Volume,需要被 Attach 到宿主机 A 上。所以,AttachDetachController 会创建一个 VolumeAttachment 对象,这个对象携带了宿主机 A 和待处理的 Volume 的名字。
这样,StatefulSet 里的 External Attacher 容器,就会监听到这个 VolumeAttachment 对象的诞生。于是,它就会使用这个对象里的宿主机和 Volume 名字,调用同一个 Pod 里的 CSI 插件的 CSI Controller 服务的 ControllerPublishVolume 方法,完成“Attach 阶段”。
上述过程完成后,运行在宿主机 A 上的 kubelet,就会通过 VolumeManagerReconciler 控制循环,发现当前宿主机上有一个 Volume 对应的存储设备(比如磁盘)已经被 Attach 到了某个设备目录下。于是 kubelet 就会调用同一台宿主机上的 CSI 插件的 CSI Node 服务的 NodeStageVolume 和 NodePublishVolume 方法,完成这个 Volume 的“Mount 阶段”。
至此,一个完整的持久化 Volume 的创建和挂载流程就结束了。