朱晔的互联网架构实践心得S2E3:品味Kubernetes的设计理念

Kubernetes(k8s)是一款开源的优秀的容器编排调度系统,其自己也是一款分布式应用程序。虽然本系列文章讨论的是互联网架构,可是k8s的一些设计理念很是值得深思和借鉴,本人并不是运维专家,本文尝试从本身看到的一些k8s的架构理念结合本身的理解来分析 k8s在稳定性、简单、可扩展性三个方面作的一些架构设计的考量。node

  • 稳定性:考虑的是系统自己足够稳定,用户使用系统作的一些动做可以稳定落地,系统自己容错性足够强能够应对网络问题,系统自己有足够的高可用等等。
  • 简单:考虑的是系统自己的设计足够简单,组件之间没有太多耦合,组件职责单一等等。
  • 可扩展性:考虑的是系统的各个模块有层次,模块对内对外一视同仁,外部能够轻易实现扩展模块插入到系统(插件),模块实现统一的接口便于替换切换具体实现等等。

下面,针对这三方面咱们都会来看一些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来进行处理最终实现这样的结果(同时也会保存组件当前的状态到数据库),即便任务执行失败也无妨,后续的任务会继续重试,这种方式是可靠性最高的。编程

二、稳定:边缘触发 vs 水平触发

K8s使用的是声明式的管理方式,也就是水平触发。另外一种作法是叫作命令式的管理,也就是边缘触发。好比咱们在作支付系统,用户充值100元,提现100元而后又充值100元,对于命令式管理就是三条命令。若是提现请求丢失了,用户帐户的余额就出错了,这确定是不能接受的,命令式管理或边缘触发必定须要配合补偿。而声明式的管理就是告诉系统,用户在进行了三次操做后的余额分别是100、0和100,最终就是100,即便提现请求丢失了,最终用户的余额就是100。设计模式

来看下下图的例子,在网络良好的状况下,边缘触发没任何问题。咱们进行了开、关、开三次操做,最后的状态是0。api

在网络出现问题的时候,丢失了关这个操做,对于边缘触发,最终停留在了2这个错误的状态。对于水平触发没有这个问题,虽然当中有一段时间网络很差,状态错误停留在了1,可是网络恢复后咱们立刻能够感知到当前的状态应该是0,状态又能回到0,最终状态也能回到正确的1。试想一下,若是咱们对咱们的Pod进行扩容缩容,若是每次告知k8s应该增长或减小多少个Pod(的这种命令式方式),最终极可能由于网络问题,Pod的状态不是咱们指望的。更好的作法是告诉k8s咱们但愿的状态,无论如今网络是否有问题,某个管理组件是否有问题,pod是否有问题,最终咱们指望k8s帮咱们调整到咱们指望的状态,宁肯慢也不要错。缓存

