Kubernetes 控制型模式
Kubernetes 作为成熟的容器编排系统,已经作为各大云厂商的标准服务,为软件企业提供了灰度发布、蓝绿发布等功能,使用了当前企业应用软件快速迭代,快速发布的节奏。支持企业应用的弹性伸缩,自动重启,保证高可用的服务;提供完整的应用管理、网络管理、存储管理解决方案,大大减少企业软件应用的运维成本,Kubernetes如何能够实现最大单个集群能够达到5000个节点,15万个Pod的完全自动化管理,更多归功于声明式API编程与控制器模式的协作。在Kubernetes 中运行了一系列控制器来确保集群的当前状态与期望状态保持一致,它们就是 Kubernetes集群内部的管理控制中心或者说是”中心大脑”。例如,ReplicaSet控制器负责维护集群中运行的 Pod 数量;Node控制器负责监控节点的状态,并在节点出现故障时,执行自动化修复流程,确保集群始终处于预期的工作状态。
控制循环原理
在机器人设计和自动化领域,循环控制是一个控制系统状态的不终止的循环。控制型模式最核心的就是控制循环的概念,包含控制器、被控制的系统和观测系统的传感器三个逻辑组件,外界通过控制被控系统的期望(spec),来声明系统要达到什么样的状态,通过观测传感器来上报当前系统的状态(status),控制器通过比较系统的期望(spec)与传感器的状态(status)的差值,最终来达到控制系统做什么事情能够使系统向 spec 表示终态趋近。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U9cbh5pk-1619690365859)(/Users/zhaobei/Library/Application Support/typora-user-images/image-20210429141335550.png)]
控制型模式核心的三个逻辑组件
传感器(Sensor)
控制循环中逻辑的传感器主要有 Reflector、Informer、Indexer 三个组件构成。Reflector 通过List 和 Watch k8s server 来获取资源的数据,List 用来在Controller 重启以及 Watch 终端的情况下,进行系统资源的全量更新,而 watch 则在多次 list 之间进行增量的资源更新, Reflector 在获取新的资源数据后,会在 D delta 队列中塞入一个包括资源对象信息本身以及资源对象事件类型的Delta记录,D elta 队列中可以保证同一个对象在队列中仅有一条记录,从而避免 Reflector 重新 List 和 watch 的时候产生重复的记录; Informer 组件不断的从 Delta 队列中弹出 delta 记录,然后把资源怼系那个倨傲给 indexer, 让那个 indexer 把资源记录在一个缓存中,,缓存在默认设置下是用资源的命名空间来做索引,并且可以被 Controller Manager 或多个 Controller 所共享。之后,再把这个事件交给事件的回调函数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-If0fQYbg-1619690365862)(/Users/zhaobei/Library/Application Support/typora-user-images/image-20210429142439542.png)]
控制循环中的控制器组件主要由事件处理以及 worker 组成,时间处理函数之间会相互关注资源的新增、更新、删除的事件,并根据控制器的逻辑去决定是否需要处理,对需要处理的事件,会把事件关联的资源的命名空间以及名字塞入一个工作队列中,并且由后续的worker池中的一个worker 来处理,工作队列会对存储的对象进行驱虫,从而避免多个worker 处理同一个资源的情况Worker 在处理资源对象时,一般需要用资源的名字来重新获得最新的资源数据,用来创建或者更新资源对象,或者调用其他的外部服务,Worker 如果处理失败的时候,一般情况下会把资源的名字重新加入到工作队列中,从而方便之后进行重试。
控制循环例子-扩容
然后通过一个例子来结合刚才文章里阐述的控制循环的工作原理, Deployment 是一个用来描述无状态应用的控制器,我们使用 deploy 通过 selector 来匹配所关联的 Pod,在yaml中描述出此控制产生pod 的状态数量,先上 yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-nginx
spec:
selector:
matchLabels:
app: my-nginx
replicas: 2
template:
metadata:
labels:
app: my-nginx
spec:
containers:
- name: my-nginx
image: nginx
1、创建 pod
kubectl apply -f nginx.yaml
效果如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-25Pp5rs4-1619690365865)(/Users/zhaobei/Library/Application Support/typora-user-images/image-20210429144251343.png)]
2、replicas 从 2 被改到 3
kubectl edit deploy my-nginx
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bKPBH0bu-1619690365866)(/Users/zhaobei/Library/Application Support/typora-user-images/image-20210429144403249.png)]
首先,Reflector 会 watch 到 Deployment 和 Pod 两种资源的变化,发现Deployment 发生变化后,在 delta 队列中塞入了对象是 rsA,而且类型是更新的记录。Informer 一方面把新的 Deployment 更新到缓存中,并与 Namespace nsA 作为索引。另外一方面,调用 Update 的回调函数,Deployment 控制器发现 Deployment 发生变化后会把字符串的 nsA/rsA 字符串塞入到工作队列中,工作队列后的一个 Worker 从工作队列中取到了 nsA/rsA 这个字符串的 key,并且从缓存中取到了最新的 Deployment 数据。Worker 通过比较 Deployment 中 spec 和 status 里的数值,发现需要对这个 Deployment 进行扩容,因此 Deployment 的 Worker 创建了一个 Pod,这个 pod 中的 Ownereference 取向了 Deployment rsA。
然后 Reflector Watch 到的 Pod 新增事件,在 delta 队列中额外加入了 Add 类型的 deta 记录,一方面把新的 Pod 记录通过 Indexer 存储到了缓存中,另一方面调用了 Deployment 控制器的 Add 回调函数,Add 回调函数通过检查 pod ownerReferences 找到了对应的 ReplicaSet,并把包括 Deployment 命名空间和字符串塞入到了工作队列中。Deployment 的 Woker 在得到新的工作项之后,从缓存中取到了新的 Deployment 记录,并得到了其所有创建的 Pod,因为 Deployment 的状态不是最新的,也就是所有创建 Pod 的数量不是最新的。因此在此时 Deployment 更新 status 使得 spec 和 status 达成一致。
控制器模式总结
1、两种 API 设计方法
Kubernetes 控制器模式依赖生命式API,另外一种常见的 API 类型是命令式APi,为什么 Kubernetes 采用声明式 API,而不是命令式 API 来设计整个控制器呢?
首先,比较两种 API 在交互行为上的差别。在生活中,常见的命令式的交互方式是家长和孩子交流方式,因为孩子欠缺目标意识,无法理解家长期望,家长往往通过一些命令,教孩子一些明确的动作,比如说:吃饭、睡觉类似的命令。我们在容器编排体系中,命令式 API 就是通过向系统发出明确的操作来执行的。
而常见的声明式交互方式,就是老板对自己员工的交流方式。老板一般不会给自己的员工下很明确的决定,实际上可能老板对于要操作的事情本身,还不如员工清楚。因此,老板通过给员工设置可量化的业务目标的方式,来发挥员工自身的主观能动性。比如说,老板会要求某个产品的市场占有率达到 80%,而不会指出要达到这个市场占有率,要做的具体操作细节。
类似的,在容器编排体系中,我们可以执行一个应用实例副本数保持在 3 个,而不用明确的去扩容 Pod 或是删除已有的 Pod,来保证副本数在三个。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZjZuUKxU-1619690365867)(/Users/zhaobei/Library/Application Support/typora-user-images/image-20210429145542297.png)]
2、命令式 API 的问题
在理解两个交互 API 的差别后,可以分析一下命令式 API 的问题。
- 命令 API 最大的一个问题在于错误处理;
在大规模的分布式系统中,错误是无处不在的。一旦发出的命令没有响应,调用方只能通过反复重试的方式来试图恢复错误,然而盲目的重试可能会带来更大的问题。
假设原来的命令,后台实际上已经执行完成了,重试后又多执行了一个重试的命令操作。为了避免重试的问题,系统往往还需要在执行命令前,先记录一下需要执行的命令,并且在重启等场景下,重做待执行的命令,而且在执行的过程中,还需要考虑多个命令的先后顺序、覆盖关系等等一些复杂的逻辑情况。
- 实际上许多命令式的交互系统后台往往还会做一个巡检的系统,用来修正命令处理超时、重试等一些场景造成数据不一致的问题;
然而,因为巡检逻辑和日常操作逻辑是不一样的,往往在测试上覆盖不够,在错误处理上不够严谨,具有很大的操作风险,因此往往很多巡检系统都是人工来触发的。
- 最后,命令式 API 在处理多并发访问时,也很容易出现问题;
假如有多方并发的对一个资源请求进行操作,并且一旦其中有操作出现了错误,就需要重试。那么最后哪一个操作生效了,就很难确认,也无法保证。很多命令式系统往往在操作前会对系统进行加锁,从而保证整个系统最后生效行为的可预见性,但是加锁行为会降低整个系统的操作执行效率。
- 相对的,声明式 API 系统里天然地记录了系统现在和最终的状态。
不需要额外的操作数据。另外因为状态的幂等性,可以在任意时刻反复操作。在声明式系统运行的方式里,正常的操作实际上就是对资源状态的巡检,不需要额外开发巡检系统,系统的运行逻辑也能够在日常的运行中得到测试和锤炼,因此整个操作的稳定性能够得到保证。
最后,因为资源的最终状态是明确的,我们可以合并多次对状态的修改。可以不需要加锁,就支持多方的并发访问。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nprix7xO-1619690365869)(/Users/zhaobei/Library/Application Support/typora-user-images/image-20210429151106909.png)]
3、控制器模式总结
最后我们总结一下:
- Kubernetes 所采用的控制器模式,是由声明式 API 驱动的。确切来说,是基于对 Kubernetes 资源对象的修改来驱动的;
- Kubernetes 资源之后,是关注该资源的控制器。这些控制器将异步的控制系统向设置的终态驱近;
- 这些控制器是自主运行的,使得系统的自动化和无人值守成为可能;
- 因为 Kubernetes 的控制器和资源都是可以自定义的,因此可以方便的扩展控制器模式。特别是对于有状态应用,我们往往通过自定义资源和控制器的方式,来自动化运维操作。这个也就是后续会介绍的 operator 的场景。