虽然 Kubernetes 的目的是为了让部署和管理分布式系统变得更容易,但 Kubernetes 本身就是一个需要管理的分布式系统。为了能够做到这一点,开发人员需要对系统架构、系统中每一块的作用以及它们如何结合在一起有很强的理解。

概念

要想了解 Kubernetes 的架构,首先要对其发展的概念和设计原则有一个很好的把握。虽然这个系统看起来相当复杂,但实际上它是基于相对较少的概念,这些概念在整个系统中重复使用。这使得 Kubernetes 能够不断发展,同时还能保持对开发者的亲和力。关于系统中一个组件的知识往往可以直接应用到其他组件上。

声明式配置

是 Kubernetes 发展背后的主要驱动力之一。例如,用户可能会对 Kubernetes 说:“我希望我的 Web 服务器有五个副本一直运行”。Kubernetes 则会接受这个声明性的陈述,并负责确保它是真的。不幸的是,Kubernetes 无法理解自然语言指令,因此该声明实际上是以结构化的 YAML 或 JSON 文档的形式存在。

声明式配置与命令式配置不同,在命令式配置中,用户会采取一系列的直接操作(例如,创建他们想要的五个副本中的每一个副本并运行)。命令式操作通常更容易理解 — 可以简单地说,“运行这个”,而不是使用更复杂的声明式语法。然而,声明式方法的强大之处在于,你给系统的不仅仅是一连串的指令 — 你给了它一个你想要的状态的声明。因为 Kubernetes 理解你的期望状态,所以它可以采取独立于用户交互的自主行动。这意味着它可以实现自动的自我纠正和自我修复行为。对于开发者来说,这一点至关重要,因为这意味着系统可以自我修复,而不会在半夜把你吵醒。

控制器和调和

为了实现这些自愈或自纠行为,Kubernetes 的结构是基于大量独立的调和或控制环路。在设计像 Kubernetes 这样的系统时,一般有两种不同的方法可以采用 — 基于单体状态的方法或基于分散式控制器的方法。

在单体系统设计中,系统知道整个世界的状态,并使用这个完整的视图以协调的方式推进一切。这可能是非常有吸引力的,因为系统的操作是集中的,因此更容易理解。单片式方法的问题是,它不是特别稳定。如果发生任何意外,整个系统就会崩溃。

Kubernetes 在设计上采用了另一种去中心化的方式。Kubernetes 不是一个单一的单片控制器,而是由大量的控制器组成,每个控制器都执行自己独立的调节循环。每个单独的循环只负责系统中的一小块(例如,更新某个负载均衡器的端点列表),而且每个小控制器完全不知道其他的世界。这种对一个小问题的关注和对更广泛的世界状态的相应无知,使得整个系统的稳定性大大提高。每个控制器在很大程度上独立于其他所有控制器,因此不受与自身无关的问题或变化的影响。不过,这种分布式方法的缺点是,系统的整体行为可能更难理解,因为没有一个单一的位置来寻找系统行为方式的解释。相反,有必要研究大量独立进程的相互作用。

控制环路的设计模式使得 Kubernetes 更加灵活稳定,并且在 Kubernetes 的系统组件中重复使用。控制循环的基本思想是不断重复以下步骤,如图 3-1 所示。

  • 获取世界的期望状态
  • 观察世界
  • 找出观察到的世界与期望的世界状态之间的差异
  • 采取行动,使观察到的世界与期望的状态相匹配

image.png
图 3-1 通用调和循环的说明

最简单的例子可以帮助你理解调节控制回路的运作,就是你家里的恒温器。它有一个期望的状态(你在恒温器上输入的温度),它对世界进行观察(你家里当前的温度),它找到这些数值之间的差异,然后采取动作(要么加热,要么制冷),使现实世界与世界的期望状态相匹配。