(图来自 这里

三、稳定:高可用设计

咱们知道etcd是基于Raft协议的分布式键值数据库/协调系统,自己推荐使用三、五、7这样奇数节点构成集群实现高可用。对于Master节点,咱们能够在每个节点都部署一个etcd,这样节点上的API Server能够和本地的etcd直接通信,而API Server由于是轻(无)状态的,因此能够在以前使用负载均衡器作代理,无论是Node节点也好仍是客户端也好均可以由负载均衡分发请求到合适的API Server上。对于相似于Job的Controller Manager以及Scheduler,显然不适合多个节点同时运行,因此它们都会采用抢占方式选举Leader,只有Leader能承担工做任务,Follower都处于待机状态。总体结构以下图所示:安全

咱们能够想一下其它一些分布式系统的高可用方案,以及咱们本身设计的系统的高可用方案,无非就是这三种大模式:

  • 无状态多节点 + 负载均衡
  • 有状态的主节点 + 从(或备份)节点
  • 对称同步的有状态多节点

四、简单:基于list-watch的发布订阅

经过前面的介绍咱们大概知道了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大概的工做过程: 组件初始化阶段:

  • Deployment Controller订阅Deployment建立事件
  • ReplicaSet Controller订阅ReplicaSet建立事件
  • Scheduler订阅未绑定Node的Pod建立事件
  • 全部Kubelet订阅本身节点的Node和Pod绑定事件

集群资源变动操做:

  1. 客户端调用API Server建立Deployment Spec
  2. Deployment Controller收到消息须要处理新的Deployment
  3. Deployment Controller调用API Server建立ReplicaSet
  4. ReplicaSet Controller收到消息须要处理新的ReplicaSet
  5. ReplicaSet Controller调用API Server建立Pod
  6. Scheduler收到消息,须要处理的新的Pod
  7. Scheduler通过处理后决定把这个Pod绑定到Node1,调用API Server写入绑定
  8. Node1上的Kubelet收到事消息须要处理Pod的部署
  9. Node1上的Kubelet根据Pod的Spec进行Pod部署

能够看到基于list-watch的API Server实现了简单可靠的消息总线的功能,基于资源消息的事件链,解耦了各组件之间的耦合,配合以前提到的基于声明式的对象管理又确保了管理稳定性。从层次上来讲,master的组件都是控制面的组件,用来控制管理集群的状态,node的组件是执行面的组件,kubelet是一个无脑执行者的角色,它们的交流桥梁是API Server的各类事件,kubelet是没法感知到控制器的存在的。

五、简单:API Sever收敛资源管理入口

以下图所示,API Server实现了基于插件+过滤器链的方式(好比咱们熟知的Spring MVC的拦截器链)来实现资源管理操做的前置校验(身份认证、受权、准入等等)。

整个流程会有哪些环节呢:

  • 身份认证,根据各类插件肯定来者是谁
  • 受权,根据各类插件肯定用户是否有资格能够操做请求的资源
  • 默认值和转换,资源默认值设置,客户端到etcd版本号转换
  • 管理控制,根据各类插件执行资源的验证或修改操做,先修改后验证
  • 验证,根据各类验证规则验证每个字段有效性
  • 幂等和并发控制,使用乐观并发方式(版本号方式)验证资源还没有被并发修改
  • 审计,记录全部资源变动日志

若是是删除资源,还会有额外的一些环节:

  • 优雅关闭
  • 终接器钩子,能够配置一些终接器,在这个时候回调
  • 垃圾回收,级联删除没有引用根的资源

对于复杂的流程式的操做,采用职责链+处理链+插件的方式来实现是很常见的作法。你可能会说这个API Server的设计整体上就不简单,怎么有这么多环节,其实这才是最简单的作法,每个环节都有独立的插件来运做(插件能够独立更新升级,也能够根据需求动态插拔配置),每个插件只是作本身应该作的事情,若是没有这样的设计,恐怕会出现1万行代码的一个大方法。

六、简单:Scheduler的设计

如图所示,相似于API Server的链式设计,Scheduler在作Pod调度算法的时候也采用了链式设计:

  • 待调度的Pod自己有一个优先级的概念,优先级高的先调度
  • 先找出全部的可用节点
  • 使用predicate(过滤器)筛选节点
  • 使用priority(排序器)对节点进行排序
  • 选择最大优先级的节点调度给Pod

常见的predicate算法有:

  • 端口冲突监测
  • 资源是否知足
  • 亲和性考量
  • ……

常见的priority算法有:

  • 网络拓扑临近
  • 平衡资源使用
  • 资源较多节点优先
  • 已使用的节点优先
  • 已缓存镜像节点优先
  • ……

好比咱们在作相似路由系统这种业务系统的时候能够借鉴这种设计模式。简单一词在于每个小组件简单,它们能够组合起来构成复杂的规则系统,这种设计比把全部逻辑堆在一块儿简单的多。

七、扩展:分层架构

K8s的设计理念是相似Linux的分层架构:

  • 核心层:Kubernetes 最核心的功能,对外提供 API 构建高层的应用,对内提供插件式应用执行环境
  • 应用层:部署(无状态应用、有状态应用、批处理任务、集群应用等)和路由(服务发现、DNS 解析等)
  • 管理层:系统度量(如基础设施、容器和网络的度量),自动化(如自动扩展、动态 Provision 等)以及策略管理(RBAC、Quota、PSP、NetworkPolicy 等)
  • 接口层:kubectl 命令行工具、客户端 SDK 以及集群联邦

以前介绍的一些组件大多数位于核心层和应用层。在更上层的管理层和接口层,咱们每每会作更多的一些二次开发。在以前的文章中我也介绍过,对于复杂的微服务互联网系统,咱们也应该把微服务进行分层,从下到上分为基础服务、业务服务、聚合业务服务等,每一层的服务聚合下层实现一些业务逻辑,不但能够作到服务重用,并且上层多变的业务服务的变更能够不影响下层基础设施的搭建。

八、扩展:接口化和插件

除了k8s大量内部组件的实现使用了插件的架构,k8s在总体设计上就把核心和外部的一些资源和服务抽象为了统一的接口,能够插件方式插入具体的实现,以下图所示:

  • 容器方面,容器运行时插件(Container Runtime Interface,简称 CRI)是 k8s v1.5 引入的容器运行时接口,它将 Kubelet 与容器运行时解耦,将原来彻底面向 Pod 级别的内部接口拆分红面向 Sandbox 和 Container 的 gRPC 接口,并将镜像管理和容器管理分离到不一样的服务。
  • 网络方面,k8s支持两种插件:
    • kubenet:这是一个基于 CNI bridge 的网络插件(在 bridge 插件的基础上扩展了 port mapping 和 traffic shaping ),是目前推荐的默认插件
    • CNI:CNI 网络插件,Container Network Interface (CNI) 最先是由CoreOS发起的容器网络规范,是Kubernetes网络插件的基础。
  • 存储方面,Container Storage Interface (CSI) 是从 k8s v1.9 引入的容器存储接口,用于扩展 Kubernetes 的存储生态。实际上,CSI 是整个容器生态的标准存储接口,一样适用于 Mesos、Cloud Foundry 等其余的容器集群调度系统 咱们看下下面这个图,k8s使用CRI插件来管理容器,为容器配置网络的时候又走了CNI插件:

CNI、CSI、CRI咱们比较熟悉了,其它更多的抽象接口这里就不描述了,k8s就像一个大主板,主板上有各类内存、CPU、IO、网络方面的接口,具体的实现k8s自己并不关心,用户和社区甚至能够根据的须要实现本身的插件。 我以为这点是最了不得的最困难的,不少时候咱们在设计一个系统的时候一开始是没法定义出抽象接口的,由于咱们不知道未来会面对什么样的实现,只有到实现愈来愈多后咱们才能抽象出接口才能制定标准。

此外,因为在kubernetes中一切皆资源,k8s 1.7以后,提供了CRD(CustomResourceDefinitions)的自定义资源二次开发能力来扩展k8s API,经过此扩展,能够向k8s API中增长新类型,会比修改k8s的源代码或者是建立自定义的API server来的更加的简洁和容易,而且不会随着k8s内核版本的升级,而出现须要代码从新合并的须要,以及兼容性方面的问题。这一功能特性的提供大大提高了k8s的扩展能力。

九、扩展:PV & PVC & StorageClass

K8s在存储方面的解耦设计特别值得一提。以下图所示,咱们来看一下k8s在存储这块的解耦设计:

(图引自Kubernetes in Action一书) 咱们要作的事情很明确,Pod须要绑定存储资源:

  • 首先,咱们确定须要有卷这种抽象,来抽象出存储方式。可是,若是每次都让k8s的使用者(无论是运维仍是开发)在部署Pod的时候设置须要的卷显然耦合太强了(好比NFS卷,每次都要设置地址,用于无需也没法关注到底层的这些细节)。卷V描述的是底层存储能力。
  • 因而,k8s抽象出持久卷PV和和持久卷声明PVC的概念,管理员能够先设置配置PV映射到卷,用户只须要建立PVC来关联PV,而后在建立Pod的时候引用PVC便可,PVC并不关注卷的一些具体细节,只关注容量需求和操做权限。PV这层抽象描述的是运维能提供出来的全局卷的资源,PVC这层描述的是用户但愿为Pod申请的存储资源请求。
  • 可是老是须要运维先建立PV仍是不方便,k8s还提供了StorageClass这层抽象,经过把PVC关联到指定的(或默认的)StorageClass来动态建立PV。

K8s中除了存储抽象的V、PV、PVC、SC,还有其它的一些组件也有相似层次的抽象以及动态绑定的理念。

咱们在使用OO语言进行编程的时候,很天然知道咱们须要先定义类,而后再实例化类来建立对象,若是类特别复杂(有不一样的实现)的话,咱们可能会使用工厂模式(或反射,外层传入目标类型名称)来建立对象。能够和k8s存储抽象比较一下,是否是这个意思,这其实就是一种解耦的方式,在架构设计中,甚至表结构设计中,咱们彻底能够引入类和实例的概念。好比工做流系统的工做流能够认为是一个类模板,每一次发起的工做流就是这个工做流的实例。

总结

好了,本文大概窥探了一下k8s的架构,不知道你是否感觉到了k8s的精良设计,对内考虑了高可用以及高可靠,对外考虑到了高可扩展性。几乎任何操做都容许失败,最终实现一致的状态,几乎任何组件都容许扩展和替换,让用户实现本身的定制需求。

若是你的业务系统也是一套复杂的资源协调系统(k8s抽象的是运维相关的资源,咱们的业务系统能够抽象的是其它资源),那么k8s的设计理念有至关多的点能够借鉴。举一个例子,咱们在作一套很复杂的流程引擎,咱们就能够考虑:

  • 流程的执行者抽象出接口,插件方式插入系统
  • 流程涉及到的资源咱们能够先梳理清楚列出来
  • 流程的管理能够把指望结果声明式方式存储到数据库
  • 流程的管控组件能够都对着统一的API服务读写&订阅变化
  • 流程的管控组件自己能够采用插件链、职责链方式执行
  • 流程的入口能够由统一的网关收口作认证和鉴权等
  • ……
相关文章
相关标签/搜索