架构设计系列文章,请参见连接。
因为Kubernetes字符太长本文中将Kubernetes简写为K8s。
0. 背景
0.1 为什么K8s战胜了Swarm、Mesos
从使用上来说以声明式API来降低运维的操作成本。在生态系统建设方面以极高的可扩展性来提升社区活跃度。从这两个方面既可以填充K8s的不足,也极大的简化了运维操作过程。
0.2 架构侧面
在K8s的各种文档、书籍中都没有从架构方面说明K8s的架构层面为什么是好的架构设计。
本文主要讨论K8s在架构层面上的一些内容,下面逐步的进行细化讨论。
1. K8s简述
本章通过对K8s内部原理的说明来对K8s有一个基础认知,来展示一些K8s的架构特种在后面对架构的分析与说明奠定基础。
在Ops的业务中有几项:
- 环境初始化:操作系统安装、运行环境安装、存储挂载、网络划分等等。
- 配置管理:根据运维配置,进行服务的配置。包括:副本数,可靠性保证,指标等
- 运行服务:选择运行环境进行服务配置与服务启动等
- 监控与升级:监控服务检查是否超过阈值进行相关的扩缩容,服务的升级工作等。
K8s主要解决的就是在Ops中的业务。以不可变基础设施来解决运行环境、配置管理、运行服务的问题。以声明式API来解决运维标准化的问题。
不可变基础设施是结果,而不是设计
基础设施的标准化问题在Ansible中是通过playbook来完成的,而K8s使用容器镜像做为基础设施的标准化。这其中的最大区别在于Ansible可以帮助进行标准化运行环境,而K8s中的容器镜像中包含的内容有运行环境,服务配置,服务,监控等。这里从业务层面上来说,K8s提供了镜像的版本而Ansible提供的是基础运行环境的运行以及部署能力。
Ansible为服务配置、服务升级、运行环境的管理比K8s更为灵活,可以通过不同的playbook的配置进行处理。而K8s对于运行环境、服务配置的管理是不足的为了简化这部分的管理复杂度。这部分工作都通过容器镜像来进行管理。所以作者认为不可变基础设施是结果,而不是设计。声明式API解决运维自动化、标准化问题
面向终态的声明式API解决了运维工作中的一个重要工作:自动化、标准化。自动化、标准化、可视化、智能化是运维管理中的重要目标。使用声明式API将服务的定义都抽象出来、标准化的定义服务,就解决了运维标准化问题。
1.1 用户概念
K8s的核心概念以及之间的关系。这里的概念都是给用户来操作、管理K8s中的对象所使用的。在K8s的使用过程中是理解这些概念并了解作用原理。
1.2 控制面过程
控制面包括的业务有定义转换、选择节点、部署服务、通信控制、节点管理、服务监控、权限控制等。而这些业务基本上都落在apiserver,controller,scheduler,kubelet,proxy组件之上。他们的关系如下图所示:
业务在组件之间的控制流的交织形成了K8s的控制面。本节主要讨论控制面中几个较为有名的过程。
1.2.1 资源过程
在资源过程中主要描述的是资源的下发过程。资源下发过程中以API Server为中心完整触发、转换、调度、启动等过程。从图中可以看到各个组件都以List-Watch的方式进行触发和处理过程的管理工作。
1.2.2 节点过程
这里展示的只是Kubelet中的SyncLoop过程,而Kubelet中的PLEG、自动过程、Informer、垃圾回收过程等都与syncLoop相关。Kubelet 的工作主要是围绕一个 SyncLoop 来展开,借助 go channel,各组件监听 loop 消费事件,或者往里面生产 pod 相关的事件,整个控制循环由事件驱动运行。可以用下图来表示:
- 用户从http,静态文件以及APIServer对pod的修改通过PodConfigchannel传递到syncLoop;
- 另外一方面,PLEG会周期(默认1s)通过relist从CRI获取所有pod当前状态并且跟之前状态对比产生Pod的event发送到syncLoop;
- syncLoop的syncLoopIteration从各种chan中取出update的内容,一方面会通过podManger里更新pod状态,另一方面会通过dispatchWork将更新内容通过PodWoker更新pod状态,调用的是syncPod这个接口(由Kubelet.syncPod实现);
- 而syncPod这里通过podStatusChannel 更新状态到statusManager, 再patch Status到APIServer;
- syncPod一方面通过containerManager更新non-runtime的信息,例如QoS,Cgroup信息;另外一方面通过CRI更新pod的状态;
1.2.3 资源调度过程
k8s中的Scheduler Framework的设计。其中核心包括:Filter,Score,Bind。
1.2.4 总结
在K8s中控制面还有很多核心过程,但因为篇幅所限这里无法讨论所有的控制面板过程。
1.3 数据面过程
数据面最主要的是CNI。在CNCF中有多种类型的网络插件,都是实现了CNI的组件。在K8s中的网络也是从iptables演进到IPVS来的过程。都是由Proxy服务负责的。
1.4 总结
架构设计过程:整体架构->核心流程设计。这里整理K8s架构的时候也是以类似的方式进行。
2. K8s架构
这里的K8s架构都是从K8s中逆向工程出来的。可能很多都不能反应K8s在设计过程和设计结果中内容,不过从作者看到的内容来说已经充分的体现了K8s架构的优点。按照作者总结的K8s架构:以控制环路的风格构建起来的C/S形式的微服务。从总结出的K8s架构就可以看出K8s使用了多种架构风格与模式处理在K8s不同的功能点的设计。架构风格的使用是对于架构结构(参照:《软件架构:架构模式、特征及实践指南》)的定义,而架构结构背后的对于架构模式的选择的原则以及决策过程才是支撑K8s的架构好坏的原因。所以下面会以架构风格的讨论引出架构原则与决策的讨论。
2.1 架构风格与模式
这里以抽象的方式总结出K8s中的涉及到的架构风格、架构模式和设计模式的内容。
微内核架构风格
微内核架构风格最大的特点就是插件。一般的微内核模式都会以定义接口的形式来进行扩展点、扩展方式、扩展点结果形式等的定义来描述一个扩展点,但是K8s不同之处在于Watch的方式进行扩展,这样就可以以最少接口定义的方式支持最多的扩展点。
这样做即提高了系统的整体可扩展性,又提高了系统的性能与稳定性。因为以事件驱动的方式来处理业务比顺序执行的方式肯定要快,并Watch在不同的进程中执行就算客户端进程退出也不会影响API Server的正常运行。从设计模式的开闭原则来看Informer即实现了对扩展的开发,有实现了对修改的关闭。可以通过实现Controller Manager的方式对业务进行扩展,又不允许对数据的存储以及Watch机制进行修改。
Scheduler Framework(以下简称SF)的扩展方式与Controller的扩展方式不一致,也与上面描述的微内核架构的扩展方式也不同。SF是提供了扩展点,但是扩展内容必须和Scheduler编译在一起。而作者认为使用dlopen去做标准的微内核方式也不失为一种更好的扩展方式。事件驱动风格(brokers)
ListAndWatch机制和Informer机制是K8s中的事件驱动风格的一种体现。这两种机制为事件驱动的一个长期争论的问题:事件中应该带多少信息?给出一种解决方案:边沿触发,水平驱动。触发事件带索引性信息,在事件处理中去根据索引查询完整信息。以这样的方式解决并发与实践传输信息量之间的关系。
另外,在K8s中Event也可以认为是一种记录型的事件驱动机制。不过在K8s中主要以日志的方式使用Event概念。控制环路架构风格
控制环路一般在IoT系统中较为常见,因为在IoT系统中需要不断的采集设备运行数据并根据设备运行数据进行相应的以命令的形式控制设备的行为。在K8是中的Controller Manager就是通过这种机制来控制Pod的状态的。
资源的状态发生变更之后,由Controller进行相关的状态变更动作。黑板架构风格
API Server和Etcd即是黑板架构风格中的中心节点。Controller、Schedule、Kubelet、Proxy等都是通过API Server的Informer进行数据的访问与操作的,所以API Server是K8s其他组件的知识源,数据结构就是K8s中定义的各种资源对象。由API Server负责维护K8s中的数据,并以事件的方式通知各组件数据状态的变更。管道过滤器风格
虽然不明显但是有很多内容。像Scheduler Framework就是一个典型的管道过滤器风格的。但在作者去查看Scheduler Framework的代码时发现不是这么实现的。微服务架构风格
微服务四原则:RESTFull,无状态服务,前后端分离,AKF。K8s中的微服务拆分风格不是DDD或者分层的方式进行的,它是以事件驱动为核心拆分的微服务。所以,K8s中的微服务的划分规则与平常业务中的微服务划分规则有明显的区别。例如:api server即使BFF也是数据存储控制,还负责事件的转发。而其他的组件基本上都是以事件机制作为业务出发条件进行业务的处理工作。CQRS架构风格
Informer和Queue机制代表着读和写的过程。虽然是小范围的命令查询职责分离模式。解释器设计模式
使用编译原理,将操作解析为原子操作,并进行执行动作。原子操作执行队列进行命令执行。
2.2 架构原则与ARD
上一节中说到K8s的架构与很多先行的微服务架构不一样的特点,也说明了这些特点的原因以及考虑点。这里就说明K8s中一些实现过程中的原则。但是K8s现在由CNCF社区进行维护,能不能遵循一些既定的原则是不被保证的。所以,这里只能说是K8s在实现之初遵循的一些架构原则。
2.2.1 架构原则
以DDD中的战略设计的统一语言来定义原则。例如:Event和ListAndWatch的区别。顶层Event对象是一种资源,Event对象时一种用户可见的日志机制。Watch事件在API服务器与控制器之间通过HTTP流的方式发送用于驱动Informer。
2.2.2 架构决策(ARD)
架构决策定义一组关于如何构建系统的规则。这里只记录Kubernetes架构中的少数几个决策:事件处理,事务模型,数据操作。
- 事件处理
事件处理机制决策:边沿触发、水平驱动+重新同步
备选方案:
- 在仅仅依赖边沿驱动的场景下,有可能会丢失一个后续事件。
- 在边沿触发的场景下,处理事件时总是重新获取最新的状态(即水平)。也可以说业务逻辑是边沿触发、水平驱动的。
- 边沿触发加上水平驱动的逻辑,同时服务已额外的重新同步逻辑。
决策:
边沿触发、水平驱动+重新同步
- 事务模型
事务模型机制决策:乐观并发
备选方案:
- 无锁
- 乐观
- 悲观
决策:
- 为了实现无锁的并发操作,Kubernetes API服务器使用乐观并发模型。
- 在内嵌的ObjectMeta结构中包含了一个资源版本
关联决策:
- 水平驱动的逻辑,可以使用水平驱动逻辑进行进行乐观重试
- 数据操作
数据操作机制决策:只有API Server操作
备选方案:
- API Server,Controller可以直接操作数据
- Controller可以操作数据
- API Server可以操作数据
2.3 设计结果
设计结果以4+1视图的方式进行说明。Kubernetes在每个视图中都有它独有的特点。
3. 总结
K8s是由开源社区维护的,那随着不断的演进肯定会朝着好的方向前进。但在发展过程中,代码的水平设计的水平都会出现参差不齐的情况。在K8s中有好的有不好的,我们只需要取其精华,去其糟粕即可。
3.1 优缺点
- K8s的对象描述不能满足架构即代码的要求
没有描述组件之间的关系,没有描述组件边界。 - 调用链的管理是不完善的
因为使用的并非分层微服务的模式对服务进行管理,所以在服务的管理如调用链的管理工作就不是很完善。 - 故障的隔离方式值得借鉴
K8s的核心过程是List-Watch的,所以,服务之间的关系都是间接的,没有任何一个服务是直接关系。这样就不会形成惊群或雪崩问题。 - Yaml Schema和XML Schema
为了规范yaml配置中的输入值,引入的Yaml Schema机制。其实也带来的一定的复杂度,不过有代码生成器可以生成Yaml Schema的内容。
5. 参考
Kubernetes 编程基础知识
一文梳理REST API的设计原则
在 Kubernetes 上开发
最新、最全、最详细的 K8S 学习笔记总结(2021最新版)!建议收藏
一文看懂 Kubelet
调度框架