本期文章来自才云科技(Caicloud)CEO 张鑫的技术原创。
导言:
Kubernetes 是一个庞大的软件系统,欲从源码层精通 Kubernetes 的进阶学习者往往会经历 “Kubernetes:从入门到放弃” 的挫败感。俗话说:擒贼先擒王,与其一开始就掉入 Kubernetes 广阔的代码海洋里迷失自己,不如先来了解一下 Kubernetes 软件架构的核心设计模式与经典架构。其中,我们先从 Kubernetes 的声明式设计模式与控制闭环讲起。

“一切皆声明”的软件架构设计模式

Kubernetes 采用了“声明式”(Declarative)的资源管理模式,该模式对资源的管理流程分为如下几个明晰的步骤:

声明(Declare):用户通过声明式的配置文件(例如 YAML 文件)向 Kubernetes 告白自己希望达到的系统状态(例如:运行拥有 5 个副本的 nginx 服务)。
观测(Observe):Kubernetes 会观测到新的用户声明,并自动分析出需要执行的操作以达到用户声明的系统状态(例如在集群中选取5个合适的节点,并在这 5 个节点上下载合适的 nginx 镜像并启动容器,以及配置相应的负载均衡策略等)。
行动(React):Kubernetes 的控制组件负责具体执行这些指令,使得用户声明的系统状态得以实现;在此过程中不需要任何人工的参与。
持续观测与收敛(Feedback and Control Loop):Kubernetes 的设计者深谙硬件故障、应用异常是大规模分布式系统的常态;因此,在用户声明的状态通过上述步骤得以实现后,Kubernetes 仍会持续地、孜孜不倦地观测系统的实时状态。当遇到系统故障、容器退出等异常导致系统的当前状态与最初的用户初心不符时,Kubernetes 仍可以进行 24 x 7 的不间断修复。例如,声明的 5 个 nginx 实例如果在运行中有 2 个实例非正常退出,Kubernetes会观测到此状态并且自动选取另外合适的节点来运行新的 2 个 nginx 实例,保证总的 nginx 实例个数为 5。在此过程中,传统需要运维人员人肉轮岗完成的工作,Kubernetes 全部以自动化来代替。

上述声明式(Declarative)管理方法和 Declare – Observe – React 的控制闭环(Control Loop)体现了谷歌内部软件系统的实践精华,同时也是我们值得学习借鉴的一个软件架构设计模式。与声明式(Declarative)相对应的是祈使式(Imperative)的设计模式:在 Imperative 设计模式中,用户需要指明一步一步的具体操作流程和指令(例如 Shell 脚本),而并非像声明式模式一样直接指明最终的想要达到的状态。

具体来说,以运行 5 个 nginx 的实例为例,声明式的语言配置片段大致如下:

replicas: 5
template:
spec:

containers:
- name: my-nginx
  image: caicloud/nginx:v1

而对应的祈使式脚本则可能需要如下的逻辑:

选择合适的五台机器作为运行的宿主机
分别获取这些机器的 IP 地址,并进行 ssh 登录
在 ssh 登陆后,判断 docker 是否已经在宿主机上运行,如果没有运行则要启动 docker daemon
分别运行 docker run 命令来启动 nginx 实例
判断 docker run 是否成功,应用是否被正常启动

Declare – Observe - React 的软件设计模式有如下的好处:

简单:声明式的配置文件更贴近“人类语言”(例如 YAML),从上述例子中可以看出,相比于传统的 imperative 设计模式,声明式配置更加可读和易于维护。

智能:声明式配置只需指定用户想要实现的目标(运行 5 个 nginx 实例),而软件系统会自动识别出达到目标所需的实际操作并执行。

可靠:“Failure is the norm”,故障是软件系统的常态;当系统或软件在执行过程中遇到异常和故障时,祈使式的脚本往往就会异常退出,并需要人工干预进行校正。此外,即使系统在一开始被正确的配置好(例如 5 个 nginx 已经成功运行),在系统的运行过程中随着时间的推移故障也随时可能发生(例如机器故障导致 1 个 nginx 实例死亡)。祈使式的脚本很难长期地对系统状态进行自动维护。然而,在 Declare-Observe-React 模式中,软件系统会时刻“Observe”当前系统的实际状态(actual state),并与用户的配置“初心”(declared state)作对比;当发现偏差时则会智能地识别出需要进行的校正,并进行执行(react),例如重新选择一个健康机器来运行一个新的 nginx 以替代之前的“先烈”。

如数家珍 Kubernetes 的经典控制闭环

俗话说,“说的总比做的容易”;英文亦有云:“Easier Said than Done”。声明仅仅是声明,而如何从声明智能地转化为一系列可执行操作,并持续地进行观测、校正,则是由 Kubernetes 的控制闭环来完成。这些经典的闭环包括:

创建和销毁 Pods 的控制闭环
驱逐、终止、重调度 Pods 的控制闭环
弹性伸缩一个服务的控制闭环
将 Pods 调度到 Nodes 上的控制闭环
创建、删除、和重启 Nodes 的控制闭环
删除、重启容器的控制闭环
控制资源消耗与分配的控制闭环

ReplicaSet控制闭环