Kubernetes 中的控制器也在做同样的事情。它们通过向 Kubernetes API 服务器发出的声明性语句来观察世界的期望状态。例如,用户可能会声明:“我想要该 Web 服务器的四个副本”。Kubernetes 副本控制器会接受这个想要的状态,然后观察这个世界。它可能会看到,该 Web 服务容器当前有三个副本。控制器发现当前状态和期望状态之间的差异(少了一个 Web 服务),然后采取行动,通过创建第四个 Web 服务容器使当前状态与期望状态相匹配。

:::info 当然,管理这种声明性状态的挑战之一是确定调和控制循环应该关注的 Web 服务器集。这就是标签和标签查询进入 Kubernetes 设计的地方。 :::

隐性或动态分组

无论是将一组副本分组,还是确定负载均衡器的后端,在 Kubernetes 的实现过程中,有很多时候都需要确定一组事物。当把事物分组到一组时,有两种可能的方法 — 显式/静态分组或隐式/动态分组。对于静态分组,每个组都由一个具体的列表来定义(例如,“我的团队成员是Alice、Bob和Carol”)。列表明确地叫出组中每个成员的名字,而且列表是静态的 — 也就是说,除非列表本身发生变化,否则成员资格不会改变。就像单片机的设计方法一样,这种静态的分组很容易理解。要知道谁在一个组中,只需阅读列表即可。静态分组的挑战在于它是不灵活的 — 它不能对动态变化的世界做出反应。希望在这一点上,你知道 Kubernetes 使用了一种更动态的分组方法。在 Kubernetes 中,组是隐式定义的。

显式、静态组的替代方案是隐式、动态组。在隐式组中,组不是成员列表,而是由一个声明来定义,比如 “我的团队的成员是穿橙色衣服的人”。这个组是隐式定义的。在组的定义中,没有任何地方定义了成员;相反,它们是通过对照一组在场的人评估组定义而隐含的。因为在场的一组人总是可以改变,所以群体的成员也同样是动态变化的。虽然这样做会带来复杂性,但因为第二步(在示例中,寻找穿橙色衣服的人),它也明显更加灵活和稳定,它可以处理不断变化的环境,而不需要不断调整静态列表。

在 Kubernetes 中,这种隐式分组是通过标签和标签查询或标签选择器实现的。Kubernetes 中的每一个 API 对象都可以拥有任意数量的键/值对,这些键/值对被称为 “标签”,它们与该对象相关联。然后,你可以使用标签查询或标签选择器来识别与该查询相匹配的一组对象。一个具体的例子如图 3-2 所示。

:::info 每个 Kubernetes 对象都有标签和注解。最初它们可能看起来是多余的,但它们的用途是不同的。标签可以被查询,应该提供以某种方式服务于识别对象的信息。注释不能被查询,应该用于关于对象的一般元数据 — 不代表其身份的元数据(例如,当对象以图形方式呈现时,显示在它旁边的图标)。 ::: image.png
图 3-2 标签和标签选择示例

结构

现在你已经对 Kubernetes 系统中实现的设计理念有了一定的了解,让我们考虑一下构建 Kubernetes 的设计原则。以下基本设计原则在 Kubernetes 开发中至关重要。

Unix 的多组件理念

Kubernetes 认同一般 Unix 的模块化和小部件做好自己工作的理念。Kubernetes 不是一个单一的单体应用,它在一个二进制中实现了所有的各种功能。相反,它是一个不同应用的集合,所有这些应用都在很大程度上相互无视的情况下一起工作,以实现称为 Kubernetes 的整体系统。即使有一个二进制(例如控制器管理器)将大量不同的功能集中在一起,这些功能在该二进制中几乎完全独立地相互持有。它们被编译在一起主要是为了让部署和管理 Kubernetes 的任务变得更容易,而不是因为组件之间的任何紧密绑定。

