标签:ace cti traffic 扩展模块 priority 定义 基础设施 模板 list
Kubernetes(k8s)是一款开源的优秀的容器编排调度系统,其本身也是一款分布式应用程序。虽然本系列文章讨论的是互联网架构,但是k8s的一些设计理念非常值得深思和借鉴,本人并非运维专家,本文尝试从自己看到的一些k8s的架构理念结合自己的理解来分析 k8s在稳定性、简单、可扩展性三个方面做的一些架构设计的考量。
我们知道,k8s定义了许多资源(比如Pod、Service、Deployment、ReplicaSet、StatefulSet、Job、CronJob
等),在管理资源的时候我们使用声明式的配置(JSON、YAML等)来对资源进行增删改查操作。我们提供的这些配置就是描述我们希望这些资源最终达成的一个目标状态,叫做Spec,k8s会对观察资源得到资源的状态,叫做Status,当Spec!=Status的时候,k8s的各种控制管理程序就会起作用,进行各种操作使得资源最终可以达到我们期望的Spec。这种声明式的管理方式和命令式管理方式相比,虽然没有后者这么直接,但是容错性会很强,后面一节会进一步详细提到这点。而且,这种管理方式非常的简洁,只要用户提供合适的Spec定义即可,并不需要对外暴露几十个几百个不同的API来实现对资源的各个方面做改变。当然,我们也可以灵活的对一些重要的动作单独开辟管理API(比如扩容,比如修改镜像),这些API底层做的操作就是修改Spec,底层是统一的。
在之前第一季的系列文章S1E2中,我分享过任务表的设计,其实这里的声明式对象管理就是类似这样的思想,我们在数据库中保存的是我们要的结果,然后由不同的任务Job来进行处理最终实现这样的结果(同时也会保存组件当前的状态到数据库),即使任务执行失败也无妨,后续的任务会继续重试,这种方式是可靠性最高的。
K8s使用的是声明式的管理方式,也就是水平触发。另一种做法是叫做命令式的管理,也就是边缘触发。比如我们在做支付系统,用户充值100元,提现100元然后又充值100元,对于命令式管理就是三条命令。如果提现请求丢失了,用户账户的余额就出错了,这肯定是不能接受的,命令式管理或边缘触发一定需要配合补偿。而声明式的管理就是告诉系统,用户在进行了三次操作后的余额分别是100、0和100,最终就是100,即使提现请求丢失了,最终用户的余额就是100。
来看下下图的例子,在网络良好的情况下,边缘触发没任何问题。我们进行了开、关、开三次操作,最后的状态是0。
在网络出现问题的时候,丢失了关这个操作,对于边缘触发,最终停留在了2这个错误的状态。对于水平触发没有这个问题,虽然当中有一段时间网络不好,状态错误停留在了1,但是网络恢复后我们马上可以感知到当前的状态应该是0,状态又能回到0,最终状态也能回到正确的1。试想一下,如果我们对我们的Pod进行扩容缩容,如果每次告知k8s应该增加或减少多少个Pod(的这种命令式方式),最终很可能因为网络问题,Pod的状态不是我们期望的。更好的做法是告诉k8s我们希望的状态,不管现在网络是否有问题,某个管理组件是否有问题,pod是否有问题,最终我们期望k8s帮我们调整到我们期望的状态,宁可慢也不要错。
(图来自这里)
我们知道etcd是基于Raft协议的分布式键值数据库/协调系统,本身推荐使用3、5、7这样奇数节点构成集群实现高可用。对于Master节点,我们可以在每一个节点都部署一个etcd,这样节点上的API Server可以和本地的etcd直接通讯,而API Server因为是轻(无)状态的,所以可以在之前使用负载均衡器做代理,不管是Node节点也好还是客户端也好都可以由负载均衡分发请求到合适的API Server上。对于类似于Job的Controller Manager以及Scheduler,显然不适合多个节点同时运行,所以它们都会采用抢占方式选举Leader,只有Leader能承担工作任务,Follower都处于待机状态。整体结构如下图所示:
我们可以想一下其它一些分布式系统的高可用方案,以及我们自己设计的系统的高可用方案,无非就是这三种大模式:
通过前面的介绍我们大概知道了k8s的一个设计原则是etcd会处于API Server之后,集群内的各种组件是无法直接和数据库对话的,不仅仅因为把数据库直接暴露给各组件会特别混乱,更重要的是谁都可以直接读写etcd会非常不安全,需要统一经过API Server做身份认证和鉴权等安全控制(后面我们会提到API Server的插件链)。
对于k8s集群内的各种资源,k8s的控制管理器和调度器需要感知到各种资源的状态变化(比如创建),然后根据变化事件履行自己的管理职责。考虑到解耦,显然这里有MQ的需求,各种管理组件可以监听各种资源的状态变化事件,不需要相互感知到对方的存在,自己做自己的事情即可。如果k8s还依赖一些消息中间件实现这个功能,那么整体的复杂度会上升,而且还需要对消息中间件进行一些安全方面的定制。
K8s给出的实现方式是仍然使用API Server来充当简单的消息总线的角色,所有的组件通过watch机制建立HTTP长链接来随时获悉自己感兴趣的资源的变化事件,完成自己的功能后还是调用API Server来写入我们组件新的Spec,这份Spec会被其它管理程序感知到并且进行处理。Watch的机制是推的机制,可以实时对变化进行处理,但是我们知道考虑到网络等各种因素,事件可能丢失,组件可能重启,这个时候我们需要推拉结合进行补偿,因此API Server还提供了List接口,用于在watch出现错误的时候或是组件重启的时候同步一次最新状态。通过推拉结合的list-watch机制满足了时效性需求和可靠性需求。
我们来看一下这个图,这个图展示了客户端创建一个Deployment后k8s大概的工作过程:
组件初始化阶段:
集群资源变更操作:
可以看到基于list-watch的API Server实现了简单可靠的消息总线的功能,基于资源消息的事件链,解耦了各组件之间的耦合,配合之前提到的基于声明式的对象管理又确保了管理稳定性。从层次上来说,master的组件都是控制面的组件,用来控制管理集群的状态,node的组件是执行面的组件,kubelet是一个无脑执行者的角色,它们的交流桥梁是API Server的各种事件,kubelet是无法感知到控制器的存在的。
如下图所示,API Server实现了基于插件+过滤器链的方式(比如我们熟知的Spring MVC的拦截器链)来实现资源管理操作的前置校验(身份认证、授权、准入等等)。
整个流程会有哪些环节呢:
如果是删除资源,还会有额外的一些环节:
对于复杂的流程式的操作,采用职责链+处理链+插件的方式来实现是很常见的做法。你可能会说这个API Server的设计总体上就不简单,怎么有这么多环节,其实这才是最简单的做法,每一个环节都有独立的插件来运作(插件可以独立更新升级,也可以根据需求动态插拔配置),每一个插件只是做自己应该做的事情,如果没有这样的设计,恐怕会出现1万行代码的一个大方法。
如图所示,类似于API Server的链式设计,Scheduler在做Pod调度算法的时候也采用了链式设计:
常见的predicate算法有:
常见的priority算法有:
比如我们在做类似路由系统这种业务系统的时候可以借鉴这种设计模式。简单一词在于每一个小组件简单,它们可以组合起来构成复杂的规则系统,这种设计比把所有逻辑堆在一起简单的多。
K8s的设计理念是类似Linux的分层架构:
之前介绍的一些组件大多数位于核心层和应用层。在更上层的管理层和接口层,我们往往会做更多的一些二次开发。在之前的文章中我也介绍过,对于复杂的微服务互联网系统,我们也应该把微服务进行分层,从下到上分为基础服务、业务服务、聚合业务服务等,每一层的服务聚合下层实现一些业务逻辑,不但可以做到服务重用,而且上层多变的业务服务的变动可以不影响下层基础设施的搭建。
除了k8s大量内部组件的实现使用了插件的架构,k8s在整体设计上就把核心和外部的一些资源和服务抽象为了统一的接口,可以插件方式插入具体的实现,如下图所示:
CNI、CSI、CRI我们比较熟悉了,其它更多的抽象接口这里就不描述了,k8s就像一个大主板,主板上有各种内存、CPU、IO、网络方面的接口,具体的实现k8s本身并不关心,用户和社区甚至可以根据的需要实现自己的插件。
我觉得这点是最了不起的最困难的,很多时候我们在设计一个系统的时候一开始是无法定义出抽象接口的,因为我们不知道将来会面对什么样的实现,只有到实现越来越多后我们才能抽象出接口才能制定标准。
K8s在存储方面的解耦设计特别值得一提。如下图所示,我们来看一下k8s在存储这块的解耦设计:
(图引自Kubernetes in Action一书)
我们要做的事情很明确,Pod需要绑定存储资源:
K8s中除了存储抽象的V、PV、PVC、SC,还有其它的一些组件也有类似层次的抽象以及动态绑定的理念。
我们在使用OO语言进行编程的时候,很自然知道我们需要先定义类,然后再实例化类来创建对象,如果类特别复杂(有不同的实现)的话,我们可能会使用工厂模式(或反射,外层传入目标类型名称)来创建对象。可以和k8s存储抽象比较一下,是不是这个意思,这其实就是一种解耦的方式,在架构设计中,甚至表结构设计中,我们完全可以引入类和实例的概念。比如工作流系统的工作流可以认为是一个类模板,每一次发起的工作流就是这个工作流的实例。
好了,本文大概窥探了一下k8s的架构,不知道你是否感受到了k8s的精良设计,对内考虑了高可用以及高可靠,对外考虑到了高可扩展性。几乎任何操作都允许失败,最终实现一致的状态,几乎任何组件都允许扩展和替换,让用户实现自己的定制需求。
如果你的业务系统也是一套复杂的资源协调系统(k8s抽象的是运维相关的资源,我们的业务系统可以抽象的是其它资源),那么k8s的设计理念有相当多的点可以借鉴。举一个例子,我们在做一套很复杂的流程引擎,我们就可以考虑:
朱晔的互联网架构实践心得S2E3:品味Kubernetes的设计理念
标签:ace cti traffic 扩展模块 priority 定义 基础设施 模板 list
原文地址:https://www.cnblogs.com/lovecindywang/p/10316898.html