本书目前没有找到中文版,自己借助 AI 翻译本书并尝试从 0 到 1 跑通整个项目。
完整代码仓库:github.com/1055373165/…
本书的第一部分将为你开启从零编写编排系统的征程奠定基础!
在第一章里,你会了解构成所有编排系统的核心组件。借助这些核心组件,你将构建出 Cube 编排器的概念模型,在本书后续章节中,我们会一同将其实现。
第二章会指导你依据在第一章学到的概念模型来创建代码框架。
第三章中,你要对任务对象的代码框架进行详细完善。通过这个实践,你会明白我们实现 Cube 其余代码库所采用的流程。
1 什么是编排器?
本章涵盖以下内容:
-
应用程序部署方式的演变
-
对编排系统的组件进行分类
-
引入编排器的概念模型
-
明确我们的编排器的需求
-
确定我们的工作范围
Kubernetes(K8s),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 行代码来实现自己的编排器。
1.2 (并非那么)美好的往昔岁月
让我们回到 2002 年,认识一下米歇尔。米歇尔是她所在公司的一名系统管理员,她负责让公司的应用程序全天候保持运行。她是如何做到这一点的呢?
和许多其他系统管理员一样,米歇尔采用了在裸机服务器上部署应用程序的常见策略。米歇尔所处世界的简易示意图如图 1.1 所示。
图 1.1 该图表示米歇尔 2002 年的世界。外框代表物理机器及其上运行的操作系统。内框代表机器上运行的应用程序,展示了应用程序过去是如何与操作系统和机器更直接地联系在一起的。
每个应用程序通常都在其自己的物理硬件上运行。更麻烦的是,每个应用程序都有其独特的硬件要求,所以米歇尔必须购买并管理为每个应用程序量身定制的服务器集群。此外,每个应用程序都有其独特的部署流程和工具。数据库团队通过光盘收到新版本和更新内容,所以其流程包括数据库管理员(DBA)将文件从光盘复制到中央服务器,然后使用一组自定义的 shell 脚本将文件推送到数据库服务器,在那里另一组 shell 脚本负责安装和更新。米歇尔亲自处理公司财务系统的安装和更新。这个过程包括从互联网上下载软件,至少这让她免去了处理光盘的麻烦。但财务软件有其自己的一套用于安装和管理更新的工具。还有其他几个团队在开发公司的软件产品,这些团队开发的应用程序又有着完全不同的一套工具和程序。
如果你在那个时期没有在这个行业工作过,也没有经历过类似米歇尔所处的那种情况,那你算很幸运了。那个世界不仅混乱且难以管理,还极其浪费资源。在 21 世纪初到中期,虚拟化技术出现了。这些工具让像米歇尔这样的系统管理员能够分割他们的物理服务器集群,这样每台物理机器上就能托管几台更小但独立的虚拟机(VM)。不再是每个应用程序在其专用的物理机器上运行,而是在虚拟机上运行。而且多个虚拟机可以被打包到一台物理机器上。虽然虚拟化让像米歇尔这样的人日子好过了一些,但它并非万灵药。
事情一直是这样,直到 21 世纪 10 年代中期,两项新技术出现了。第一项是 Docker,它将容器技术推广到了更广泛的领域。容器的概念并不是新的,它从 1979 年就存在了(可查看埃尔・马尔克斯(Ell Marquez)的《容器技术的历史》,网址:mng.bz/oro2)。在 Docker 出现之前,容器大多局限于像太阳微系统公司(Sun Microsystems)和谷歌这样的大公司,以及那些寻找有效且安全地为客户提供虚拟化环境方法的托管服务提供商。在这个时期出现的第二项新技术是 Kubernetes,这是一个专注于自动化容器部署和管理的容器编排器。
1.3 什么是容器,它与虚拟机有何不同?
如前文所述,从米歇尔早期的物理机和操作系统的世界迈出的第一步是引入虚拟机。虚拟机(VM)对计算机的物理组件(CPU、内存、磁盘、网络、光盘驱动器等)进行了抽象,这样管理员就可以在一台物理机上运行多个操作系统。在物理机上运行的每个操作系统都是独立的,各自有自己的内核、网络栈和资源(例如 CPU、内存、磁盘 )。
虚拟机在成本和效率方面有了巨大的提升。然而,成本和效率的提升仅体现在机器和操作系统层面。在应用程序层面,变化不大。如图 1.2 所示,应用程序仍然与操作系统紧密耦合。如果你想运行应用程序的两个或更多实例,就需要两个或更多的虚拟机。
图 1.2 在虚拟机上运行的应用程序
与虚拟机不同,容器没有内核,没有自己的网络栈,也不控制 CPU、内存和磁盘等资源。实际上,“容器” 这个术语只是一个概念,它不像虚拟机那样是一个具体的技术实体。
“容器” 这个术语实际上只是 Linux 内核中进程和资源隔离的简称。所以当我们谈论容器时,实际上谈论的是命名空间(namespaces)和控制组(cgroups),这两者都是 Linux 内核的特性。命名空间是一种将进程及其资源相互隔离的机制,控制组则为一组进程提供资源限制和统计功能。
不过我们先不要过于纠结这些底层细节,在阅读本书后续内容时,你并不需要了解命名空间和控制组的知识。但如果你感兴趣,我建议你观看利兹・赖斯(Liz Rice)的演讲《从零开始理解容器》(www.youtube.com/watch?v=8fi…
随着容器的引入,应用程序可以与操作系统层解耦,如图 1.3 所示。有了容器,如果我有一个应用程序启动了一个监听 80 端口的服务器进程,现在我可以在一台物理主机上运行该应用程序的多个实例。或者假设我有六个不同的应用程序,每个都有自己监听 80 端口的服务器进程。同样,有了容器,我可以在同一主机上运行这六个应用程序,而无需在应用层为每个应用程序分配不同的端口。
图 1.3 运行在容器中的应用程序
容器真正的优势在于,它让应用程序觉得自己是运行在操作系统上的唯一应用程序,因此可以访问操作系统的所有资源。
表 1.1 米歇尔的新旧世界
1.5 编排系统的组件
所以编排器能够实现容器部署、扩展和管理的自动化。接下来,让我们确定那些使这些功能得以实现的通用组件及其要求,如下:
- The task(任务)
- The job(作业)
- The scheduler(调度器)
- The manager(管理器)
- The worker(工作者)
- The cluster(集群)
- The command-line interface(CLI,命令行接口)
这些组件中的一些可以在图 1.4 中看到:
图 1.4 编排系统的基本组件。无论不同的编排器使用何种术语,每个编排器都有一个调度器、一个管理器和一个工作节点,并且它们都围绕任务进行运作。
1.5.1 The task(任务)
任务是编排系统中最小的工作单元,通常在容器中运行。你可以把它想象成在单台机器上运行的一个进程。单个任务可以运行像 NGINX 这样的反向代理实例,也可以运行像 RESTful API 服务器这样的应用程序实例;它还可能是一个简单的程序,进入无限循环并执行一些特定操作,比如 ping 一个网站并将结果写入数据库。
一个任务应该指定以下内容:
- 有效运行所需的内存、CPU 和磁盘资源量。
- 出现故障时编排器应采取的操作,通常称为重启策略。
- 用于运行该任务的容器镜像名称。
任务定义可能会指定更多详细信息,但这些是核心要求。
1.5.2 The job(作业)
作业是任务的集合。它包含一个或多个任务,这些任务通常构成一个更大的逻辑任务组,以执行一系列功能。例如,一个作业可以由一个 RESTful API 服务器和一个反向代理组成。
Kubernetes 与作业的概念
如果你只熟悉 Kubernetes,那么一开始可能会对这里关于作业的定义感到困惑。在 Kubernetes 的世界里,作业是一种特定类型的工作负载,传统上被称为批处理作业,即启动后运行直至完成的作业。Kubernetes 有多种资源类型,它们是 Kubernetes 针对作业概念的特定实现:
- 部署(Deployment)
- 副本集(ReplicaSet)
- 有状态集(StatefulSet)
- 守护进程集(DaemonSet)
- 作业(Job)
在本书的语境中,我们将使用作业更通用的定义。
一个作业应该在高层次上指定详细信息,这些信息将应用于它定义的所有任务:
-
组成该作业的每个任务。
-
作业应该在哪些数据中心运行。
-
每个任务应该运行多少个实例。
-
作业的类型(它应该持续运行,还是运行完成后停止?)
为了简单起见,在我们的实现中不会涉及作业。相反,我们将只专注于单个任务层面的工作。
1.5.3 The scheduler
调度器负责决定哪台机器最适合托管作业中定义的任务。决策过程可能简单到以轮询的方式从一组机器中选择节点,也可能复杂到像增强型并行虚拟机(E - PVM)调度器(作为谷歌 Borg 调度器的一部分使用)那样,基于多个变量计算得分,然后选择得分最高的节点。
调度器应执行以下功能:
-
确定任务可以运行的一组候选机器。
-
对候选机器进行评分,从最优到最劣排序。
-
选择得分最高的机器。
在本书后续内容中,我们将同时实现轮询调度器和 E - PVM 调度器。
1.5.4 The manager
管理器是编排器的核心,也是用户的主要入口。为了在编排系统中运行作业,用户会将他们的作业提交给管理器。然后,管理器会借助调度器找到一台能够运行该作业任务的机器。管理器还会定期从各个工作节点收集指标,这些指标会在调度过程中使用。
管理器应执行以下操作:
- 接受用户启动和停止任务的请求。
- 将任务调度到工作节点机器上。
- 跟踪任务、任务的状态以及任务运行所在的机器。
1.5.5 The worker
工作节点是编排器的执行单元。它负责运行由管理器分配给它的任务。如果某个任务由于任何原因失败,工作节点必须尝试重新启动该任务。工作节点还会提供关于其任务以及所在机器整体健康状况的指标,以便管理器进行轮询获取。
工作节点需承担以下职责:
- 以 Docker 容器的形式运行任务。
- 接受管理器分配的待运行任务。
- 向管理器提供相关统计信息,以便进行任务调度。
- 跟踪自身所运行的任务及其状态。
1.5.6 The cluster
集群是上述所有组件的逻辑组合。一个编排集群可以在单个物理机或虚拟机上运行。不过,更常见的情况是,集群由多台机器构建而成,机器数量少则五台,多则可达数千台甚至更多。
在集群层面,诸如高可用性(HA)和可扩展性等问题开始发挥作用。当你开始使用编排器来运行生产作业时,这些问题就变得至关重要。就我们的目标而言,我们不会详细讨论与我们要构建的编排器相关的高可用性或可扩展性问题。然而,请记住,我们所做出的设计和实现选择,将影响到以满足生产环境中高可用性和可扩展性需求的方式来部署我们的编排器的能力。
1.5.7 Command-Line Interface(CLI, 命令行界面)
最后,我们的命令行界面作为主要的用户界面,应该能让用户做到以下几点:
-
启动和停止任务。
-
获取任务的状态。
-
查看机器(即工作节点)的状态。
-
启动管理器。
-
启动工作节点。
所有的编排系统都具备这些相同的基本组件。如图 1.5 所示的谷歌的 Borg 系统,它把管理器称为 BorgMaster,把工作节点称为 Borglet,但除此之外,使用的术语与之前定义的是一样的。
图 1.5 谷歌的 Borg 系统。在底部是多个 Borglet(即工作节点),它们在容器中运行单个任务。中间部分是 BorgMaster(即管理器),它利用调度器将任务分配到各个工作节点上。
如图 1.6 所示的 Apache Mesos,于 2009 年在 Usenix HotCloud 研讨会上亮相,并从 2010 年起被推特(Twitter)所使用。Mesos 将管理器简单地称为 “主节点(master)” 。在任务调度方面,它与 Borg 模型存在相似之处。Mesos 有一个 “框架(framework)” 的概念,该框架包含两个组件:一个是向主节点注册以获取资源的调度器,另一个是在代理节点上启动以运行框架任务的执行器进程(详情可查阅mesos.apache.org/documentati…)。
图 1.6 Apache Mesos
Kubernetes 由谷歌创建,并受到了 Borg 的影响。它将管理器称为 “控制平面(control plane)”,将工作节点称为 “kubelet”。它把 “作业(job)” 和 “任务(task)” 的概念整合进了 Kubernetes 对象中。最后,Kubernetes 保留了 “调度器(scheduler)” 和 “集群(cluster)” 这两个术语的用法。这些组件可以在图 1.7 的 Kubernetes 架构图中看到。
图 1.7 Kubernetes 架构。在图左侧看到的控制平面,其功能等同于管理器,或者说类似于 Borg 的 BorgMaster。
HashiCorp 公司的 Nomad 在 Kubernetes 发布一年后推出,它使用了更为基础的术语。管理器被称作 “服务器(server)”,工作节点被称作 “客户端(client)”。尽管图 1.8 中没有展示,但 Nomad 使用了我们在此所定义的 “调度器(scheduler)”、“作业(job)”、“任务(task)” 和 “集群(cluster)” 这些术语。
图 1.8 Nomad 的架构。虽然它看起来更为简洁,但它的功能与其他编排器仍有相似之处。
1.6 认识 Cube
我们将把我们的实现命名为 Cube(立方体)。如果你熟悉《星际迷航:下一代》的相关内容,你就会记得博格人乘坐的是一艘立方体形状的宇宙飞船。
Cube 的设计会比谷歌的 Borg、Kubernetes 或 Nomad 简单得多。而且它远不会像博格人的飞船那样具有强大的适应能力。不过,它会包含与那些系统相同的所有组件。
图 1.9 中的概念模型是对图 1.4 中所概述的架构的进一步拓展。除了更高级别的组件之外,它还更深入地探讨了三个主要组件:管理器、工作节点和调度器。
图 1.9 Cube 的概念模型。它有一个管理器、一个工作节点和一个调度器,并且用户(也就是你)将通过命令行与它进行交互。
从图中左下角的调度器开始看,我们看到它包含三个部分:可行性评估、打分和选择。这些部分代表了调度器的一般阶段,并且它们按照调度器将任务调度到工作节点上的过程顺序排列:
-
可行性评估:这个阶段评估是否有可能将一个任务调度到一个工作节点上。会存在一些情况,即一个任务无法调度到任何工作节点上;也会有一些情况,一个任务可以被调度,但只能调度到一部分工作节点上。我们可以把这个阶段想象成选择购买哪辆车。我的预算是 1 万美元,但根据我去的不同车行,车行里所有的车可能都超过 1 万美元,或者只有一部分车在我的预算范围内。
-
打分:这个阶段对可行性评估阶段确定的工作节点进行打分。这个阶段是最重要的,并且可以通过多种方式完成。例如,继续我们买车的类比,我可能会根据燃油效率、颜色和安全评级等变量,给三辆在我预算范围内的车分别打分。
-
选择:这个阶段是最简单的。调度器从打分列表中选择最好的一个。这可能是最高分或者最低分。
再看向上图中的管理器部分。管理器组件内的第一个部分表明管理器使用我们之前描述的调度器。接下来是 API 部分。API 是与 Cube 交互的主要机制。用户通过 API 提交作业并请求停止作业。用户还可以查询 API 以获取有关作业和工作节点状态的信息。再接下来是任务存储部分。管理器必须跟踪系统中的所有作业,以便做出良好的调度决策,同时也为了回答用户关于作业和工作节点状态的查询。最后,管理器还跟踪工作节点的指标,例如一个工作节点当前正在运行的作业数量、可用内存量、CPU 的负载情况以及可用磁盘空间量。这些数据,就像作业存储层中的数据一样,用于调度。
图中的最后一个组件是工作节点。和管理器一样,它也有一个 API,尽管其用途不同。这个 API 的主要使用者是管理器。API 为管理器提供了向工作节点发送任务、告诉工作节点停止任务以及检索有关工作节点状态指标的方法。接下来,工作节点有一个任务运行时环境,在我们的例子中是 Docker。和管理器一样,工作节点也跟踪它所负责的工作,这是在任务存储层中完成的。最后,工作节点提供关于自身状态的指标,并通过其 API 提供这些指标。
1.7 我们将使用哪些工具?
为了专注于我们的主要目标,我们将限制所使用的工具和库的数量。以下是我们要使用的工具和库的列表:
-
Go 编程语言
-
chi(一个轻量级路由器)
-
Docker SDK(软件开发工具包)
-
BoltDB(嵌入式键值存储数据库)
-
goprocinfo(一个库)
-
Linux 操作系统
正如本书的标题所示,我们将使用 Go 编程语言来编写代码。Kubernetes 和 Nomad 都是用 Go 编写的,所以对于大规模系统来说,Go 显然是一个合理的选择。Go 语言相对轻量级,便于快速学习。如果你以前没有使用过 Go,但使用过诸如 C/C++、Java、Rust、Python 或 Ruby 等语言编写过较为复杂的软件,那么你应该能够轻松上手。如果你想要关于 Go 语言的更深入的资料,《Go 编程语言》(www.gopl.io/)或《Go 编程实战》(www.manning.com/books/get-p…)都是不错的资源。也就是说,书中展示的所有代码都可以编译和运行,所以只要跟着做应该就没问题。
编写代码对集成开发环境(IDE)没有特别要求。任何文本编辑器都可以。使用你最习惯、最顺手的编辑器就好。
我们的系统将专注于支持 Docker 容器。这是一个设计选择。我们本可以扩大系统的范围,让编排器能够运行各种类型的作业,比如容器、独立可执行文件或 Java JAR 文件。然而,请记住,我们的目标不是构建一个能与现有编排器竞争的东西。这只是一个学习练习。将范围缩小到只专注于 Docker 容器将有助于我们更轻松地实现学习目标。也就是说,我们将使用 Docker 的 Go SDK(pkg.go.dev/github.com/…)。
我们的管理器和工作节点将需要一个数据存储。为此,我们将使用 BoltDB(github.com/boltdb/bolt),这是一个嵌入式键值存储。使用 BoltDB 有两个主要好处。首先,由于它嵌入在我们的代码中,我们不需要运行数据库服务器。这一特性意味着我们的管理器和工作节点都不需要通过网络来读写数据。其次,使用键值存储可以快速、简单地访问我们的数据。
管理器和工作节点都将提供一个 API 来公开它们的功能。管理器的 API 主要面向用户,允许系统用户启动和停止作业、查看作业状态以及获取集群中节点的概览。工作节点的 API 是面向内部的,它将提供一种机制,使管理器能够将作业发送给工作节点并从中检索指标。在许多其他语言中,我们可能会使用一个 Web 框架来实现这样的 API。例如,如果我们使用 Java,可能会使用 Spring;如果使用 Python,可能会选择 Django。虽然 Go 也有这样的框架,但并不总是必要的。在我们的例子中,我们不需要像 Spring 或 Django 这样完整的 Web 框架。相反,我们将使用一个名为 chi(github.com/gochi/chi)的轻量级路由器。我们将用普通的 Go 语言编写处理程序,并将这些处理程序分配给不同的路由。
为了简化工作节点指标的收集,我们将使用 goprocinfo 库(github.com/c9s/goproci…)。这个库将抽象掉一些与从 proc 文件系统获取指标相关的细节。
最后,虽然你可以在任何操作系统上编写本书中的代码,但它需要在 Linux 上编译和运行。任何较新的 Linux 发行版应该都足够了。
对于其他所有方面,我们将依赖 Go 语言及其默认安装在每个 Go 版本中的标准工具。由于我们将使用 Go 模块,你应该使用 Go v1.14 或更高版本。我在开发本书中的代码时使用了 1.20、1.19 和 1.16 版本。
1.8 关于硬件的说明
完成本书的学习内容,你不需要大量的硬件设备。你可以在一台机器上完成所有操作,无论是笔记本电脑、台式电脑,甚至是树莓派都可以。唯一的要求是这台机器运行的是 Linux 系统,并且有足够的内存和磁盘空间来存储和编译源代码。
如果你打算在一台机器上完成所有操作,还有一些额外的事情需要考虑。你可以运行单个工作节点实例。这意味着当你向管理器提交一个作业时,它会将该作业分配给这个单个的工作节点。就此而言,任何作业都会被分配给这个工作节点。为了获得更好的体验,同时也能更好地测试调度器并展示你将要完成的工作,你可以运行多个工作节点实例。一种方法是简单地打开多个终端,并在每个终端中运行一个工作节点实例。或者,你可以使用像 tmux(github.com/tmux/tmux)这样的工具,如图 1.10 所示,它能达到类似的效果,而且还允许你从终端分离,让所有操作继续运行。
如果你身边有闲置的硬件设备(比如一台旧的笔记本电脑、台式电脑或者几台树莓派),你可以将它们用作工作节点。同样,唯一的要求是这些设备运行的是 Linux 系统。例如,在为编写本书而开发代码的过程中,我使用了八台树莓派作为工作节点,用我的笔记本电脑作为管理器。
1.9.1 分布式计算
分布式计算是一种架构风格,在这种风格中,系统的组件运行在不同的计算机上,通过网络进行通信,并且必须协调操作和状态。这种风格的主要优点是可扩展性和对故障的弹性。编排器就是一个分布式系统。它使工程师能够扩展系统,使其超出单个计算机的资源限制,从而使这些系统能够处理越来越大的工作负载。编排器还通过让工程师相对容易地运行其服务的多个实例,并以自动化的方式管理这些实例,来提供对故障的弹性。
也就是说,我们不会深入探讨分布式计算的理论。如果你特别对这个主题感兴趣,有很多资源可以详细介绍这个主题。
关于分布式计算的资源包括:
- 《设计数据密集型应用》(mng.bz/6nqZ)
- 《设计分布式系统》(mng.bz/5oqZ)
1.9.2 服务发现
服务发现为用户(可以是人,也可以是其他机器)提供了一种发现服务位置的机制。和所有的编排系统一样,Cube 将允许我们运行一个或多个任务实例。当我们请求 Cube 为我们运行一个任务时,我们无法提前知道 Cube 会将任务放置在哪里(也就是说,该任务将在哪个工作节点上运行)。如果我们有一个包含三个工作节点的集群,一个任务有可能被调度到这三个节点中的任何一个上。
为了在任务被调度并运行后帮助找到它们,我们可以使用一个服务发现系统(例如,Consul;www.consul.io)来回答关于如何访问某个服务的查询。虽然服务发现在大型编排系统中是必不可少的,但对于我们的目标来说,它并不是必需的。
关于服务发现的资源包括:
- 《微服务架构中的服务发现》(mng.bz/0lpz/)
- 《Nomad 中的服务发现》(mng.bz/W1yX)
- 《Kubernetes 中的服务发现》(mng.bz/84Pg)
1.9.3 高可用性
“可用性” 这个术语指的是一个系统可供其目标用户群体使用的时间量。你经常会听到 “高可用性(HA)” 这个术语,它指的是将系统可供用户使用的时间最大化的策略。高可用性策略的几个例子如下:
-
通过冗余消除单点故障;
-
自动检测故障并从故障中恢复;
-
隔离故障以防止系统整体停机。
编排系统从设计上来说,就是一种使工程师能够实施这些策略的工具。比如说,通过运行多个关键任务的 Web API 实例,我可以确保如果其中一个实例由于某种原因出现故障,该 API 不会对我的用户完全不可用。通过在编排器上运行多个 Web API 实例,我可以确保如果其中一个实例确实因某种原因失败,编排器将检测到它并尝试从故障中恢复。如果我的 Web API 的任何一个实例失败,在某些例外情况下(见以下讨论),该故障不会影响其他实例。
同时,使用这些策略来部署编排系统本身也是很常见的。生产环境中的编排系统通常会使用多个工作节点。例如,Borg 集群中的工作节点数量达到数万个。通过运行多个工作节点,系统允许像我这样的用户在许多不同的机器上运行多个关键任务的 Web API 实例。如果运行我的 Web API 的其中一台机器遭遇灾难性故障(也许一只老鼠住在机器的机架里,不小心拔掉了机器的电源线),我的应用程序仍然可以为用户提供服务。
就本书的目标而言,我们将实现我们的编排器,以便能够像谷歌的 Borg 那样轻松运行多个工作节点实例。然而,对于管理器,我们将只运行一个实例。那么,为什么我们的工作节点可以以高可用的方式运行,而管理器却不能呢?
我们的编排系统(实际上任何编排系统都是如此)的管理器和工作节点组件具有不同的作用范围。
工作节点的作用范围较窄,只关心它负责运行的任务。如果工作节点 2 由于某种原因出现故障,工作节点 1 不会在意。它不仅不在意,甚至根本不知道工作节点 2 的存在。
然而,管理器的作用范围涵盖了整个编排集群。它维护着集群的状态:有多少个工作节点、每个工作节点的状态(CPU、内存和磁盘容量,以及这些容量已经被使用了多少),以及用户提交的每个任务的状态。要运行多个管理器实例,就会产生更多需要考虑的问题:
-
在多个管理器实例中,是否会有一个主管理器来处理所有的管理职责,还是任何一个管理器实例都可以处理这些职责?
-
状态更新是如何复制到每个管理器实例的?
-
如果状态数据不同步,管理器如何决定使用哪一个数据?
这些问题最终会引出 “共识” 这个话题,这是分布式系统中的一个基本问题。虽然这个话题很有趣,但对于我们学习和理解编排系统的工作原理来说,它并不是关键的。如果我们的管理器出现故障,它不会影响我们的工作节点。工作节点将继续运行已经分配给它们的任务。这确实意味着我们的集群将无法接受新的任务,但就我们目前的学习目的而言,我们认为这种情况对于手头的练习来说是可以接受的。
关于高可用性的资源包括:
-
《高可用性计算简介:概念与理论》(mng.bz/mj5y)
-
《在一个月的午餐时间内学习亚马逊网络服务》(mng.bz/7vqV)
关于共识的资源包括:
- 《共识:达成一致》(mng.bz/qjvN)
- 《Paxos 算法详解》(mng.bz/K9Qn)
- 《Raft 共识算法》(raft.github.io/)
1.9.4 Load balancing
- 负载均衡的作用及常用工具:负载均衡是构建高可用、可靠和响应迅速的应用程序的策略,常用的负载均衡器有 NGINX、HAProxy 以及 AWS 的各类负载均衡器(经典弹性负载均衡器、网络负载均衡器和较新的应用负载均衡器)。
- 负载均衡的复杂应用方式:通常会有一个面向公众的负载均衡器作为系统的入口点,它知晓编排系统中的每个节点,并将请求转发到其中一个节点。接收请求的节点自身也运行着与服务发现系统集成的负载均衡器,进而能将请求转发到集群中可处理该请求的任务节点。
- 负载均衡算法的复杂性:简单的如轮询算法,负载均衡器维护一个集群节点列表和一个指向上次所选节点的指针,请求到来时选择列表中的下一个节点;复杂的则是根据资源可用情况或连接数最少等标准来选择最佳节点。
- 负载均衡与编排系统的关系:负载均衡是构建高可用生产系统的重要工具,但不是编排系统的基本组成部分。
- 相关参考资源:“Quick Introduction to Load Balancing and Load Balancers”(mng.bz/9QW8)和 “Types of Load Balancing Algorithms”(mng.bz/wjaB)。
分享
用自己的语言解释一下负载均衡的概念
编排系统的主要对象有哪些?
常见的负载均衡算法有哪些?
1.9.5 安全性
安全性就像洋葱一样,有着许多层面,其复杂程度远超我们在本书中所能合理涵盖的范围。如果要在生产环境中运行我们的编排器,就需要回答以下这些问题:
- 如何确保管理器的安全性,使得只有经过身份验证的用户才能提交任务或执行其他管理操作?
- 是否应该使用授权机制来区分用户及其可执行的操作?
- 如何保障工作节点的安全,使其仅接受来自管理器的请求?
- 管理器和工作节点之间的网络流量是否应该加密?
- 系统应如何记录事件,以便审计何人在何时执行了何种操作?
关于安全性的参考资源包括:
- 《API 安全实战》(www.manning.com/books/api-s…)
- 《设计即安全》(www.manning.com/books/secur…)
- 《Web 应用安全:现代 Web 应用的攻击与防范》(mng.bz/Jdqz)
在下一章中,我们将开始编码,把概念模型转化为框架代码。
总结
-
编排器将机器和操作系统对开发者进行了抽象,使开发者能够专注于应用程序本身。
-
编排器是一个由管理器、工作节点和调度器组成的系统。编排系统的主要对象是任务和作业。
-
编排器以机器集群的方式运行,其中的机器分别承担管理器和工作节点的角色。
-
在编排系统中,应用程序通常在容器中运行。
-
编排器实现了一定程度的标准化和自动化,这在过去是很难实现的。