同样,这种模块化方法的优势在于,Kubernetes 是灵活的。大块的功能可以被撕掉并替换,而系统的其他部分不会注意到或关心。当然,缺点是复杂,因为部署、监控和理解系统需要在许多不同的工具上整合信息和配置。有时,这些部件被编译成单一的二进制可执行文件,但即使在这种情况下,它们仍然通过 API Server 进行通信,而不是直接在运行的进程中进行通信。

API 驱动的交互

Kubernetes 内部的第二个结构设计是,组件之间的所有交互都是通过一个集中的 API 表面区域来驱动的。这种设计的一个重要推论是,组件使用的 API 与其他每个集群用户使用的 API 完全相同。这对 Kubernetes 有两个重要的后果。第一,系统中没有任何一个部分比其他任何部分有更多的特权或更直接的内部访问权限。事实上,除了实现 API 的 API Server 之外,根本没有人可以访问内部结构。因此,每一个组件都可以换成其他的实现,并且可以在不重新架构核心组件的情况下增加新的功能。正如我们将在后面的章节中看到的那样,即使是像调度器这样的核心组件也可以被替换掉,用其他的实现来代替(或者仅仅是增强)。

API 驱动的交互激励系统在存在版本偏斜的情况下进行稳定的设计。当你向一组机器推出一个分布式系统时,在一段时间内,你将同时运行旧版本和新版本的软件。如果你没有直接对这种版本倾斜进行规划,那么新旧版本之间未经规划的(通常未经测试的)交互会导致不稳定和中断。因为在 Kubernetes 中,所有的事情都是通过 API 来调解的,而且 API 提供了强定义的 API 版本和不同版本号之间的转换,所以版本倾斜的问题基本可以避免。不过在现实中,偶尔还是会出现一些问题,版本歪曲和升级测试是 Kubernetes 发布资质的重要组成部分。

组件

在了解了 Kubernetes 架构中的概念和结构后,我们现在可以讨论组成 Kubernetes 的各个组件。这算是一个词汇表 — 当你需要了解 Kubernetes 系统的各个部分是如何结合在一起的时候,你可以参考这个世界地图。其中一些组件比其他组件更重要,因此在后面的章节中会有更详细的介绍,但本参考指南将有助于为后面的探索奠定基础和提供背景。

Kubernetes 是一个系统,它将一个庞大的机器群组合成一个单一的单元,可以通过 API 进行消费,但 Kubernetes 的实现实际上将机器群细分为两组:工作节点和头部节点。构成 Kubernetes 基础架构的大部分组件都运行在头部或控制平面(Control Plane)节点上。在一个集群中,这样的节点数量有限,一般为一个、三个或五个。这些节点运行实现 Kubernetes 的组件,比如 etcd 和 API Server。这些节点的数量是奇数,因为它们需要使用 Raft/Paxos 算法来保持共享状态下的法定人数。集群的实际工作是在工作节点上完成的。这些节点还运行着更有限的 Kubernetes 组件选择。最后,还有一些 Kubernetes 组件在 Kubernetes 集群创建后被调度到集群中。从 Kubernetes 的角度来看,这些组件与其他工作负载没有区别,但它们确实实现了整体 Kubernetes API 的一部分。

下面对 Kubernetes 组件的讨论将它们分成三组:运行在头部节点上的组件,运行在所有节点上的组件,以及运行在集群上的预定组件。

头部节点组件

头节点是 Kubernetes 集群的大脑。它包含了实现 Kubernetes API 功能的核心组件的集合。通常情况下,只有这些组件在头部节点上运行,没有共享这些节点的用户容器。

etcd

