我们知道 Kubernetes(以下简称“K8s”)中各种资源对象的数据是通过 K8s 的 API 进行提交并持久化到存储 etcd 中的(称为K8s对象),K8s 对象是使用 K8s 的接口,kubelet 客户端通过操作这些对象来使用K8s能力。
其中 kubectl 是我们使用最多的命令行工具。K8s官方对 kubectl 管理 K8s 对象的技术做了如下表的总结:
假如我们现在要在名为 test 的 namespace 下创建一个 Deployment 对象来运行 nginx 容器,接着对 nginx 的副本数量进行更改为5,三种方式流程如下:
-
指令式命令方式
创建 deployment 对象:
kubectl create deployment nginx-deployment --image nginx -n test
更改副本数:
kubectl scale deployment nginx-deployment --replicas 5 -n test
-
指令式对象配置方式
首先创建配置文件定义,定义如图1-1所示:
图1-1 ngin.yaml配置
创建 deployment 对象:
kubectl create -f nginx.yaml
将配置文件中的replicas由2改成5,执行命令更改副本数:
kubectl replace -f nginx.yaml
-
声明式对象配置方式
使用图1-1的配置,创建deployment对象:
kubectl apply -f nginx.yaml
将配置文件中的replicas由2改成5,执行命令更改副本数:
kubectl apply -f nginx.yaml
后两者对象配置的方式提供了用于创建新对象的模板,并且能够提供与更改关联的审核跟踪,命令除了实时内容外,不能提供记录源。所以在生产项目的情况下,我们使用 K8s 对象的方式往往是通过编写对应的.yaml
文件交给 K8s(声明式API的形式),而不是直接使用指令式命令。
我们配置文件只是声明了想要创建的K8s对象的信息以及想要它们达到的期望状态,这是一种声明式API的交互方式。不同于吃饭、睡觉等明确的指令(这种方式也称为:命令式API),使用配置创建的对象的交互感觉类似顾客与实施方的交流模式,因为顾客跟实施方相比,往往不清楚为了达到目标而需要进行的操作,比如说:给我搭建一个标准的篮球场。
声明式API往往预备了合并多个操作的能力,我们在搭建球场时可以同时进行地板的铺设和大屏幕的安装,命令式API在并发访问情况下会十分复杂、低效,往往通过加锁才能保证最后结果的可预见性,孩子必须吃完了饭才能睡觉,而不是睡觉的时候吃饭。并且声明式API天然地记录了现在和最终状态,便于对结果进行检查。每天必须吃3碗饭和保持体重在50kg,不同体重的人吃三碗饭导致的体重结果是无法估计的,不同的人但却可以通过调整热量的摄入而达到50kg的目标。这也是为什么K8s中声明式API如此重要的原因,而K8s声明式API描述的主体便是K8s对象,正如开头所说它是我们操作K8s的媒介和接口。
那么K8s对象在K8s API中是如何表示的,我们怎么用.yaml
文件描述它们,当把一个.yaml
文件提交给 K8s后,它究竟又是如何创建出一个K8s对象的呢?
1、K8s对象
K8s对象指的是K8s系统中持久化后的实体,K8s用这些实体去表示整个集群的运行状态。它们特别描述了以下信息:
-
哪些容器化应用在运行,以及运行在哪些节点上
-
可以被应用使用的资源
-
应用运行时的表现策略,如重启策略、升级策略以及容错策略。
K8s对象的操作(创建、修改、删除)都需要使用K8s API,无论是使用kubectl命令行还是程序中使用客户端,最终都会调用K8s API。
每个K8s对象都包含两个嵌套的对象字段 spec(规约)和 status(状态)。其中spec 描述的是 K8s 在创建完毕后应该达到的期望状态,如期望开启多少个副本,status 描述了对象的当前状态,它由 K8s 系统和组件进行监管设置并积极地使它与spec 相匹配。这两个字段也是对于 K8s 声明式 API 实现最为关键的一环。
2、K8s是如何描述和管理API对象的
我们在创建 K8s 对象时,必须提供对象的spec字段,告知K8s我们期望这个对象所达到状态,同时也必须提供一些关于对象的基本信息(例如名称)。不论我们是通过kubectl 命令行或者是客户端调用K8s API创建对象,API 请求必须在请求体中包含JSON 格式的信息。
大多数情况下,我们都以 .yaml
文件的形式提供这些信息。kubectl 在发起API请求时,再将这些信息转换成 JSON 格式。以图1-1举例,图中展示了使用.yaml
来描述K8s对象的必需字段和对象规约,以下是其中必需字段的简要说明:
-
apiVersion - 创建该对象所使用的K8s API版本
-
kind - 想要创建的对象类别
-
metadata – 帮助唯一性表示对象的一些数据,包括一个name字符串、UID和可选的namespace
-
spec – 你期望该对象所达到的状态
其中 spec 的精确格式对于不同类别的 K8s 对象来说往往是不同的,它是对管理对象详细描述的主体,会被 K8s 持久化到 etcd 中保存。如果 spec
被删除,那么该对象也会从系统中被删除。
一个API对象在etcd里的完整资源路径是由Group(API组)、Version(API版本)、Resource(API资源类型)三个部分组成。具体案例如图1-2所示:
图1-2 K8s的API结构
从图中我们可以看出,在 K8s 内 API 对象的组织方式是层层递进的,这里我们不去深究具体怎么使用这些 api 去检索需要的资源,感兴趣的读者可以参考如下链接地址,这是官方的详细说明:
https://kubernetes.io/zh/docs/reference/using-api/api-concepts/
假定现在我们要创建一个 CronJob
对象,且它的 yaml 文件开头部分如下所示:
apiVersion: batch/v2alpha1
kind: CronJob
...
其中,batch
就表示它的 API 组(Group),v2alpha1
就是它的API版本(Version),CronJob
即是它所属 API 资源类型(Resource)conjobs
下的一个具体类别。而其整体创建流程下图1-3所示:
图1-3 持久化流程
➤ 提交阶段
当我们的 yaml 提交给 K8s 集群后(不论是通过程序客户端还是kubectl),请求会被转化成一个POST,接着交由K8s的API Server进行处理。
➤ 过滤和前置处理阶段
API Server首先会对请求进行过滤,获取其中的有效信息,再完成诸如:授权、超时处理、审计等前置性工作。
➤ 路由查找阶段
主要是借由 MUX 和 Routes 路由查找 CronJob 的定义,具体流程大致为:
-
匹配Group。对于 CronJob 这类非核心 API 对象,K8s 会在如图1-2所示的
/apis
层级内查找它的所属组(核心对象,如 Pod、Node,隶属于 /api 层级且它们的组为""),根据yaml
的开头信息,查找到/apis/batch
。 -
匹配版本号。接着,根据
v2alpha1
这个版本号进一步匹配到/apis/batch/ v2alpha1
这个路径,在 K8s 中,同种 API 对象可以有多个版本,这是 K8s 给予用户管理共存多版本 API 的手段。 -
匹配资源类型。最后匹配资源类型cronjobs,这时 K8s 便可明确知道自己所需要创建的具体资源类型为CronJob。
➤ 创建资源阶段
在获取到 CornJob 的类型定义后,API Server 会进行一个 Convert 工作:把用户提交的 yaml 转换成一个 Super Version 的对象,它是该 API 资源类型所有版本的字段全集,之后用户提交其他版本的 yaml,也都可以用这个 Super Version 对象来处理。
接着,再进行 Admit 处理,主要是在对象创建完成后,注入一些公共处理逻辑,例如加入一些标签 Label。最后,对这个对象里各个字段的合法性进行校验。
➤ 定义存储阶段
若通过了 Validate,会被 API Server 保存在名为 Registry 的结构中。它包含了对一个有效 K8s API 对象的定义。
➤ 持久化阶段
API Server会把通过验证的API对象由Super Version版本重新转换为最初提交的版本,再进行序列化操作,最后调用etcd的API将它们变为K8s内的实体对象。
此时,对象已经被创建持久化完成,那么它又是如何一步步达到我们所期望的状态的呢?K8s对象章节所提及到的spec和status信息正是这个解决问题的基础,K8s基于此采用了控制器模式来进行实现。
3、控制器模式实现声明式API的sepc
K8s是通过控制器模式来实现声明式API中描述的spec状态靠拢的,而控制型模式最核心的就是“控制循环“的概念。在K8s中,控制器是一个控制循环,它通过API服务器监视集群的共享状态,并进行更改,试图将当前状态(status)转移到期望状态(spec)。
实际状态的获取往往是通过K8s本身的一些组件,例如定时调用API获取资源状态信息,或kubelet通过心跳汇报的容器状态和节点状态。所需期望状态,往往由yaml文件的spec字段进行定义提交持久化到K8s的etcd中。图1-4大致表述了K8s中控制循环的过程:
图1-4控制循环
-
比较spec和status,若相同,不进行操作,继续进行1,否则输出不同点,进入2;
-
控制器(Controller)根据不同点,对系统发出调控操作,系统执行操作,进入3;
-
系统上报当前系统状态给传感器(Sensor)或者传感器主动拉取状态,输出当前状态status,返回1。
目的是使 status 不断趋近达到 spec,从而完成 yaml 声明状态的实现。从图中也可以看出 Sensor 和 Controller 在整个循环中扮演了极其重要的角色。
在 K8s 中,Sensor 主要由Reflector
、Informer
、Indexer
三个组件构成。
Sensor 的整体工作流程如图1-5所示:
图1-5 Sensor组件
-
Reflector通过List和Watch操作从API Server 获取各个资源对象的状态。其中,List用来描述返回多个资源的集合,主要用于系统资源的全量更新;Watch用于客户端获取当前状态,然后订阅后续更改,主要用于系统资源的增量更新;
-
Reflector在获取新的资源数据后,会往Delta队列中推入一个Delta记录,这个记录包含了资源对象本身的信息以及资源对象事件的类型。Delta队列可以保证同一个对象在队列中仅有一条记录,从而避免Reflector重新List和Watch的时候产生重复的记录。
-
Informer组件不断地从Delta队列中弹出delta记录,然后把资源对象交给indexer。indexer把资源记录在一个默认使用命名空间做索引的缓存中,记录操作是线程安全的并且缓存是可以在Controller Manager或多个Controller间共享的。之后,informer再把这个事件交给事件回调函数。
Controller的主要流程如图1-6所示:
图1-6 controller组件
控制循环中的Controller(控制器)组件主要由事件处理函数以及worker组成,事件处理函数根据控制器逻辑决定是否要进行相应的回调处理。若需要进行事件处理,它会把事件关联资源的命名空间和资源名字当成一个标识塞入工作队列中,由后续worker池中的一个worker来处理,工作队列会对存储的对象进行去重,从而避免多个woker处理同一个资源的情况。
worker 在处理资源对象时,一般需要用资源的名字来重新获得最新的资源数据,之后使用这些数据会有三种情况:
-
创建或者更新资源对象
-
调用其他的外部服务
-
worker处理失败,把资源的名字重新加入到工作队列中进行重试
现在假如我们需要将图1-1所配置的nginx副本由2扩容为3个进行举例说明,方便读者理解,整体流程如图1-7所示:
图1-7副本扩展整体流程图
首先,Reflector会watch到Deployment发生变化,在delta队列中塞入了对象是nginx-deployment并且类型是更新的记录。
Informer一方面把新的Deployment更新到缓存中,并用名为test的namespace作为索引。另外一方面,调用Update的回调函数,Deployment控制器发现Deployment发生变化后会把字符串test/nginx-deployment塞入到工作队列中,工作队列后的一个 worker从工作队列中取到了test/nginx-deployment这个字符串的key,并且从缓存中取到了最新的Deployment数据。
worker通过比较Deployment中spec和status里的数值,发现需要对这个Deployment进行扩容,因此Deployment的worker创建了一个Pod,这个pod中的Ownereference指向了nginx-deployment。
然后Reflector Watch到的Pod新增事件,在delta队列中额外加入了Add类型的deta记录。
Informer一方面把新的Pod记录通过Indexer存储到了缓存中,另一方面调用了 Deployment控制器的Add回调函数,Add回调函数通过检查pod3的ownerReferences 找到了对应的Deployment为nginx-deployment,把test/nginx-deployment字符串塞入到了工作队列中。
Deployment的woker在得到新的工作项之后,从缓存中取到新的Deployment记录,并得到了其所有的Pod信息,对比发现Deployment的状态不是最新的(关联的pod数量已经发生改变)。此时 Deployment更新 status使得spec和 status达成一致。
总结
-
由于声明式API具有天然的记录初始和最终状态等特性,K8s采用了其作为最关键部分的API交互方式。
-
在生产环境中,对于K8s的管理往往是通过管理器API对象,而API对象又是通过yaml进行描述的,其中的spec和status是最为关键的信息,K8s会通过定位和路由把一个yaml描述的信息持久化到etcd中成为实体。
-
K8s所采用的控制器模式,是由声明式API驱动的。它是实现K8s声明式API的关键,资源对应的控制器结合感应器通过控制循环,异步地将控制系统向设置的终态趋近。
-
因为控制器是自主运行的,使得整体系统的自动化和无人值守成为可能。