本章内容 应用部署的演变 编排系统组件的分类 编排器的思维模型介绍 定义编排器的需求 明确工作范围
Kubernetes,Kubernetes,Kubernetes。如果你在过去五年里曾在科技行业工作或接触过这个领域,你至少听说过这个名字。也许你在日常工作中使用过它,或者你使用过其他系统,如Apache Mesos或HashiCorp的Nomad。
在本书中,我们将通过编写代码,构建我们自己的Kubernetes,以更好地理解Kubernetes究竟是什么。简单来说,Kubernetes 与 Mesos 和 Nomad 一样, 是一个编排器。
读完本书后,你将学到以下内容: 组成编排系统的基础组件是什么 这些组件之间如何交互 每个组件如何维护其自身状态及其底层原理 在设计和实现编排系统时做出的权衡
1.1 从头开始实现编排器的原因 我们为什么要从头开始编写一个编排器呢? 首先,我们并不是希望编写一个能够取代 Kubernetes、Mesos 或 Nomad 的系统。 答案比这更为实际。 如果你像我一样,习惯通过实践来学习新的知识。 当我们处理小事情时,通过实践学习是很容易的。比如: 如何在我正在学习的新编程语言中编写一个for循环? 如何使用curl命令向新的API发出请求? 这些事情很容易通过实践来学习,因为它们覆盖的知识面很小,不需要太多的努力就能完成。
然而,当我们想要学习更大型的系统时,通过实践学习变得具有挑战性。显而易见的解决方法是阅读源代码。Kubernetes、Mesos和Nomad的代码都可以在GitHub上找到。那么,如果源代码是可用的,为什么还要从头开始编写一个编排器呢?难道我们不能通过查看它们的源代码获得同样的收益吗?
也许可以。不过,请记住,这些都是大型软件项目。Kubernetes包含超过200万行源代码,Mesos和Nomad的代码量也刚刚超过70万行。虽然这并非不可能,但在如此庞大的代码库中摸索学习系统可能不是最好的方法。
相反,我们将卷起袖子,亲自动手。我们将实现一个不到3000行代码的编排器。
为了确保我们专注于编排器的核心部分而不被分散注意力,我们将缩小实现的范围。在这个项目过程中你编写的编排器将是完全功能性的。你将能够启动和停止任务,并与这些任务进行交互。
然而,它不会是生产就绪的。毕竟,我们的目的是不是实现一个可以替代Kubernetes、Nomad或Mesos的系统。相反,我们的目的是实现一个最小系统,从而深入了解像Kubernetes和Nomad这样的生产级系统是如何工作的。
1.2 那些(不太)美好的旧时光 让我们回到2002年,认识一下 Michelle。Michelle 是一名公司系统管理员,负责确保公司应用程序全天候正常运行。她是如何做到这一点的呢?
像许多其他系统管理员一样,Michelle采用了一种常见的策略:将应用程序部署在裸金属服务器上。图 1-1 展示了 Michelle 的世界的一个简化示意图。每个应用程序通常运行在自己的物理硬件上。更复杂的是,每个应用程序都有其独特的硬件需求,因此Michelle必须购买并管理一组针对每个应用程序量身定制的服务器。此外,每个应用程序都有其独特的部署过程和工具。 数据库团队通过光盘邮寄的方式获取新版本进行更新,因此其过程涉及数据库管理员(DBA)将文件从光盘复制到中央服务器,然后使用一套自定义的Shell脚本将文件推送到数据库服务器,然后使用另一套Shell脚本负责安装和更新。Michelle亲自处理公司财务系统的安装和更新。这个过程包括了从互联网下载软件,虽然省去了处理光盘的麻烦,但财务软件自带了一套安装和管理更新的工具。与此同时,其他几个团队正在构建公司的软件产品,这些团队构建的应用程序也有一套完全不同的工具和程序。
图 1-1 展示了 2002 年 Michelle 的世界。外框代表物理机器及其上运行的操作系统。内框代表运行在这些机器上的应用程序,展示了应用程序如何更直接地与操作系统和机器绑定在一起。
定制应用程序 金融应用
如果你正处于那个时代,但没有在相关行业中工作过,没有经历过像Michelle那样的情况,你是幸运的。当时管理软件的方式不仅困难混乱,还极其浪费资源。接下来在2000年代初期到中期,虚拟化技术出现了。这些工具允许像Michelle这样的系统管理员将他们的物理服务器划分成多个较小但独立的虚拟机(VM)。每个应用程序不再运行在专用的物理机器上,而是运行在虚拟机上。多个虚拟机可以被打包到一台物理机器上。虽然虚拟化让像Michelle这样的人生活变得更好,但它并不是万能的解决方案。
这种情况一直持续到2010年代中期,当时两项新技术出现在人们的视野中。第一项是Docker,它将容器技术引入了更广泛的世界。容器的概念并不新鲜,自1979年以来就已经存在(参见Ell Marquez的《容器技术的历史》,网址:http://mng.bz/oro2)。 在Docker之前,容器主要局限于大型公司,如Sun Microsystems和Google,以及那些希望为客户提供高效且安全的虚拟化环境的托管服务提供商。第二项新技术是Kubernetes,一个专注于自动化部署和管理容器的编排器。
1.3 容器是什么,它与虚拟机有何不同? 如前所述,从Michelle早期的物理机器和操作系统世界转变的第一步是引入虚拟机。虚拟机(VM)抽象了计算机的物理组件(CPU、内存、磁盘、网络、CD-ROM等),使管理员可以在一台物理机器上运行多个操作系统。每个运行在物理机器上的操作系统都是独立的。每个操作系统都有自己的内核、自己的网络堆栈和自己的资源(如CPU、内存、磁盘)。
虚拟机(VM)世界在成本和效率方面是一个巨大的改进。然而,这些成本和效率的提升仅适用于机器和操作系统层面。在应用层面,变化并不大。如图1-2所示,应用程序仍然紧密耦合于操作系统。如果你想运行两个或更多实例的应用程序,你需要两个或更多的虚拟机。
图1-2 虚拟机上运行的应用程序
与虚拟机不同,容器没有自己的内核,也没有自己的网络堆栈。它不控制资源如CPU、内存和磁盘。事实上,容器这个术语只是一个概念;它不像虚拟机那样是一个具体的技术实体。
容器这个术语实际上是Linux内核中进程和资源隔离的简称。因此,当我们谈论容器时,我们实际上是在谈论命名空间(namespaces)和控制组(cgroups),这两者都是Linux内核的特性。命名空间是一种将进程及其资源相互隔离的机制。控制组则为一组进程提供资源限制和计算。
但让我们不要过于纠结于这些底层细节。你不需要了解命名空间和控制组就能继续阅读本书的其余部分。不过,如果你感兴趣,我鼓励你观看Liz Rice的演讲《从零开始的容器》(https://www.youtube.com/watch?v=8fi7uSYlOdc)。
随着容器的引入,应用程序可以从操作系统层面解耦,如图1-3所示。有了容器,如果我有一个启动服务器进程并监听80端口的应用程序,我现在可以在单个物理主机上运行多个实例。或者,假设我有六个不同的应用程序,每个应用程序都有自己的服务器进程监听80端口。同样,有了容器,我可以在同一主机上运行这六个应用程序,而不必在应用层面为每个应用程序分配不同的端口。
图1-3 容器中运行的应用程序
容器的真正好处在于,它们给应用程序一种错觉,让它以为自己是操作系统上唯一运行的应用程序,因此可以访问操作系统的所有资源。
1.4 什么是编排器? Michelle世界的最新一次演变是使用编排器来部署和管理她的应用程序。编排器是一个为部署、扩展和管理容器提供自动化的系统。在许多方面,编排器类似于CPU调度器。不同之处在于,编排系统的目标对象是容器,而不是操作系统级别的进程。(虽然容器通常是编排器的主要关注点,但有些系统也提供其他类型工作负载的编排。例如,HashiCorp的Nomad除了支持Docker外,还支持Java、命令行和QEMU虚拟机运行器等工作负载类型。)
有了容器和编排器,Michelle的世界发生了巨大的变化。过去,她部署和管理的物理硬件和操作系统大多由应用程序供应商的要求决定。例如,她公司的财务系统必须运行在AIX(IBM拥有的专有Unix操作系统)上,这意味着物理服务器必须是基于RISC(https://riscv.org/)的IBM机器。 因为开发和销售财务系统的供应商认证该系统必须在AIX上运行。如果Michelle尝试在Debian Linux上运行财务系统,供应商将不会提供支持,因为这不是一个经过供应商认证的操作系统。而这只是Michelle为公司操作的众多应用程序之一。
现在,Michelle可以部署一组标准化的机器,所有机器都运行相同的操作系统。她不再需要处理多个硬件供应商提供的专用服务器,也不再需要处理每个操作系统独有的管理工具。最重要的是,她不再需要应用程序供应商提供的各种部署工具。相反,她可以使用相同的工具来部署、扩展和管理公司所有的应用程序(见表1-1)。
表1-1 Michelle的旧世界与新世界 Michelle的旧世界 Michelle的新世界 多个硬件供应商 单一硬件供应商(或云提供商) 多个操作系统 单一操作系统 运行时要求由应用程序供应商决定 应用程序供应商构建标准化(容器和编排)
通过这种转变,Michelle的工作变得更加简化和高效。
1.5 编排系统的组成部分 编排器自动化了容器的部署、扩展和管理。接下来,让我们识别出实现这些功能所需的通用组件及其要求。它们包括:
任务(Task) 作业(Job) 调度器(Scheduler) 管理器(Manager) 工作节点(Worker) 集群(Cluster) 命令行界面(CLI) 其中一些组件可以在图1-4中看到。
1.5.1 任务 任务是编排系统中最小的工作单元,通常运行在容器中。你可以将其视为在单台机器上运行的进程。一个任务可以运行一个反向代理实例,如NGINX,或者运行一个应用程序实例,如RESTful API服务器;它也可以是一个简单的程序,运行在一个无限循环中,做一些简单的事情,比如ping一个网站并将结果写入数据库。
一个任务应指定以下内容: 1 运行所需的内存、CPU和磁盘量 2 在发生故障时编排器应采取的措施,通常称为重启策略 3 用于运行任务的容器镜像的名称
任务定义可能会指定其他详细信息,但这些是核心要求。
图 1-4 编排系统的基本组件。无论不同的编排器使用什么术语,每个编排器都有调度器、管理器和工作节点,它们都在任务上操作。
1.5.2 作业 作业是任务的集合。它包含一个或多个任务,通常形成一个更大的逻辑任务组来执行一组功能。例如,一个作业可以由一个RESTful API服务器和一个反向代理组成。
Kubernetes与作业的概念 如果你只熟悉Kubernetes,这个作业的定义可能一开始会让你感到困惑。在Kubernetes中,作业是一种特定类型的工作负载,历史上被称为批处理作业(batch job),即启动后运行到完成的作业。Kubernetes有多种资源类型,它们是Kubernetes特有的作业概念实现:
部署(Deployment) 副本集(ReplicaSet) 有状态集(StatefulSet) 守护进程集(DaemonSet) 作业(Job) 在本书的上下文中,我们将使用作业的更通用定义。
一个作业应在上层定义其具体的特性,并作用于它定义的所有任务: 1 构成作业的每个任务是什么? 2 作业应该运行在哪个数据中心? 3 每个任务应运行的实例数量有多少? 4 作业的类型(应持续运行还是运行到完成后停止?)
为了简化起见,我们不会实现作业这种类型的工作负载。相反,我们将只专注于单个任务。
1.5.3 调度器 调度器决定哪台机器最适合托管作业中定义的任务。决策过程可以像从一组机器中以轮询方式选择一个节点一样简单,也可以像增强并行虚拟机(E-PVM)调度器(作为Google的Borg调度器的一部分使用)那样复杂,它基于多个变量计算得分,然后选择得分最高的节点。
调度器应执行以下功能:
确定一组任务可以运行的候选机器 从最好到最差对候选机器进行评分 选择得分最高的机器 我们将在本书后续章节中实现轮询调度器和E-PVM调度器。
1.5.4 管理器 管理器是编排器的大脑,也是用户的主要入口点。为了在编排系统中运行作业,用户将他们的作业提交给管理器。管理器使用调度器找到可以运行作业任务的机器。管理器还会定期从每个工作节点收集指标,这些指标用于调度过程。
管理器应执行以下功能:
1 接受用户启动和停止任务的请求 2 将任务调度到工作节点上 3 监控任务状态以及运行任务的机器
1.5.5 工作节点 工作节点负责运行管理器分配给它的任务。如果任务因任何原因失败,它必须尝试重新启动任务。工作节点还提供其任务和整体机器健康状况的指标,供管理器轮询。
工作节点负责以下任务: 1 以Docker容器的形式运行任务 2 接受来自管理器的任务 3 向管理器提供相关统计数据,以便调度任务 4 监控任务状态
1.5.6 集群 集群是前面提到的所有组件的逻辑分组。一个编排集群可以在单个物理或虚拟机上运行。然而,更常见的是,集群由多台机器组成,数量少至5台,多至数千台甚至更多。
集群是高可用性(HA)和可扩展性等特性发挥作用所在的层级。当你开始使用编排器运行生产作业时,这些特性变得至关重要。出于本书的目的,我们不会详细讨论与我们将要构建的编排器相关的高可用性或可扩展性。然而,请记住,我们的设计和实现方式将影响编排器的部署能力,以满足生产环境的高可用性和可扩展性需求。
1.5.7 命令行界面 最后,我们的CLI作为主要用户界面,应允许用户执行以下操作:
1 启动和停止任务 2 获取任务状态 3 查看机器状态(即工作节点) 4 启动管理器 5 启动工作节点 所有编排系统都共享这些基本组件。Google的Borg(见图1-5)将管理器称为BorgMaster,将工作节点称为Borglet,但其他术语与之前定义的相同。
图1-5 Google的Borg。在底部是多个Borglet或工作节点,它们在容器中运行单个任务。在中间是BorgMaster或管理器,它使用调度器将任务分配到工作节点上。
Apache Mesos(见图1-6)于2009年在Usenix HotCloud研讨会上发布,并于2010年开始被Twitter使用。Mesos将管理器简单地称为master,将工作节点称为agent。然而,它在调度任务的方式上与Borg模型略有不同。它有一个框架的概念,框架有两个组件:一个向master注册以获取资源的调度器,以及一个在agent节点上启动以运行框架任务的执行进程(http://mesos.apache.org/documentation/latest/architecture/)。
图1-6 Apache Mesos
Kubernetes由Google创建,受Borg的影响,将管理器称为控制平面(control plane),将工作节点称为kubelet。它将作业和任务的概念整合为Kubernetes对象。最后,Kubernetes保留了调度器和集群的术语。这些组件可以在Kubernetes架构图中看到(见图1-7)。
图1-7 Kubernetes架构。左侧的控制平面相当于管理器功能或Borg的BorgMaster。
HashiCorp的Nomad在Kubernetes发布一年后发布,使用了更基本的术语。管理器称为服务器(server),工作节点称为客户端(client)。虽然图1-8中未显示,但Nomad使用了我们在此定义的调度器、作业、任务和集群等术语。
图1-8 Nomad的架构。虽然看起来更简洁,但它的功能与其他编排器类似。
1.6 认识Cube 我们把将要实现的系统称为Cube。如果你熟悉电影《星际迷航:下一代》,你会记得Borg乘坐的是一个立方体形状的飞船。
Cube的设计将比Google的Borg、Kubernetes或Nomad简单得多。并且它的可靠性远不及Borg的飞船。然而,它将包含与这些系统相同的所有组件。
图1-9中的思维模型扩展了图1-4中概述的架构。除了更高层次的组件外,它还深入探讨了3个主要组件:管理器、工作节点和调度器。
从图中左下角的调度器开始,我们看到它包含3个框:可行性(Feasibility)、评分(Scoring)和挑选(Picking)。这些框代表了调度器进行调度的一般步骤,调度器按照顺序依次执行,将任务调度到工作节点中:
- 可行性——这个阶段评估是否有可能将任务调度到工作节点上。某些情况下,任务无法调度到任何工作节点上;又或者,任务只能调度到部分工作节点上。我们可以将这个阶段类比为购买汽车。例如,我的预算是1万美元,但根据我去的车行,所有的车可能都超过1万美元,或者只有一部分车符合我的价格范围。
用户通过管理器的API(通常是CLI或Web UI)与系统交互。
管理器通过工作节点的API向其发送任务并从中获取指标。
管理器将系统中的所有任务持久化到数据存储中。
工作节点将其自身的任务持久化到数据存储中。
管理器维护系统中工作节点的列表以及每个工作节点的指标(任务数量、可用内存/磁盘、CPU负载)。
工作节点提供其任务的指标和其他相关系统指标供管理器使用。
管理器通过调度器计算应该将任务放置在哪里。
编排系统
管理器
工作节点
调度器
图1-9 Cube的思维模型。它包含一个管理器、一个工作节点和一个调度器,用户将通过命令行与其交互。
-
评分——这个阶段对可行性阶段识挑选出的工作节点进行评分。这个阶段是最重要的,可以通过多种方式完成。例如,继续我们的购车类比,我可能会根据燃油效率、颜色和安全评级等变量为符合我预算的3辆车打分。
-
挑选——这个阶段是最简单的。调度器从评分列表中挑选出一个最合适的节点。可以是获得最高或最低分数的节点。
图表的上半部分是管理器。管理器组件中的第一个框是调度(Scheduling),使用的是我们之前所描述的调度器。接下来是API,它是与Cube交互的主要机制。用户通过API提交和停止作业。用户还可以通过查询API以获取作业和工作节点的状态信息。接下来是任务存储(Task storage)。管理器必须监控系统中的所有作业,以便做出良好的调度决策,并向用户提供关于作业和工作节点状态的查询结果。最后,管理器还监控工作节点的指标,如工作节点当前运行的作业数量、可用内存量、CPU负载和可用磁盘空间。这些数据与作业存储层中的数据一样,用于调度。
图表中的最后一个组件是工作节点。与管理器类似,它也有一个API,尽管用途不同。这个API的主要用户是管理器。API向管理器提供了一套方法,使管理器能够向工作节点发送、停止任务以及检索工作节点状态指标。接下来,工作节点中存在一个任务运行时组件,在我们的例子中将是Docker。与管理器一样,工作节点也监控其负责运行的任务,这将在任务存储层中实现。最后,工作节点通过暴露API接口提供其自身状态的监控指标。
1.7 我们将使用哪些工具? 为了专注于我们的主要目标,我们将限制使用的工具和库的数量。以下是我们将使用的工具和库列表:
Go chi Docker SDK BoltDB goprocinfo Linux 正如本书标题所提到的,我们将使用Go语言编写代码。Kubernetes和Nomad都是用Go编写的,显然它是大规模系统的合理选择。Go也相对轻量,易于快速学习。如果你以前没有使用过Go,但用C/C++、Java、Rust、Python或Ruby等语言编写过复杂的软件,那么使用Go语言应该没有问题。如果你想要更深入了解Go语言,可以参考《The Go Programming Language》(www.gopl.io/)或《Get Programming with Go》(www.manning.com/books/get-programming-with-go)。 话虽如此,本书中展示的所有代码都可以编译和运行,所以只需跟随书中的内容学习即可。
编写代码时没有特定的IDE要求。任何文本编辑器都可以。使用你最熟悉和喜欢的工具即可。
我们将专注于实现一个支持Docker容器的系统。这是一个设计上的选择。我们可以扩大范围,使我们的编排器能够运行各种作业:容器、独立的可执行文件或Java JAR包。然而,请记住,我们的目标不是构建一个可以与现有编排器竞争的系统。这只是一个学习和练习的过程。将范围缩小到仅关注Docker容器将帮助我们更容易地实现学习目标。我们将使用Docker的Go SDK(https://pkg.go.dev/github.com/docker/docker/client)。
我们的管理器和工作节点将需要一个数据库进行数据存储。为此,我们使用BoltDB(https://github.com/boltdb/bolt),一个嵌入式键/值存储数据库。使用Bolt有两个主要好处。首先,由于它嵌入在我们的代码中,我们不需要运行一个数据库服务器。这意味着我们的管理器和工作节点都不需要通过网络读取或写入数据。其次,使用键/值存储提供了快速、简单的数据访问。
管理器和工作节点将各自提供一个API来暴露其功能。管理器的API主要面向用户,允许系统用户启动和停止作业、查看作业状态以及获取集群节点的概览。工作节点的API是内部的,提供管理器向工作节点发送作业和检索指标的机制。在许多其他语言中,我们可能会使用一个Web框架来实现这样的API。例如,如果我们使用Java,可能会选择Spring;如果使用Python,可能会选择Django。虽然Go也有类似的框架,但它们并不总是必要的。在我们的例子中,我们不需要像Spring或Django这样的完整Web框架。相反,我们将使用一个轻量级的路由器chi(https://github.com/go-chi/chi)。我们将用纯Go语言编写处理程序,并将这些处理程序分配给对应的路由。
为了简化工作节点指标的收集,我们将使用goprocinfo库(https://github.com/c9s/goprocinfo)。这个库能够对从proc文件系统中获取指标的一些操作细节进行抽象。
最后,虽然你可以在任何操作系统上编写本书中的代码,但它最终需要在Linux上编译和运行。任何最近的发行版Linux系统都能够满足需求。
此外,我们将依赖Go及其默认安装的标准工具。由于我们将使用Go modules,所以需要使用Go v1.14或更高版本。我在本书中的代码使用了包括1.20、1.19和1.16等版本的Go。
1.8 关于硬件的说明 你不需要大量硬件来完成本书的内容。只需要在一台机器上就能够完成所有操作,无论是笔记本电脑、台式机,甚至是Raspberry Pi。唯一的要求是这台机器运行Linux操作系统,并且有足够的内存和磁盘空间来存储源代码并进行编译。
如果你打算在一台机器上完成所有操作,还有一些事情需要考虑。你可以运行单个工作节点实例。这意味着当你向管理器提交一个作业时,它会将该作业分配给单个工作节点。实际上,任何作业都会被分配给那个工作节点。为了获得更好的体验,并更好地测试调度器和展示你将要完成的工作,你可以运行多个工作节点实例。一个方法是简单地打开多个终端,并在每个终端中运行一个工作节点实例。或者,你可以使用类似tmux(https://github.com/tmux/tmux)工具,如图1-10所示。
图1-10 一个tmux会话,显示了3个运行Cube工作节点的Raspberry Pi
tmux实现了类似的效果,但它能允许你从终端退出后仍然保持所有程序运行。
如果你有多余的硬件(例如,一台旧笔记本电脑或台式机,或者多个Raspberry Pi),你可以将它们用作你的工作节点。同样,唯一的要求是它们运行Linux操作系统。例如,在为写这本书开发代码时,我使用了八个Raspberry Pi作为工作节点。我用我的笔记本电脑作为管理器。
1.9 不会实现或讨论的内容 再次重申,我们的目的是不是构建一个可以替代生产级系统(例如,Kubernetes)的东西。工程学是一门关于根据需求权衡取舍的学科。这只是一个学习和练习的过程,旨在让我们更好地理解编排器的工作原理。因此,我们不会处理或讨论任何可能涉及到生产级系统的内容: 分布式计算 服务发现 高可用性 负载均衡 安全性
1.9.1 分布式计算 分布式计算是一种架构特性,其中系统的组件运行在不同的计算机上,通过网络进行通信,并需要协调操作和状态。这种特性的主要优点是可扩展性和弹性。编排器本身就是一个分布式系统。它允许工程师将系统扩展到单台计算机资源之外,从而使这些系统能够处理越来越大的工作负载。编排器还通过使工程师能够轻松运行并自动管理多个服务实例,提供了对故障的弹性。
话虽如此,我们不会深入探讨分布式计算的理论。如果你对这个话题特别感兴趣,有很多资源对此进行了详细地介绍。
关于分布式计算的资源: 《Designing Data-Intensive Applications》(http://mng.bz/6nqZ) 《Designing Distributed Systems》(http://mng.bz/5oqZ)
1.9.2 服务发现 服务发现提供了一种机制,使用户(无论是人类还是其他机器)能够发现服务所在的位置。像所有编排系统一样,Cube将允许我们运行一个或多个任务实例。当我们要求Cube运行一个任务时,我们无法提前知道Cube会将任务放置在哪里(即任务将在哪个工作节点上运行)。如果我们有一个包含3个工作节点的集群,任务可能会被调度到这3个节点中的任何一个。
为了帮助找到已调度并运行的任务,我们可以使用服务发现系统(例如,Consul;www.consul.io)。虽然服务发现在较大的编排系统中是不可或缺的,但对于我们当前的目标来说并不是必要的。
关于服务发现的资源: 《Service Discovery in a Microservices Architecture》(http://mng.bz/0lpz/) 《Service Discovery in Nomad》(http://mng.bz/W1yX) 《Service Discovery in Kubernetes》(http://mng.bz/84Pg)
1.9.3 高可用性 可用性一词指的是系统可供其预期用户使用的时间量。你经常会听到高可用性(HA)这个术语,它指的是最大化系统可用时间的策略。几个高可用策略的例子: 通过冗余消除单点故障 自动检测和恢复故障 隔离故障以防止系统完全中断 编排系统本质上是一个工具,使工程师能够实现这些策略。通过运行多个实例,例如一个关键任务的Web API,我可以确保如果其中一个实例因某种原因宕机,API不会完全不可用。通过在编排器上运行多个Web API实例,确保如果其中一个实例因某种原因失败,编排器将检测到并尝试从故障中恢复。如果Web API的任何一个实例失败,不会影响其他实例(有一些例外情况;见下文讨论)。
同时,使用这些策略来部署编排系统自身是非常常见的做法。生产编排系统通常使用多个工作节点。例如,Borg集群中的工作节点数量达到数万个。通过运行多个工作节点,系统允许用户在不同的机器上运行多个关键任务Web API实例。如果运行Web API的其中一台机器发生灾难性故障(例如,一只老鼠在机器的机架中安家并意外拔掉了机器的电源线),应用程序仍然可以为用户服务。
对于本书的目的,我们将实现自己的编排器,使多个工作节点实例可以像Google的Borg一样轻松运行。然而,对于管理器,我们只会运行一个实例。因此,虽然我们的工作节点可以以高可用的方式运行,但我们的管理器不能。这是为什么呢?
我们编排系统的管理器和工作节点组件(任何编排系统的管理器和工作节点组件)有不同的职责范围。工作节点的职责范围很小,只需关注它负责运行的任务。如果工作节点2因某种原因发生了故障,工作节点1并不关心。不仅不关心,它甚至不知道工作节点2的存在。
然而,管理器的职责范围涵盖了整个编排集群。它维护集群的状态:有多少个工作节点,每个工作节点的状态(CPU、内存和磁盘容量,以及使用量),以及用户提交的每个任务的状态。要运行多个管理器实例,还有许多问题需要回答: 在管理器实例之间,是否会有一个领导者来处理所有管理任务,还是任何管理器实例都可以处理这些管理任务? 状态更新如何复制到每个管理器实例? 如果状态数据不同步,管理器如何决定使用哪些数据? 这些问题最终引出了共识问题,这是分布式系统中的一个基本问题。虽然这个话题很有趣,但它对我们学习和理解编排系统的工作原理并不是至关重要的。如果我们的管理器宕机,不会影响我们的工作节点。它们将继续运行已经分配给它们的任务。这确实意味着我们的集群将无法接受新任务,但对于练习目的而言,我们认为这是可以接受的。
关于高可用性的资源: 《An Introduction to High Availability Computing: Concepts and Theory》(http://mng.bz/mj5y) 《Learn Amazon Web Services in a Month of Lunches》(http://mng.bz/7vqV) 关于共识的资源: 《Consensus: Reaching Agreement》(http://mng.bz/qjvN) 《Paxos Made Simple》(http://mng.bz/K9Qn) 《The Raft Consensus Algorithm》(https://raft.github.io/)
1.9.4 负载均衡 负载均衡是一种构建高可用、可靠和响应迅速的应用程序的策略。常见的负载均衡器(LB)包括NGINX、HAProxy和AWS的各种负载均衡器(经典弹性负载均衡器、网络负载均衡器和较新的应用负载均衡器)。虽然它们通常与编排器一起使用,但往往会很复杂,因为它们使用的方式五花八门。
例如,通常会有一个公开的负载均衡器,作为系统的入口点。这个负载均衡器可以感知编排系统中的每个节点,并将请求转发到其中一个节点中。接收此请求的节点本身运行了一个集成了服务发现系统的负载均衡器,因此可以将请求转发到集群中运行任务的节点,该节点可以处理请求。
负载均衡本身也是一个很复杂的话题。它可以简单地使用轮询算法,负载均衡器维护了一个集群中节点的列表和一个指向最后一次所选择节点的指针。当请求到来时,负载均衡器从列表中选择下一个节点。或者,它可以复杂地选择一个最能满足某些标准的节点,例如可用资源最多或连接数最少的节点。虽然负载均衡是构建高可用生产系统的重要工具,但它不是编排系统的基本组件。
关于负载均衡的资源: 《Quick Introduction to Load Balancing and Load Balancers》(http://mng.bz/9QW8) 《Types of Load Balancing Algorithms》(http://mng.bz/wjaB)
1.9.5 安全性 安全性就像洋葱,有很多层,比本书中所覆盖的要多得多。如果我们要在生产环境中运行编排器,需要回答以下问题: 如何确保只有经过身份验证的用户才能提交任务或执行其他管理操作? 是否应该使用授权来划分用户及其可以执行的操作? 如何确保工作节点只接受来自管理器的请求? 管理器和工作节点之间的网络流量是否应该加密? 系统应该如何记录事件以审计谁在何时做了什么?
关于安全性的资源: 《API Security in Action》(https://www.manning.com/books/api-security-in-action) 《Security by Design》(https://www.manning.com/books/secure-by-design) 《Web Application Security》(http://mng.bz/Jdqz) 在下一章中,我们将通过思维模型转化为代码骨架来开始编码。
总结 编排器将机器和操作系统抽象化,使开发人员能够专注于开发应用程序本身。 编排器是由管理器、工作节点和调度器组成的系统。编排系统的主要对象是任务和作业。 编排器作为一组机器的集群运行,这些机器承担管理器和工作节点的角色。 在编排系统中,应用程序通常运行在容器中。 编排器能够实现以往难以实现的标准化和自动化。