etcd 系统是任何 Kubernetes 集群的核心。它实现了键值存储,Kubernetes 集群中的所有对象都被持久化。etcd 服务器实现了一个分布式共识算法,即 Raft,它确保即使其中一个存储服务器发生故障,也有足够的复制来维护存储在 etcd 中的数据,并在一个 etcd 服务器重新变得健康并将自己重新添加到集群中时恢复数据。etcd 服务器还提供了 Kubernetes 大量使用的另外两个重要功能。第一个是乐观的并发性。etcd 中存储的每一个值都有一个对应的资源版本。当一个键值对被写入 etcd 服务器时,可以根据特定的资源版本进行条件化。这意味着,使用 etcd,你可以实现比较和交换,这是任何并发系统的核心。比较和交换使用户能够读取一个值,并在知道系统中没有其他组件也更新了这个值的情况下更新它。这些保证使系统能够安全地让多个线程在 etcd 中操作数据,而不需要悲观的锁,因为锁会大大降低服务器的吞吐量。

除了实现比较和交换之外,etcd 服务器还实现了 watch 协议。watch 的价值在于,它可以让客户端高效地观察整个值目录的键值存储的变化。举个例子,一个 Namespace 中的所有对象都存储在 etcd 的一个目录中。使用 watch 可以使客户机高效地等待并对变化做出反应,而无需持续轮询 etcd 服务器。

API Server

虽然 etcd 是 Kubernetes 集群的核心,但实际上只有一个服务器被允许直接访问 Kubernetes 集群,那就是 API Server。API Server 是 Kubernetes 集群的中心;它负责调解客户机与存储在 etcd 中的 API 对象之间的所有交互。因此,它是所有各种组件的中心会议点。由于它的重要性,API 服务器值得更深入地反省,并在第 4 章中介绍。

调度器

有了 etcd 和 API Server 的正确运行,一个 Kubernetes 集群在某些方面已经完成了功能。你可以创建所有不同的 API 对象,比如 Deployment 和Pod。然而,你会发现,它从来没有开始运行。为 Pod 找到一个运行的位置是 Kubernetes 调度器的工作。调度器会在 API Server 上扫描未调度的 Pod,然后确定运行它们的最佳节点。和 API Server 一样,调度器也是一个复杂而丰富的话题,在第 5 章会有更深入的介绍。

控制器管理器

etcd、API Server 和调度器运行后,你可以成功地创建 Pod,并看到它们被调度到节点上,但你会发现 ReplicaSet、Deployment 和 Service 并不能像你期望的那样工作。这是因为实现该功能所需的所有调和控制循环当前都没有运行。执行这些循环是控制器管理器的工作。控制器管理器是所有 Kubernetes 组件中变化最大的,因为它内部有许多不同的调节控制循环来实现整个 Kubernetes 系统的许多部分。

所有节点上的组件

除了只在头部节点上运行的组件外,还有一些组件存在于 Kubernetes 集群的所有节点上。这些组件实现了所有节点上都需要的基本功能。

kubelet

:::tips kubelet 是 Kubernetes 集群中所有机器的节点守护程序。kubelet 是将节点的可用 CPU、磁盘和内存连接到大型 Kubernetes 集群的桥梁。kubelet 与 API Server 进行通信,以找到应该在其节点上运行的容器。同样,kubelet 也会将这些容器的状态回传给 API Server,以便其他调和控制循环可以观察这些容器的当前状态。 :::

除了调度和报告 Pod 中的容器在其机器上运行的状态,kubelet 还负责健康检查和重启应该在其机器上执行的容器。如果将所有的健康状态信息推送回 API Server,以便调和循环能够采取行动来修复特定机器上容器的健康状态,这将是相当低效的。相反,kubelet 会绕过这种交互,自己运行调和循环。因此,如果一个被 kubelet 运行的容器死亡或健康检查失败,kubelet 会重新启动它,同时也会将这个健康状态(和重新启动)回传给 API服务器。

kube-proxy