我们先来研究创建和销毁 Pods 的控制闭环。Kubernetes 早期版本的用户会熟悉“Replication Controller”并容易与 ReplicaSet 造成混淆:两者都是用来根据用户的意愿(通过用户声明的配置文件)来保证某个应用(容器镜像)运行指定数量的副本(例如保证 5 个 nginx 实例时刻运行在合适的机器节点上)。

而两者的区别其实非常简单:ReplicaSet 是新一代的 Replication Controller,Replication Controller 则应该退出历史舞台。原因是,ReplicaSet 能支持更灵活的标签匹配规则,而 Replication Controller 只能支持标签的 exact match 。因此,一个 Replication Controller 只能根据严格的字符串匹配来选择“身背”某个标签的 Pods 进行管理(例如管理背负着 app = nginx 的 Pods 进行控制)。

相比之下,长江后浪推前浪,新一代的 ReplicaSet 则可以利用数学中的集合论和操作来支持更灵活的匹配规则,例如 tier notin (frontend, backend)(具体可以参考 Kubernetes 官方文档:https://kubernetes.io/docs/co...)。

然而,在 Kubernetes 的实际使用中,ReplicaSet 也属于底层概念,由 Deployment 来管理;因此在实操层面,用户在绝大多数情况下应该与 Deployment 打交道,通过创建 Deployment 来创建应用,而不应该直接创建 ReplicaSet;Deployment 会自动地创建并管理对应的底层 ReplicaSet,从而最终控制 Pods 的副本数量。

虽然如此,Pods 的生命周期和副本数量都是由 ReplicaSet 来具体实现的;因此下面我们剖析一下 ReplicaSetController 的代码,来理解前述的 Kubernetes 的控制闭环是如何落实到代码层面的。

ReplicaSetController 源码分析

ReplicaSet 的控制逻辑主要位于 ReplicaSetController,其源码位于:
https://github.com/kubernetes...

ReplicaSetController 的结构体主要包含如下成员:
所有 ReplicaSet 对象的列表
所有 Pods 对象的列表
一个工作队列用来循环处理需要更新的 ReplicaSet 对象
控制 ReplicaSet 触发行动频率的参数 burstReplicas

创建一个 ReplicaSetController 由 NewReplicaSetController 函数完成,其具体的创建过程如下:

clipboard.png

ReplicaSetController 工作流程

下面,我们按照 Declare – Observe - React 的控制流程来梳理 ReplicaSetController 的工作流程。

Declare
Kubernetes 将用户在 YAML 配置中指定的应用副本数量等配置转化为一个 extensions.ReplicaSet 结构体(其源代码文件位于:Kubernetes/pkg/apis/extensions/types.go),主要包含 ReplicaSetSpec 和 ReplicaSetStatus 两个成员结构体。其中的 ReplicaSetSpec 结构体中记录了用户 Declare 的配置信息, 例如用户指定的副本数量(Replicas)、标签选择器(Selector)、用以创建应用副本所需的 Pod 模版(Template)等信息。

Observe
ReplicaSetController 通过多个 worker 线程来进行持续的 Pod 状态观测和校正。ReplicaSetController 维护一个工作队列(work queue),工作队列中的元素是需要被观测、进而进行处理的一个个 ReplicaSet 对象。而什么样的 ReplicaSet 会被放到这个工作队列中呢?原来 ReplicaSetController 采用了事件驱动的设计模型(Event Driven),在 ReplicaSetController 被创建的时候就注册了一系列的事件 callback 函数。这些事件包括 Pod 的创建、消亡、ReplicaSet 的更新(例如标签、副本数量)等。每当上述事件发生,这个事件所牵扯到的具体的 ReplicaSet 对象就会被放入这个工作队列中。因此,总结来说,Kubernetes 采用了 Event Driven 的设计模型实现了不间断的 Observe 操作,并通过工作队列实现了多线程不间断的处理逻辑,如下图所示:

clipboard.png

如上图所示:
ReplicaSetController 以 Run 函数为程序入口,启动了多个 go routine,而每个go routing 中通过 worker 函数不间断地通过 processNextWorkItem 函数来从工作队列中获取需要被观测的 ReplicaSet 进行检查。
检查通过 syncReplicaSet 函数进行,其中包括判断该 ReplicaSet 是否需要被同步,如果答案是肯定的,则进一步通过 manageReplicas 来完成同步或校验。而实际观测到的 ReplicaSet 的状态也会被更新到 ReplicaSet 中的 ReplicaSetStatus 成员结构体中。
React
React 的逻辑尽在 manageReplicas 函数中,该函数首先判断用户指定的副本数量(ReplicaSet.Spec.Replicas)是否与当前的实际 Pod 副本数量保持一致,如果不一致:
若实际的 Pod 副本数量少于指定值,则通过 PodControlInterface 进行 Pod 的创建
若实际的 Pod 副本数量大于实际值,则通过 PodControlInterface 进行 Pod 的删除

不论何种情况,我们可以看到 Kubernetes 都是自动地分别出所需要的操作并自动地进行执行,体现了自动化的优势。


Caicloud
337 声望89 粉丝

Caicloud 作为中国最早期的容器技术云平台开拓者之一,启用新一带容器集群技术(Container Cluster),推出全新容器云计算平台和云平台运维管理系统以服务企业和开发者。核心团队成员来自美国谷歌(Google)、亚...