另一个在所有机器上运行的组件是 kube-proxy。kube-proxy 负责实现 Kubernetes 服务负载均衡器网络模型。kube-proxy 始终在观察 Kubernetes 集群中所有 Service 的端点对象。然后,kube-proxy 会对其节点上的网络进行编程,从而使向某个 Service 的虚拟 IP 地址发出的网络请求事实上会被路由到实现这个 Service 的端点。Kubernetes 中的每个 Service 都会得到一个虚拟 IP 地址,而 kube-proxy 是负责定义和实现本地负载均衡器的守护进程,它将机器上的 Pod 的流量路由到集群中任何地方实现该 Service 的Pod。

预定组件

当刚才描述的所有组件都成功运行时,它们就提供了一个最小可行的 Kubernetes 集群。但是,还有几个额外的预定组件对 Kubernetes 集群来说是必不可少的,它们实际上是依靠集群本身来实现的。这意味着,虽然它们对集群功能至关重要,但它们也是使用对 Kubernetes API Server 本身的调用进行调度、健康检查、操作和更新的。

KubeDNS

一个虚拟的 IP 地址,但这个 IP 地址也被编入 DNS 服务器中,以方便服务发现。KubeDNS 容器为 Kubernetes Service 对象实现了这个名称服务。KubeDNS 服务本身就表现为 Kubernetes 服务,所以 kube-proxy 提供的路由同样将 DNS 流量路由到 KubeDNS 容器。一个重要的区别是,KubeDNS 服务被赋予了一个静态的虚拟 IP 地址。这意味着 API Server 可以将 DNS 服务器编程到它创建的所有容器中,实现 Kubernetes 服务的命名和服务发现。

:::info 除了从第一个版本开始就存在于 Kubernetes 中的 KubeDNS 服务之外,还有一个更新的替代性 CoreDNS 实现,在 Kubernetes 的 1.11 版本中达到了通用性(GA)。 :::

DNS 服务能够被换掉,这既显示了模块化的特点,也显示了使用 Kubernetes 来运行 DNS 服务器等组件的价值。用 CoreDNS 替换 KubeDNS 就像停止一个 Pod 并启动另一个 Pod 一样简单。

Heapster

另一个预定组件是一个名为 Heapster 的二进制组件,它负责收集 Kubernetes 集群内运行的所有容器的 CPU、网络和磁盘使用情况等指标。这些指标可以被推送到监控系统,比如 InfluxDB,用于对集群中的应用健康状况进行提醒和一般监控。另外,重要的是,这些指标被用来实现 Kubernetes 集群内 Pod 的自动伸缩。Kubernetes 有一个自动缩放器的实现,例如,只要 Deployment 中容器的 CPU 使用率超过 80%,就可以自动缩放 Deployment 的大小。Heapster 就是收集和汇总这些指标的组件,为自动缩放器实现的调节循环提供动力。自动缩放器通过对 Heapster 的 API 调用来观察当前世界的状态。

:::info 截至目前,Heapster 仍然是许多 Kubernetes 集群中自动伸缩的度量源。然而,从 1.11 版本开始,它已经被废弃,转而使用新的 metrics-server 和 Metrics API。Heapster 将在 1.13 版本中从 Kubernetes 中移除。 :::

附加组件

除了这些核心组件之外,在大多数 Kubernetes 的安装上还可以找到许多系统。这些包括 Kubernetes Dashboard,以及社区附加组件,如函数即服务(FaaS)、自动证书代理等。Kubernetes 附加组件太多,无法用几段话来描述,所以在第 13 章中会介绍扩展你的 Kubernetes 集群。

总结

Kubernetes 是一个有些复杂的分布式系统,它有许多不同的组件来实现完整的 Kubernetes API,包括运行 API Server 的控制平面节点和构成 API 后盾存储的 etcd 集群。此外,调度器与 API Server 交互,将容器调度到特定的工作节点上,控制器管理器操作大部分的控制循环,以保持集群的正常运行。在集群正常运行之后,还有许多组件运行在集群本身之上,包括集群 DNS 服务、Kubernetes 服务负载平衡器基础架构、容器监控等。我们将在第 12 章和第 13 章中探讨更多可以在集群上运行的组件。