分布式体系结构之集中式结构

云这个话题对我们来说已经非常熟悉了。可以说,云在我们的生活中无处不在,比如我们平时看的视频通常就是放在云上的。当我们要播放一段视频时,请求会先转发到云上,从云上下载数据到本地,然后播放。在这里,你肯定会疑惑,云上资源那么丰富吗,可以存放这么多东西吗?云上的资源确实丰富,因为它可以尽可能地把更多的服务器组织起来,作为一个统一的资源,为多个用户提供服务。这里的重点是,把多个服务器管理起来,作为一个统一的资源提供服务。而如何组织,就是分布式体系结构的范畴了。

你会发现,很多场景下,我们的请求都会汇总到一台服务器上,由这台服务器统一协调我们的请求和其他服务器之间的关系。这种由一台服务器统一管理其他服务器的方式,就是分布式体系结构中的集中式结构(也称为 Master/Slave 架构),其中统一管理其他服务器的服务器是主,其他服务器是从,可以形象地比喻为“一人在上,万人在下”。

经典集中式架构

集中式结构就是,由一台或多台服务器组成中央服务器,系统内的所有数据都存储在中央服务器中,系统内所有的业务也均先由中央服务器处理。多个节点服务器与中央服务器连接,并将自己的信息汇报给中央服务器,由中央服务器统一进行资源和任务调度:中央服务器根据这些信息,将任务下达给节点服务器;节点服务器执行任务,并将结果反馈给中央服务器。

集中式结构最大的特点,就是部署结构简单。这是因为,集中式系统的中央服务器往往是多个具有较强计算能力和存储能力的计算机,为此中央服务器进行统一管理和调度任务时,无需考虑对任务的多节点部署,而节点服务器之间无需通信和协作,只要与中央服务器通信协作即可,具体示意图如下所示:

现在,我们理解了什么是集中式结构,为了加深理解,接下来我以 Google Borg、Kubernetes 和 Apache Mesos 三个经典的集群管理系统为例,带你深入学习集中式结构的原理。

Google Borg

Borg 是 Google 内部使用的集群管理系统,采用了典型的集中式结构,负责提交、调度、开始、重启和管理 Google 运行在其上的所有应用。在 Borg 中,一个集群称为一个 Cell,每个 Cell 里面有一个 Leader,称为 BorgMaster,即为中央服务器;其他服务器为节点服务器或从服务器,被称为 Borglet。

首先,我们一起看看BorgMaster。它由两个进程组成,一个是 Borgmaster 主进程,一个是独立的 scheduler 进程:

主进程处理客户端的 RPC 请求,比如任务的执行状态更新或者查询等;同时,管理系统中所有实体的状态(比如,服务器、任务等),并负责和 Borglet 通信。

scheduler 进程负责任务调度,通过任务对资源的需求以及当前 Borglet 所在服务器的资源情况进行匹配,为任务寻找一个合适的节点服务器执行。我会在第 11 篇文章“分布式调度之单体调度:物质文明、精神文明一手抓”中与你详细讲述具体的调度流程。

接下来,我们一起看看 Borglet。它是运行在每个节点机器的一个 agent,负责任务的拉起、停止、重启等,并管理和搜集本服务器资源,将任务的状态、服务器状态等信息上报给 BorgMaster。而 BorgMaster 会周期性地轮询每个 Borglet,以获取节点服务器的状态和资源信息等。

Borg 的整体架构示意图如下所示:

Borg 的主要用户是 Google 的开发者以及运行 Google 应用和服务的系统管理员(网站可靠性工程师,简称 SRE)。用户以 Job 的形式向 Borg 提交工作,每个 Job 由运行一个或多个运行相同程序的 Task 组成。每个 Job 运行在一个 Borg Cell 中,并将一组机器当做一个单元进行管理。

Borg 可以运行各种各样的任务,这些任务主要分为两类:

第一类是长服务,即长时间运行不停止的服务,并且要求能够处理短暂的、延迟敏感的请求(延迟要求在几微秒到几百毫秒之间)。这些服务主要用于面向终端用户的服务(比如 Gmail、Google Docs、Web 搜索),以及内部的一些基础设施服务(比如 BigTable)。

第二类是批处理任务。通常需要几秒到几天的时间来完成的批处理 Job,这些 Job 对短时间的性能波动并不是非常敏感,这些负载通常在 Cell 之间混合分布,每个 Cell 随着主要租户以及时间的不同会运行各种不同的应用:批处理类型的 Job 来了又走,而许多面向终端用户的 Job 又期望一个能长时间使用的模式。

对于这些不同的服务,要求 Borg 能很好地处理所有的情况。Borg 主要有三大优点:1.开发者只需关注应用,不需要关注底层资源管理。它隐藏了资源管理以及错误处理,因此用户能集中精力开发应用。2.高可靠性和可用性,支持多种应用。3.支持上千级服务器的管理和运行。

Borg 并不是第一个解决这些问题的系统,但却是少数能在这么大规模处理这些问题的同时,还能实现这样的弹性和完整性的系统之一。

Kubernetes

Kubernetes 是 Google 开源的容器集群管理系统,是 Borg 的一个开源版本。Kubernetes 是用于自动部署、扩展和管理容器化应用程序的开源系统。其核心是,在集群的节点上运行容器化应用,可以进行自动化容器操作,包括部署、调度和在节点间弹性伸缩等。

Kubernetes 也是典型的集中式结构,一个 Kubernetes 集群,主要由 Master 节点和 Worker 节点组成,以及客户端命令行工具 kubectl 和其他附加项。我们先来看看 Master 节点。它运行在中心服务器上,Master 节点由 API Server、Scheduler、Cluster State Store 和 Control Manger Server 组成,负责对集群进行调度管理。

API Server:是所有 REST 命令的入口,负责处理 REST 的操作,确保它们生效,并执行相关业务逻辑。

Scheduler:根据容器需要的资源以及当前 Worker 节点所在节点服务器的资源信息,自动为容器选择合适的节点服务器。

Cluster State Store:集群状态存储,默认采用 etcd,etcd 是一个分布式 key-value 存储,主要用来做共享配置和服务发现。

Control Manager:用于执行大部分的集群层次的功能,比如执行生命周期功能(命名空间创建和生命周期、事件垃圾收集、已终止垃圾收集、级联删除垃圾收集等)和 API 业务逻辑。

接下来,我们看看 Worker 节点吧。它作为真正的工作节点,运行在从节点服务器,包括 kubelet 和 kube-proxy 核心组件,负责运行业务应用的容器。

kubelet:用于通过命令行与 API Server 进行交互,根据接收到的请求对 Worker 节点进行操作。也就是说,通过与 API Server 进行通信,接收 Master 节点根据调度策略发出的请求或命令,在 Worker 节点上管控容器(Pod),并管控容器的运行状态(比如,重新启动出现故障的 Pod)等。Pod 是 Kubernetes 的最小工作单元,每个 Pod 包含一个或多个容器。

kube-proxy:负责为容器(Pod)创建网络代理 / 负载平衡服务,从 API Server 获取所有 Server 信息,并根据 Server 信息创建代理服务,这种代理服务称之为 Service。Kube-proxy 主要负责管理 Service 的访问入口,即实现集群内的 Pod 客户端访问 Service,或者是集群外访问 Service,具有相同服务的一组 Pod 可抽象为一个 Service。每个 Service 都有一个虚拟 IP 地址(VIP)和端口号供客户端访问。

Kubernetes 架构示意图如下所示:

图中, Kube DNS 负责为整个集群提供 DNS 服务;CNI 是 Container Network Interface 的一个标准的通用接口,用于连接容器管理系统和网络插件。与 Borg 不同的是,Kubernetes 主要是一个容器编排引擎,不仅支持 Docker,还支持 Rocket(另一种容器技术)。Kubernetes 也已经被很多公司采用,比如网易云、华为在需要使用容器进行资源隔离以运行相关业务的场景下,采用了大规模 Kubernetes 集群。

在容器管理方面,Kubernetes 有很多优势。1.自动化容器的部署和复制。Kubernetes 执行容器编排,因此不必人工编写这些任务的脚本。2.将容器组织为组,弹性伸缩。Kubernetes 引入 Pod 机制,Pod 代表着能够作为单一应用程序加以控制的一组容器集合。通过 Pod 机制,Kubernetes 实现了多个容器的协作,能够有效避免将太多功能集中到单一容器镜像这样的错误实践中。此外,软件可以向外扩展跨越多个 Pods 实现初步部署,且相关部署可随时进行规模伸缩。3.容器间负载均衡。Services 用于将具备类似功能的多个 Pod 整合为一组,可轻松进行配置以实现其可发现性、可观察性、横向扩展以及负载均衡。4.易于版本控制与滚动更新。Kubernetes 采取“滚动方式”实现编排,且可跨越部署范围内的全部 Pod。这些滚动更新可进行编排,并以预定义方式配合当前可能尚不可用的 Pods 数量,以及暂时存在的闲置 Pods 数量。Kubernetes 利用新的应用程序镜像版本对已部署 Pods 进行更新,并在发现当前版本存在不稳定问题时回滚至早期部署版本。

Mesos

理解了 Google Borg 和 Kubernetes 的集中式结构,接下来我们再看看 Apache 旗下的开源分布式资源管理框架 Mesos 吧。它被称为是分布式系统的内核,最初由加州大学伯克利分校的 AMPLab 开发,后在 Twitter 得到广泛使用。

Mesos 的开发受到了 Borg 系统的启发,也是采用的典型的集中式架构。Mesos 与 Borg 不同之处在于,Borg 的 Master 直接对接用户应用,也就是说用户可以向 Borg 的 Master 直接请求任务。但 Mesos 不可以,Mesos 只负责底层资源的管理和分配,并不涉及存储、 任务调度等功能,因此 Mesos Master 对接的是 Spark、Hadoop、Marathon 等框架,用户的任务需要提交到这些框架上。也正因为此,Mesos 的任务调度框架是双层结构。

在 Mesos 中,一个集群包括 Mesos Master 和多个 Mesos Agent。其中,Mesos Master 运行在中央服务器,Mesos Agent 运行在节点服务器上。

Mesos Master 负责收集和管理所有 Agent 所在服务器的资源和状态,并且对接 Spark、Hadoop 等框架,将集群中服务器的资源信息告知给这些框架,以便这些框架进行任务资源匹配和调度。Mesos Agent 负责任务的拉起、停止、重启等,并负责收集所在服务器的资源 (比如 CPU、内存等) 信息和状态,上报给 Mesos Master。

Mesos Master 通常采用一主两备的方式,以方便故障处理和恢复。而 Mesos Master 的选主策略,采用的就是我们在第 4 篇文章“分布式选举:国不可一日无君”中介绍的 ZAB 算法。Mesos 架构示意图如下所示:

如上所述,Mesos 对接的是框架,并且可以同时对接多个框架,目前已经被很多公司使用。比如,国外的 Twitter、Apple、Airbnb、Uber 等,国内的爱奇艺、去哪儿、携程、当当等。这些公司选择 Mesos,主要是因为它具有如下优势:

效率。Mesos 对物理资源进行了逻辑抽象,在应用层而不是物理层分配资源,通过容器而不是虚拟机(VM)分配任务。因为应用程序的调度器知道如何最有效地利用资源,所以在应用层分配资源能够为每个应用程序的特殊需求做考量 ; 而通过容器分配任务则能更好地进行“装箱”。

可扩展性。Mesos 可扩展设计的关键是两级调度架构,其中 Framework 进行任务调度,Mesos Master 进行资源分配。由于 Master 不必知道每种类型的应用程序背后复杂的调度逻辑,不必为每个任务做调度,因此可以用非常轻量级的代码实现,更易于扩展集群规模。

模块化。每接入一种新的框架,Master 无需增加新的代码,并且 Agent 模块可以复用,为此开发者可以专注于应用和框架的选择。这,就使得 Mesos 可以支持多种框架,适应不同的应用场景。

随着分布式应用程序和微服务的流行,越来越多的用户正在寻找一种技术,来帮助他们管理这些复杂的应用程序。而 Mesos 为数据中心带来的这些好处,就使得越来越多的人关注 Mesos 及其相关项目。

分析对比

Borg、Kubernetes 和 Mesos 采用的都是集中式结构,要理解它们的实现原理,就要清楚其架构。所以,虽然这部分内容理解起来有难度,但希望你可以深入进去探其本质,这样在实际操作中,就可以从用途出发选择合适的集群管理架构。

非集中式结构

在非集中式结构中,服务的执行和数据的存储被分散到不同的服务器集群,服务器集群间通过消息传递进行通信和协调。也就是说,在非集中式结构中,没有中央服务器和节点服务器之分,所有的服务器地位都是平等(对等)的,也就是我们常说的“众生平等”。这样一来,相比于集中式结构,非集中式结构就降低了某一个或者某一簇计算机集群的压力,在解决了单点瓶颈和单点故障问题的同时,还提升了系统的并发度,比较适合大规模集群的管理。

所以近几年来,Google、 Amazon、Facebook、阿里巴巴、腾讯等互联网公司在一些业务中也相继采用了非集中式结构。接下来,我将为你介绍 3 种典型的非集中式架构系统,包括 Akka 集群、Redis 集群和 Cassandra 集群,来帮助你深入理解非集中式架构。

Akka 集群

在介绍 Akka 集群的结构之前,我带你了解一下什么是 Akka 框架吧。Akka 框架基于 Actor 模型,提供了一个用于构建可扩展的、弹性的、快速响应的应用程序的平台。其中,Actor 是一个封装了状态和行为的对象,它接收消息并基于该消息执行计算。Actor 之间互相隔离,不共享内存,但 Actor 之间可通过交换消息(mail)进行通信(每个 Actor 都有自己的 MailBox)。

比如,在分布式系统中,一个服务器或一个节点可以视为一个 Actor,Actor 与 Actor 之间采用 mail 进行通信,如下图所示:

可以看到,Actor 发送的 Mail 消息会存储在接收方的 MailBox 中。默认情况下,接收方按照 mail 到达的先后顺序,从 MailBox 中提取 mail 消息,并进行相应的计算处理。 备注:关于 Actor 模型更详细的内容,我会在第 17 篇文章中会与你讲述。

显然,Actor 模型为系统并发度提供了非常好的解决方案,且是一个异步的、非阻塞的、高性能的事件驱动编程模型。Akka 集群充分利用了 Actor 模型的优势,提供了一个非集中式架构的集群管理模块,用来构建可扩展的、弹性的分布式应用程序。

Akka 集群负责 Actor 模型底层的节点管理,包括故障检测、节点加入 / 退出集群等。也就是说,Akka 集群为 Actor 模型提供了一个可容错、去中心化的节点集群管理系统,来保证 Actor 的运行和 Actor 之间的通信。

如下图所示,Akka 集群是一个完全去中心化的分布式集群管理系统。一个集群由多个节点组成,每个节点都可以进行数据处理和任务执行,节点之间均可进行通信。节点有 Leader 节点和非 Leader 节点之分。与非 Leader 节点相比,Leader 节点只是增加了负责节点的加入和移除集群的功能,所以并不会影响非集中式结构中节点的平等关系。

可以看到,Akka 集群的两个重点是数据传输和集群组建及管理,所以接下来我将从这两个方面与你介绍 Akka 集群。首先,我们看一下数据传输。在 Akka 集群中,节点是对等的,也就是说每个节点是可以并发处理的,因此必然存在数据传输和一致性的问题。

比如,我们要针对数据进行操作,将 X=1 修改为 X=2。现在集群中节点 1 进行了修改使得 X=2,但其他节点上还是 X=1,因此节点 1 需要将 X=2 的消息告知其他节点,以保证最终集群中所有节点上均为 X=2。

其实,这个问题就是分布式共识问题。我已经在第 5 篇文章“分布式共识:存异求同”中,与你介绍了 PoW、PoS 和 DPoS 三种达成共识的方法,你可以再复习下相关内容。

Akka 集群主要采用的是谁的时间戳最新(也就是数据最新),就以谁为准的原则。在这里我要重点与你讲述的是,如何将 X=2 这个消息传输给集群中的每个节点。Akka 集群采用了Gossip 协议,该协议是最终一致性协议。它的原理是每个节点周期性地从自己维护的集群节点列表中,随机选择 k 个节点,将自己存储的数据信息发给这 k 个节点,接收到该信息的节点采用前面讲的共识原则,对收到的数据和本地数据进行合并,这样迭代几个周期后,集群中所有节点上的数据信息就一致了。

这就好比我们生活中的“谣言传播”一样,用户 A 告诉用户 B“商场新开了一家火锅店”,用户 B 收到信息后再告诉用户 C,然后用户 C 再告诉用户 D。这样,用户 A、B、C、D 最终都知道了这个消息。

接下来,我们看一下集群组建及管理。下图展示了 Akka 集群的创建过程。在创建集群时,节点被分为三种类型,即:

种子节点。使用静态配置文件方式或者系统运行时指定方式,可以生成种子节点;种子节点是普通节点加入集群的联系点,可以自动接收新加入集群的节点的信息。

首种子节点。首种子节点是配置文件中的第一个种子节点,其功能是集群第一次启动时,首种子节点启动起来,集群才能组建成功,保证集群第一次创建时只有一个集群。如下图 A 节点,就是 Akka 集群的首种子节点。

普通节点。可以向种子节点或集群中的任意节点发送 Join 消息,请求加入集群。如下图的 B 和 C 节点,通过向 A 节点发送 Join 消息,从而加入到 Akka 集群。

Akka 集群的每个节点启动后,读取配置文件获取种子节点列表,然后开始组建集群: 如果本节点为首种子节点,则把自己加入到集群列表中,即以自己为中心构建集群; 如果本节点为种子节点,则向首种子节点请求加入集群,当首种子节点回复同意消息后,可以加入集群,否则不可加入集群; 如果本节点为普通节点,则可以向任一种子节点(包括首种子节点)请求加入集群,收到同意后,则加入集群,否则不可加入集群。

加入首种子节点或种子节点的节点信息,会通过 Gossip 协议的传播方式传播给当前已加入的所有节点,以完成集群组建。当集群组建完成后,就不存在种子节点与普通节点之分了,每个节点均可执行 Actor 应用程序。

Akka 集群可以构建可扩展的、弹性的分布式应用程序,因此在 JVM 中应用了 Akka 框架,从而实现并发编程。目前,豌豆荚、蘑菇街等公司采用了 Akka 集群。到这里,我们小结一下吧。Akka 集群是一个完全去中心化的集群管理系统,当集群组建完成后,每个节点均可执行 Actor 应用程序,因此支持并发操作。但这个并发操作引入了数据同步和一致性问题,所以 Akka 集群采用了 Gossip 协议进行数据同步,通过谁的时间戳最新就以谁为准,来解决一致性问题。

在实际业务场景中,除了面向应用程序平台的分布式集群管理之外,分布式数据存储也是一个非常重要的话题。在这其中,分布式数据存储中的集群管理便是一个关键因素。那么接下来,我就以开源数据库 Redis 的集群管理系统为例,与你展开介绍吧。

Redis 集群

Redis 是一个开源的高性能分布式 key-value 数据库,应用广泛,其特征主要表现为: 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启时可以再次加载并使用; 支持多种数据结构,不仅支持简单的 key-value 类型的数据,同时还提供 list、set、hash 等数据结构的存储; 支持数据的备份,即 Master/Slave 模式的数据备份。

Redis 的这些特征均是为数据存储进行服务的,数据可分片存储在不同的 Redis 节点上,多个 Redis 节点间可共享数据,而提供这项能力的就是 Redis 集群。Redis 集群中不存在中央节点,是典型的去中心化结构,每个节点均可与其他节点通信。所有节点均可负责存储数据、记录集群的状态(包括键值到正确节点的映射),客户端可以访问或连接到任一节点上。集群节点同样能自动发现其他节点,检测故障的节点,并在需要的时候在从节点中推选出主节点。Redis 集群的架构图如下所示。

当然,节点之间的数据传输仍采用了 Gossip 协议,来保证集群中数据的最终一致性。Redis 集群中的节点用于数据存储,所以在设计时,需要考虑数据的可靠性和分片存储问题。对于可靠性的问题,集群中每个节点均存在主备,也就是说每台服务器上都运行两个 Redis 服务,分别为主备,主故障后,备升主。而对于数据的分片存储问题,Redis 集群引入了哈希槽的概念。Redis 集群内置了 16384 个哈希槽,每个节点负责一部分哈希槽。当客户端要存储一个数据或对象时,对该对象的 key 通过 CRC16 校验后对 16384 取模,也就是 HASH_SLOT = CRC16(key) mod 16384 来决定哈希槽,从而确定存储在哪个节点上。

比如当前集群有 3 个节点,那么: 节点 A 包含 0 到 5500 号哈希槽; 节点 B 包含 5501 到 11000 号哈希槽; 节点 C 包含 11001 到 16383 号哈希槽。

Redis 集群采用集群分片方式实现了数据的分片存储,从而将 Redis 的写操作分摊到了多个节点上,提高了写并发能力。到这里,我们小结一下。Redis 集群是一个非集中式集群管理系统,没有中心节点,不会因为某个节点造成性能瓶颈,每个节点均支持数据存储,且采用分片存储方式,提高了写的并发能力。同时,每个节点的设计采用主备设计,提高了数据的可靠性。

鉴于这些优点,Redis 已被 Twitter、Uber、GitHub、Instagaram 等公司采用。除了 Redis 外,还有一个开源分布式 key-value 数据库系统 Cassandra。接下来,我就再与你分享下 Cassandra 集群的设计,以加深你对非集中式架构的理解。

Cassandra 集群

与 Redis 类似,Cassandra 也支持数据的分布式存储和操作。因此,Cassandra 的集群架构与数据分片存储方案,与 Redis 集群类似。如下图所示,Cassandra 集群的系统架构是基于一致性哈希的完全 P2P 结构,没有 Master 的概念,所有节点都是同样的角色,彻底避免了因为单点问题导致的系统不稳定。Cassandra 集群节点间的状态同步,也是通过 Gossip 协议来进行 P2P 通信的。

集群中的每个节点,都可以存储数据,并接收来自客户端的请求。Cassandra 集群数据存储与 Redis 的不同之处是,Redis 集群每个节点代表一部分哈希槽,一个哈希槽代表一个哈希值区间,而 Cassandra 集群中每个节点代表一个哈希值。在 Cassandra 集群中,每次客户端随机选择集群中的一个节点来请求数据,对应接收请求的节点将对应的 key 在一致性哈希环上定位出是哪些节点应该存储这个数据,然后将请求转发到对应的节点上,并将对应若干节点的查询反馈返回给客户端。目前,Cassandra 集群因为完全去中心化的结构模式,已经被 Apple、Comcast、Instagram、Spotify、eBay、Netflix 等公司使用。

到这里,我们小结一下吧。Cassandra 采用去中心化的架构,解决了集中式结构的单点故障问题,同时因为数据基于哈希值分区存储,提高了读写数据的并发能力。在 Cassandra 集群中,没有 Master 的概念,每个节点代表一个哈希值,通过哈希映射的方式决定数据存储的位置。集群间的状态同步通过 Gossip 协议来进行 P2P 的通信。

对比分析

便于理解与记忆,主要特征梳理了一张表格,如下所示:

集中式结构虽然易于理解,但容易出现单点瓶颈和单点故障等问题,而非集中结构才是超大规模分布式系统的首选结构。所以今天,我以 Akka 集群、Redis 集群和 Cassandra 集群的结构为例,与你详细介绍了非集中式架构。Akka 集群是一个完全去中心化的集群管理系统,节点之间都是 P2P 的连接模式,通过 Gossip 协议来进行通信,节点之间有角色划分,负责数据存储的节点会进行存储数据。Redis 集群也是 P2P 的网状连接模式,但是基于 key-value 的数据库模型,每个节点都可以执行数据的计算和存储。此外,Redis 集群引入了哈希槽的概念,来解决数据的分片存储问题。Cassandra 集群的结构是一致性哈希的 P2P,节点会构成一个环结构,通过哈希映射来选择对应的节点。

单体调度

分布式系统中的单体调度是指,一个集群中只有一个节点运行调度进程,该节点对集群中的其他节点具有访问权限,可以搜集其他节点的资源信息、节点状态等进行统一管理,同时根据用户下发的任务对资源的需求,在调度器中进行任务与资源匹配,然后根据匹配结果将任务指派给其他节点。

单体调度器拥有全局资源视图和全局任务,可以很容易地实现对任务的约束并实施全局性的调度策略。目前很多集群管理系统采用了单体调度设计,比如我们第 9 篇文章中讲到的 Google Borg、Kubernetes 等。

如下图所示,图中展示了一个典型的单体调度框架。Master 节点上运行了调度进程(负责资源管理、Tasks 和资源匹配);Node 1,Node 2,…, Node N 对应着我们在第 9 篇文章中讲的 Master/Slave 架构中的 Slave 节点。

Slave 节点会将 Node State 上报给 Master 节点的 Cluster State 模块,Cluster State 模块用于管理集群中节点的资源等状态,并将节点的资源状态传送给 Scheduling Logic 模块,以便 Scheduling Logic 模块进行 Tasks 与资源匹配,并根据匹配结果将 Task 发送给匹配到的节点。

单体调度设计

在集群管理中,单体调度模块称为“Scheduler”或“单体调度器”。单体调度器也叫作集中式调度器,指的是使用中心化的方式去管理资源和调度任务。也就是说,调度器本身在系统中以单实例形式存在,所有的资源请求和任务调度都通过这个实例进行。集中式调度器的常见模型,如下图所示。可以看到,在这一模型中,资源的使用状态和任务的执行状态都由调度器进行管理。

在 Borg 和 Kubernetes 这两个集群管理系统中,Scheduler 是它们的核心。而 Kubernetes 又是 Borg 的开源版本。所以接下来,我就以 Borg 为例,与你讲述它的调度器是如何设计的,才能保证在上万台机器规模的集群上,运行来自几千个不同应用的几十万个作业。

Borg 调度设计

调度的初衷是为作业或任务寻找合适的资源,也就是说作业或任务是调度的对象。那么作业和任务到底是什么呢?下面,我带你先了解一下作业和任务的概念以及关系。一个 Borg 作业的属性包括名称、拥有者和任务个数。作业可以有一些约束来强制其任务运行在有特定属性的机器上,比如处理器架构、操作系统版本、是否有外网 IP 地址等。这些约束可以是硬性的也可以是柔性的,其中柔性约束表示偏好,而非需求。一个作业只在一个集群中运行。

而一个任务对应的是一组 Linux 进程,运行在一台机器上的一个容器内或直接运行在节点上。任务也有一些属性,比如资源需求量、在作业中的序号等。

概括来说,一个作业可以包含多个任务。作业类似于用户在一次事务处理或计算过程中要求计算机所做工作的总和,而任务就是一项项具体的工作,二者属于包含关系。

一个作业中的任务大多有相同的属性,但也可以被覆盖 ,比如特定任务的命令行参数、各维度的资源(比如,CPU 核、内存、硬盘空间、硬盘访问速度、TCP 端口等)。多个任务可以在多台机器上同时执行,从而加快作业的完成速度,提高系统的并行程度。而具体将哪个任务分配给哪个机器去完成,就是调度器要做的事儿了。

接下来,我就与你讲述下 Borg 的 Scheduler 组件,来帮助你理解 Borg 内部的任务调度流程,以加深你对单体调度的理解。其实,很多框架比如 Hadoop、Spark 等都是采用了单体调度设计,它们整体的思想类似,所以我希望通过对 Borg 调度的讲解,能够帮助你理解你所在业务中的调度逻辑。我们先来回忆下 Borg 的系统架构图吧。

Scheduler 负责任务的调度,当用户提交一个作业给 BorgMaster 后,BorgMaster 会把该作业保存到 Paxos 仓库中,并将这个作业的所有任务加入等待队列中。调度器异步地扫描等待队列,将任务分配到满足作业约束且有足够资源的计算节点上。

这里我要再强调一下,调度是以任务为单位的,而不是以作业为单位。调度器在扫描队列时,按照任务的优先级从高到低进行选择,同优先级的任务则以轮询的方式处理,以保证用户间的公平,并避免队首的大型作业阻塞队列。

Borg 调度算法

Borg 调度算法的核心思想是“筛选可行,评分取优”,具体包括两个阶段: 可行性检查,找到一组可以运行任务的机器(Borglet); 评分,从可行的机器中选择一个合适的机器(Borglet)。

首先,我们看一下可行性检查。在可行性检查阶段,调度器会找到一组满足任务约束,且有足够可用资源的机器。比如,现在有一个任务 A 要求能部署的节点是节点 1、节点 3 和节点 5,并且任务资源需求为 0.5 个 CPU、2MB 内存。根据任务 A 的约束条件,可以先筛选出节点 1、节点 3 和节点 5,然后根据任务 A 的资源需求,再从这 3 个节点中寻找满足任务资源需求的节点。

这里需要注意的是,每个节点上的可用资源,包括已经分配给低优先级任务但可以抢占的资源。在评分阶段,调度器确定每台可行机器的适宜性。Borg 根据某一评分机制,对可行性检查阶段中筛选出的机器进行打分,选出最适合调度的一台机器。在评分过程中,我们可以制定多种评价指标,比如考虑如何最小化被抢占的任务数、尽量选择已经下载了相同 package 的机器、目标任务是否跨域部署、在目标机器上是否进行高低优先级任务的混合部署等。 根据不同的考虑因素,可以定制不同的评分算法。

其中,常见的评分算法,包括“最差匹配”和“最佳匹配”两种。

Borg 早期使用修改过的 E-PVM 算法来评分,该算法的核心是将任务尽量分散到不同的机器上。该算法的问题在于,它会导致每个机器都有少量的无法使用的剩余资源,因此有时称其为“最差匹配”(worst fit)。

比如,现在有两个机器,机器 A 的空闲资源为 1 个 CPU 和 1G 内存、机器 B 的空闲资源为 0.8 个 CPU 和 1.2G 内存;同时有两个任务,Task1 的资源需求为 0.4 个 CPU 和 0.3G 内存、Task2 的资源需求为 0.3CPU 和 0.5G 内存。按照最差匹配算法思想,Task1 和 Task2 会分别分配到机器 A 和机器 B 上,导致机器 A 和机器 B 都存在一些资源碎片,可能无法再运行其他 Task。

与之相反的是“最佳匹配”(best fit),即把机器上的任务塞得越满越好。这样就可以“空”出一些没有用户作业的机器(它们仍运行存储服务),来直接放置大型任务。

比如,在上面的例子中,按照最佳匹配算法的思想,Task1 和 Task2 会被一起部署到机器 A 或机器 B 上,这样未被部署的机器就可以用于执行其他大型任务了。但是,如果用户或 Borg 错误估计了资源需求,紧凑的装箱操作会对性能造成巨大的影响。比如,用户估计它的任务 A 需要 0.5 个 CPU 和 1G 内存,运行该任务的服务器上由于部署了其他任务,现在还剩 0.2 个 CPU 和 1.5G 内存,但用户的任务 A 突发峰值时(比如电商抢购),需要 1 个 CPU 和 3G 内存,很明显,初始资源估计错误,此时服务器资源不满足峰值需求,导致任务 A 不能正常运行。

所以说,最佳匹配策略不利于有突发负载的应用,而且对申请少量 CPU 的批处理作业也不友好,因为这些作业申请少量 CPU 本来就是为了更快速地被调度执行,并可以使用碎片资源。还有一个问题,这种策略有点类似“把所有鸡蛋放到一个篮子里面”,当这台服务器故障后,运行在这台服务器上的作业都会故障,对业务造成较大的影响。

因此,这两个评分算法各有利弊。在实践过程中,我们往往会根据实际情况来选择更适宜的评分算法。比如,对于资源比较紧缺,且业务流量比较规律,基本不会出现突发情况的场景,可以选择最佳匹配算法;如果资源比较丰富,且业务流量会经常出现突发情况的场景,可以选择最差匹配算法。

Borg 的设计是支持高优先级抢占低优先级任务的,也就是说如果评分后选中的机器上没有足够的资源来运行新任务,Borg 会抢占低优先级的任务,从最低优先级逐级向上抢占,直到可用资源足够运行该任务。被抢占的任务放回到调度器的等待队列里,而不会被迁移或使其休眠。

当然有很多调度框架是支持用户根据自己的场景自定义调度策略的,比如优先级策略、亲和性策略、反亲和性策略等。

知识扩展:多个集群 / 数据中心如何实现单体调度呢?

单体调度器虽然具有单点瓶颈或单点故障问题,但因为其具有全局资源视图和全局任务,简单易维护,被很多公司广泛采用,比如 Google、阿里、腾讯等公司。另外,我们今天介绍的 Borg 集群管理系统,以及其开源版 Kubernetes 集群管理系统,使用的都是单体调度结构。

两层调度

单体调度结构虽然结构单一,但是其调度算法可以扩展甚至自定义,也就是说你可以根据业务特征,自定义调度策略,比如优先级策略、亲和性策略等。单体调度的核心是,所有节点的资源以及用户的任务均由中央服务器统一管理和调度。因此,中央服务器很容易成为单点瓶颈,会直接导致其支持的调度规模和服务类型受限。于是两层调度就出现了。那么,到底什么是两层调度呢,它是如何设计的,又有哪些调度算法呢?接下来,就和我一起打卡分布式调度架构的两层调度,去探寻这些问题的答案吧。

在单体调度架构中,中央服务器的单点瓶颈问题,会限制调度的效率和支持的任务类型。中央服务器的性能会限制调度的效率,很好理解,但为什么会限制支持的任务类型呢?简单地说,这是因为不同的服务具有不同的特征,对调度框架和计算的要求都不一样。比如说,你的业务最开始时只有批处理任务,后来发展到同时还包括流数据任务,但批处理任务是处理静态数据,流数据任务却是处理实时数据。显然,单体调度框架会随着任务类型增加而变得越来越复杂,最终出现扩展瓶颈。

那么,为了提升调度效率并支持多种类型的任务,最直接的一个想法就是,能不能把资源和任务分开调度,也就是说一层调度器只负责资源管理和分配,另外一层调度器负责任务与资源的匹配呢。很显然,这个解决方案是可以的。这种调度架构,就是我们通常所说的两层调度。如果我们还是把资源比作物质文明、把任务比作精神文明的话,两层调度就可以理解为“物质文明与精神文明两手抓”。两层调度结构对应的就是两层调度器,资源的使用状态同时由中央调度器和第二层调度器管理,中央调度器从整体上进行资源的管理与分配,将资源分配到第二层调度器;再由第二层调度器负责将资源与具体的任务配对,因此第二层调度可以有多个调度器,以支持不同的任务类型。

如下图所示,Scheduler-1 表示第一层调度,负责收集和管理集群中的资源信息;Scheduler-2 表示第二层调度,Scheduler-1 会将集群资源发送给 Scheduler-2,然后 Scheduler-2 根据任务的资源需求和 Scheduler-1 发送的资源信息进行任务匹配和调度。

两层调度器中的第一层调度器仍是一个经简化的中央调度器,通常放在分布式集群管理系统中,而第二层调度则是由各个应用程序框架完成。两层调度器的职责分别是:第一层调度器负责管理资源并向框架分配资源,第二层调度器接收分布式集群管理系统中第一层调度器分配的资源,然后根据任务和接收到的资源进行匹配。

采用两层调度结构的集群管理系统有很多,典型代表是 Apache Mesos 和 Hadoop YARN。我在第 9 篇文章中讲述 Mesos 的体系结构时,和你分析了它采用的是典型的两层调度。那么今天,我就继续以 Mesos 为例,带你学习两层调度的架构设计和对应的分配算法吧。

两层调度设计

由于 Mesos 只负责底层资源的管理和分配,并不涉及存储、 任务调度等功能,因此 Mesos 要实现类似 Borg 那样的资源与任务管理,还需要上层框架的配合。

具体到两层调度架构上,Mesos 本身实现的调度器为第一层调度,负责资源管理,然后将第二层任务调度交给了框架完成。接下来,我们就具体看看吧。

两层调度架构

以 Mesos 为基础的分布式资源管理与调度框架包括两部分,即 Mesos 资源管理集群和框架。

资源管理集群是由一个 Master 节点和多个 Slave 节点组成的集中式系统。每个集群有且仅有一个 Master 节点,负责管理 Slave 节点,并对接上层框架;Slave 节点向 Master 节点周期汇报资源状态信息,并执行框架提交的任务。

框架(Framework)运行在 Mesos 上,是负责应用管理与调度的“组件”,比如 Hadoop、Spark、MPI 和 Marathon 等,不同的框架用于完成不同的任务,比如批处理任务、实时分析任务等。框架主要由调度器(Scheduler)和执行器(Executor)组成,调度器可以从 Master 节点获取集群节点的信息 ,执行器在 Slave 节点上执行任务。

从上述的架构描述可以看出,Mesos 是一个典型的双层调度框架。Mesos Master 上有一个调度器(也就是 Allocation Module),负责管理并分配集群中的所有资源,是第一层调度。框架上负责任务的管理与调度的调度器,是第二层调度,如下图所示。

框架向 Mesos Master 注册;

Mesos Slave 节点定期或周期向 Mesos Master 上报本节点的空闲资源;

Mesos Master 的 Scheduler 进程收集所有节点的空闲资源信息,并以 Resource Offer 的方式将空闲资源发送给注册的框架;

框架的 Scheduler 接收到 Mesos 发送的资源后,进行任务调度与匹配,匹配成功后,将匹配结果下发给 Mesos Master,并由 Mesos Master 转发给相应节点的执行器执行任务。

可以看出,Mesos 实现双层调度时,采用 Resource Offer 机制衔接了第一层和第二层调度。Resource Offer 机制指的是,Mesos Master 主动将节点空闲资源,以类似发放(Offer)的方式发给每个框架,如果框架需要则使用,不需要则还回。

也就是说,通过 Resource Offer 机制,第一层调度将资源主动告知第二层调度,然后第二层调度进行具体的任务匹配,从而实现了任务调度与资源管理的分离,Mesos Master 通过资源分配算法决定给各个 Framework 提供多少资源,而 Framework 则决定接受哪些资源,以及哪些任务使用这些资源运行。这样一来,一个两层调度架构就实现了。

在 Mesos 的两层调度中,Framework 第二层调度器中的任务与资源匹配的调度策略很常见,也有很多文章做了比较深入的分析了,所以如果你想要深入研究的话,可以参考下 Hadoop、Spark 等的调度策略,这里我就不多说了。

接下来,我们重点看下 Mesos 第一层调度算法,理解其如何为框架分配资源,以支持多用户多框架。

资源分配算法

Mesos 的资源分配算法解决的问题是,决策需要将当前可用资源分配给哪些框架以及分配多少。接下来,我将重点与你介绍两种主要的资源分配算法,即:最大最小公平算法(Max-min Fairness,MMF)和主导资源公平算法(Dominant Resource Fairness,DRF)。

首先,我们看看最大最小公平算法。这是一种在兼顾公平的前提下,尽可能让更多人满意的资源分配算法。为什么这么说呢?因为这个算法有 3 个主要原则:

按照用户对资源需求量递增的顺序进行空闲资源分配;

不存在用户得到的资源超过自己需求的情况;

对于分配的资源不满足需求的用户,所获得的资源是相等的。

在执行资源分配时,最大最小公平算法按照上述 3 条原则进行多次迭代,每次迭代中资源均平均分配,如果还有剩余资源,就进入下一次迭代,一直到所有用户资源得到满足或集群资源分配完毕,迭代结束。接下来,我们通过一个具体的例子来看看最大最小公平算法的资源分配流程吧。

假设,现在有总量为 100 的空闲资源,有 4 个用户 A、B、C、D 对该资源的需求量分别为(35,10,25,45),分配流程如下:

1.按照用户对资源的需求量升序排列,则 4 个用户的需求量为(B:10,C:25,A:35,D:45)。

2.平均分配空闲资源。资源空闲总量 100,除以用户数 4,则平均空闲资源量为 25;按照第一步中需求量分配后,用户资源需求量为(0,0,10,20),且用户 B 由于资源需求量小于 25,因此会剩余资源。此时空闲资源量为 15,资源需求人数为 2。

3.重复第二步,平均分配资源,15/2=7.5,即分别为用户 A 和 D 分配 7.5 份资源,此时用户资源需求量为(0,0,2.5,12.5),空闲资源量为 0,资源需求人数为 2。

4.所有资源已分配完,算法终止。

最大最小公平算法的执行流程,如下图所示。

在这个案例中,最大最小公平算法是由于所有资源全部分配完才终止的。至此,对于需求量为(10,25,35,45)的用户们来说,分配到的资源是(10,25,32.5,32.5)。这个算法的另外一个结束条件是,资源分配满足了所有用户的资源需求,即当没有用户有资源需求时,算法也会终止。

最大最小公平算法采用了绝对公平的方式分配资源,会导致大量的资源浪费,比如用户需求量为 35 和 45 的用户 A 和用户 D,均分配了 32.5 的空闲资源,但由于资源不满足需求,这两个用户均无法使用。

而主导资源公平算法在考虑用户公平性的前提下,还考虑了用户对不同资源类型的需求,以尽可能地合理分配资源。也就是说,同样的资源量,主导资源公平算法可以尽可能地满足更多的用户。

在 Mesos 中,框架对资源的需求往往包括对 CPU、内存等多种类型资源的需求。针对多种资源的需求,主导资源公平算法首先计算已经分配给用户的每一种资源的占用率(Resource Share),比如已经分配的 CPU 占总资源量的多少,已经分配的内存占总资源量的多少。所有资源占用率中的最大值称作该用户的主导资源占用率,而主导资源占用率对应的资源就是用户的主导资源。

我们通过一个具体的案例,看看如何判断用户的主导资源吧。如下图所示,假设系统中的资源共包括 18 个 CPU 和 36 GB 内存,有两个 Framework(Framework A 和 Framework B)分别运行了两种任务,假设 Framework A 运行内存密集型任务,Framework B 运行 CPU 密集型任务,且每个任务所需要的资源量是一致的,分别是 <2 CPU, 8 GB> 和 <6 CPU, 2 GB>。

第一步:计算资源分配量。假设 x 和 y 分别是 Framework A 和 Framework B 分配的任务数,那么 Framework A 消耗的资源为{2x CPU,8x GB},Framework B 消耗的资源数为{6y CPU,2y GB},分配给两个 Framework 的总资源量为(2x+6y)个 CPU 和(8x+2y)GB 内存。

第二步:确定主导资源。对于 Framework A 来说,每个任务要消耗总 CPU 资源的 2/18,总内存资源的 8/36,所以 Framework A 的主导资源为内存;对于 Framework B 来说,每个任务要消耗总 CPU 资源的 6/18 和总内存资源的 2/36,因而 Framework B 的主导资源为 CPU。

第三步:DRF 算法的核心是平衡所有用户的主导资源占用率,尽可能试图最大化所有用户中最小的主导资源占用率。通过求解下列公式,可以计算出 Framework A 和 Framework B 分配的任务数,并且要在满足公式的条件下,使得 x 和 y 越大越好。

2x+6y≤18

8x+2y≤36

8x/36=6y/18

通过求解可以得出:x=3,即 Framework A 可以运行 3 个任务;y=2,即 Framework B 可以运行 2 个任务。这样分配的话,每个 Framework 获取了相同比例的主导资源,即:A 获取了 2/3 的内存,B 获取了 2/3 的 CPU,从而在主导资源上体现了调度算法的公平性。

在实际任务分配过程中,主导资源率是根据已经分配给 Framework 的资源,占集群中总资源量的多少进行计算的,并且在每次分配过程中,会选择主导资源最小的 Framework 进行分配,也就是试图最大化所有用户中最小的主导资源占用率。

最大最小公平算法适用于单一类型的资源分配场景,而主导资源公平算法适用于多种类型资源混合的场景。并且,最大最小公平算法从公平的角度出发,为每个用户分配不多于需求量的资源;而主导资源公平算法从任务出发,目的在于尽量充分利用资源使得能够执行的任务越多越好。

知识扩展:两层调度如何保证不同的业务不会互相干扰?

类似 Mesos 这样的两层调度机制,可以同时支持多个框架和多种类型的业务,那么如何保证这些业务运行时不会互相干扰呢?

首先,我们思考一下什么情况下会存在业务运行时相互干扰呢。答案就是,当多个业务运行在同一台机器上,共同使用 CPU、内存,以及系统环境时会存在相互干扰。要解决这个问题,我想你肯定会问,不同的业务能在独立的环境中运行吗?也就是说,隔离不同的业务资源和环境,应该就不会存在相互干扰了吧。不错,解决这个问题的办法就是资源隔离,就好比我们现在接触的虚拟机一样,在同样的服务器上安装多个虚拟机,不同的用户在不同的虚拟机上运行,这些用户互不干扰。在 Mesos 中,实现这种资源隔离的是容器。

容器的实质是进程,该进程运行于属于自己的独立的命名空间,可以拥有自己的 root 文件系统、自己的网络配置、自己的进程空间,甚至是自己的用户 ID 空间。Mesos 支持的容器,包括 Linux 自带的 cgroups 和 Docker。

所以说,Mesos 正是用容器隔离开了不同的业务,使得它们运行时不会互相干扰。两层调度是一种资源和任务分开调度的设计,也就是说一层调度器只负责资源的管理和分配,另外一层调度器负责任务与资源的匹配。

在 Mesos 中,第一层资源调度由 Mesos 提供,第二层任务调度由框架提供,Mesos 将资源以 Resource Offer 的形式发放给框架调度器,框架调度器根据任务需求和得到的资源信息进行任务匹配调度,为此提高了调度的并发性。

而关于第一层的调度算法,通常有最大最小公平算法和主导资源公平算法等。两层调度的一个问题是,由于第二层调度只能获得部分资源视图,因此无法实现全局最优调度。

两层调度提供了多租户多框架的支持,如果你的业务类型比较多或者面向的是不同的租户的话,建议你采用两层调度框架。相信你通过这篇文章可以看到,在分布式领域中,同时支持多种框架、支持多种类型任务调度的调度机制,并没有那么神秘,只要你静下心来弄明白这篇文章的调度机制,以后遇到类似的调度机制,就可以做到心中有数了。不得不说,Mesos 的两层调度设计得非常巧妙,并且 Mesos 支持你自己写一个调度器注册到 Mesos 作为第二层调度。赶快动手实践一下吧,Mesos 的官网提供了相应的案例,方便你入门,加油,相信你一定可以!

共享状态调度

通过我们前两篇文章的讲述,不难发现,集群中需要管理的对象主要包括两种: 一是,资源的分配和使用状态; 二是,任务的调度和执行状态;

在单体调度中,这两种对象都是由单体调度器管理的,因此可以比较容易地保证全局状态的一致性,但问题是可扩展性较差(支持业务类型受限),且存在单点瓶颈问题。而在两层调度中,这两种对象分别由第一层中央调度器和第二层 Framework 调度器管理,由于 Framwork 调度器只能看到部分资源,因此不能保证全局状态的一致性,也不容易实现全局最优的调度。

为了解决这些问题,一种新的调度器架构被设计了出来。这种架构基本上沿袭了单体调度器的模式,通过将单体调度器分解为多个调度器,每个调度器都有全局的资源状态信息,从而实现最优的任务调度,提供了更好的可扩展性。

也就是说,这种调度架构在支持多种任务类型的同时,还能拥有全局的资源状态信息。要做到这一点,这种调度架构的多个调度器需要共享集群状态,包括资源状态和任务状态等。因此,这种调度架构,我们称之为共享状态调度器。如果我们继续把资源比作物质文明、把任务比作精神文明的话,相对于单体调度和两层调度来说,共享状态调度就是“物质文明与精神文明多手协商抓”。

共享状态调度架构的示意图,如下所示:

可以看出,共享状态调度架构为了提供高可用性和可扩展性,将集群状态之外的功能抽出来作为独立的服务。具体来说就是:1.State Storage 模块(资源维护模块)负责存储和维护资源及任务状态,以便 Scheduler 查询资源状态和调度任务;2.Resource Pool 即为多个节点集群,接收并执行3.Scheduler 调度的任务;4.而 Scheduler 只包含任务调度操作,而不是像单体调度器那样还需要管理集群资源等。

共享状态调度也支持多种任务类型,但与两层调度架构相比,主要有两个不同之处:1.存在多个调度器,每个调度器都可以拥有集群全局的资源状态信息,可以根据该信息进行任务调度;2.共享状态调度是乐观并发调度,在执行了任务匹配算法后,调度器将其调度结果提交给 State Storage,由其决定是否进行本次调度,从而解决竞争同一种资源而引起的冲突问题,实现全局最优调度。而,两层调度是悲观并发调度,在执行任务之前避免冲突,无法实现全局最优匹配。

看到这里,我再和你说说乐观并发调度和悲观并发调度的区别吧:乐观并发调度,强调事后检测,在事务提交时检查是否避免了冲突:若避免,则提交;否则回滚并自动重新执行。也就是说,它是在执行任务匹配调度算法后,待计算出结果后再进行冲突检测。

悲观并发调度,强调事前预防,在事务执行时检查是否会存在冲突。不存在,则继续执行;否则等待或回滚。也就是说,在执行任务匹配调度算法前,通过给不同的 Framework 发送不同的资源,以避免冲突。

现在,我们已经对共享状态调度有了一个整体印象,知道了它可以解决什么问题。那么接下来,我们再看看这种调度架构是如何设计的吧。

共享状态调度设计

共享状态调度的理念最早是 Google 针对两层调度器的不足,提出的一种调度架构。这种调度结构的典型代表有 Google 的 Omega、微软的 Apollo,以及 Hashicorp 的 Nomad 容器调度器。

作为 Google 公司的第二代集群管理系统,Omega 在设计时参考了 Borg 的设计,吸收了 Borg 的优点,并改进了其不足之处。所以接下来,我就以 Omega 为例和你讲述共享状态调度的架构和工作原理吧。这样一来,你可以对照着第 11 篇文章中 Borg 的调度设计一起理解。

Omega 调度架构

Omega 集群中有一个“Cell”的概念,每个 Cell 管理着部分物理集群,一个集群有多个 Cell。实际上,你可以直接将这里的“Cell”理解为一个集群的子集群或子节点的集合。Omega 集群的调度架构示意图,如下所示。

我在介绍共享状态调度的架构时提到,State Storage 模块负责存储和维护资源及任务状态,里面有一个 Cell State 文件,记录着全局共享的集群状态。实际上,State Storage 组件中的集群资源状态信息,就是主本,而 Cell State 就是以主副本的形式存在的。每个调度器都包含一个私有的 Cell State 副本,也就是拥有了一个集群资源状态信息的副本,进而达到了共享集群资源状态信息的目的。

在 Omega 系统中,没有中央资源分配器,所有资源分配决策都在调度器(Scheduler)中进行。每个调度器都可以根据私有的 Cell State 副本,来制定调度决策。调度器可以查看 Cell 的整个状态,并申请任何可用的集群资源。一旦调度器做出资源调度决策,它就会在原子提交中更新本地的 Cell State 的资源状态副本。若同时有多个调度器申请同一份资源,State Storage 模块可以根据任务的优先级,选择优先级最高的那个任务进行调度。

可以看出,在 Omega 系统中的每个调度器,都具有对整个集群资源的访问权限,从而允许多个调度器自由地竞争空闲资源,并在更新集群状态时使用乐观并发控制来调解资源冲突问题。

这样一来,Omega 就有效地解决了两层调度中 Framework 只拥有局部资源,无法实现全局最优的问题。接下来,我们看一下 Omega 共享调度的工作原理吧。

Omega 共享调度工作原理

Omega 使用事务管理状态的设计思想,将集群中资源的使用和任务的调度类似于基于数据库中的一条条事务(Transaction)一样进行管理。显然,数据库是一个共享的状态,对应 Omega 中的 Cell State,而每个调度器都要根据数据库的信息(即集群的资源信息)去独立完成自己的任务调度策略。

如下图所示,在一个应用执行的过程中,调度器会将一个 Job 中的所有 Task 与 Resource 进行匹配,可以说 Task 与 Resource 之间是进行多对多匹配的。其间,调度器会设置多个 Checkpoint 来检测 Resource 是否都已经被占用,只有这个 Job 的所有 Task 可以匹配到可用资源时,这个 Job 才可以被调度。

这里的 Job 相当于一个事务,也就是说,当所有 Task 匹配成功后,这个事务就会被成功 Commit,如果存在 Task 匹配不到可用资源,那么这个事务需要执行回滚操作,Job 调度失败。无论事务是否执行成功,调度器都会在事务执行之后,重新从主本那里同步更新本地 Cell State 的资源状态副本,以保证本地集群信息状态的有效性。若事务未成功执行,则调度器会在必要时重新运行其调度算法并再次尝试申请资源。

也就是说,调度器对 Job 的调度是具有原子性的,一个 Job 的所有 Task 都是一起调度的,即使部分 Task 调度失败了,调度器再次调度时也必须再次调度整个 Job。多个调度器可以并行调度,无需等待其他调度器调度结果,若存在冲突时,进行冲突处理,比如根据 Job 的优先级,优先级高则获得资源。

由此我们可以看到,Omega 涉及了 Job 并发调度。针对这一点,Omega 采用了传统数据库中的乐观锁(MVCC,Multi-Version Concurrency Control,基于多版本的并发访问控制),即每一个应用都发放了所有的可用资源,在更新集群状态时使用乐观并发控制来解决资源冲突问题,来提高 Omega 的并发度。

不同的 Omega 调度器可以实现不同的策略,但有一些调度规则是所有调度器必须达成一致的,比如哪些资源是允许分配的、如何评估作业的优先级等。

因此,Omega 调度器将两层调度器中的集中式资源调度模块简化成了一些持久化的共享数据(状态)和针对这些数据的验证代码。而这里的“共享数据”,实际上就是整个集群的实时资源状态信息,而验证代码就是解决调度冲突的调度规则。

知识扩展:单体调度、两层调度和共享调度的区别是什么,现在,我已经带你学习了单体调度、双层调度和共享调度,那么这三种调度的区别是什么呢?接下来,我们就一起回忆并对比下吧。我把这三种调度的架构示意图放到一起,先帮你有一个整体认识。

单体调度,是由一个中央调度器去管理整个集群的资源信息和任务调度,也就是说所有任务只能通过中央调度器进行调度。这种调度架构的优点是,中央调度器拥有整个集群的节点资源信息,可以实现全局最优调度。但它的缺点是,无调度并发性,且中央服务器存在单点瓶颈问题,导致支持的调度规模和服务类型受限,同时会限制集群的调度效率。因此,单体调度适用于小规模集群。

两层调度,是将资源管理和任务调度分为两层来调度。其中,第一层调度器负责集群资源管理,并将可用资源发送给第二层调度;第二层调度接收到第一层调度发送的资源,进行任务调度。这种调度架构的优点是,避免了单体调度的单点瓶颈问题,可以支持更大的服务规模和更多的服务类型。但其缺点是,第二层调度器往往只对全局资源信息有部分可观察性,因此任务匹配算法无法实现全局最优。双层调度适用于中等规模集群。

共享状态调度,多个调度器,每个调度器都可以看到集群的全局资源信息,并根据这些信息进行任务调度。相较于其他两个调度架构来说,共享状态调度架构适用的集群规模最大。这种调度架构的优点是,每个调度器都可以获取集群中的全局资源信息,因此任务匹配算法可以实现全局最优性。但,也因为每个调度器都可以在全局范围内进行任务匹配,所以多个调度器同时调度时,很可能会匹配到同一个节点,从而造成资源竞争和冲突。

虽然 Omega 的论文宣称可以通过乐观锁机制,避免冲突。但在工程实践中,如果没有妥善处理资源竞争的问题,则很可能会产生资源冲突,从而导致任务调度失败。这时,用户就需要对调度失败的任务进行处理,比如重新调度、任务调度状态维护等,从而进一步增加了任务调度操作的复杂度。

我将单体调度、两层调度、共享状态调度总结在了一张表格中:

接下来,我以 Google 的 Omega 集群管理系统为例,和你分享了共享状态调度的架构和工作原理。共享状态调度包含多个调度器,每个调度器都可以看到集群的全局资源信息,并根据这些信息进行任务调度。共享状态调度是乐观并发调度,调度器将其调度的结果以原子的方式提交给资源维护模块,由其决定是否进行本次调度。接下来,我整理一张思维导图来帮助你巩固今天的核心知识点。

我想让你知道的是,在分布式领域中,共享状态调度是 Google 号称的下一代集群管理系统 Omega 的调度机制,可以解决双层调度无法实现全局最优的问题,同时也避免了单体调度的单点瓶颈问题。但说到这儿你可能会回想起曾经看到的两句话:1.为了达到设计目标,Omega 的实现逻辑变得越来越复杂。 在原有的 Borg 共享状态模型已经能满足绝大部分需要的情况下,Omega 的前景似乎没有那么乐观。2.Omega 系统缺点是,在小集群下没有优势。

这里,我再与你解释下,为什么说 Omega 是 Google 准备打造的下一代集群管理系统。从调度架构方面来看,Borg 无法支持同时存在多种业务类型的场景,并且存在单点瓶颈问题。而 Omega 解决了 Borg 的这两个问题,但是当多个调度器并行调度时,可能存在资源冲突,当资源申请产生冲突时,会导致大量任务或任务多次调度失败,增加了任务调度失败的故障处理的复杂度,比如需要进行作业回滚、任务状态维护等。

因此,设计一个好的冲突避免策略是共享状态调度的关键。对于小规模集群来说,其集群规模、任务数量等都不大,使用单体调度就可以满足其任务调度的需求,避免了考虑复杂的冲突避免策略。也就是说,共享状态调度比较适合大规模、同时存在多种业务类型的场景,不太适合小规模集群。

分布式技术原理(九):分布式体系结构之集中式结构相关推荐

  1. 分布式体系结构:集中式结构

    分布式体系结构:集中式结构 前言 什么是集中式结构? 经典集中式结构 Google Borg Kubernetes Mesos 分析对比 知识扩展:Mesos 是如何支持容器部署的? 总结 前言 云上 ...

  2. 分布式体系结构:非集中式结构

    分布式体系结构:非集中式结构 前言 什么是非集中式结构? Akka 集群 Redis 集群 Cassandra 集群 对比分析 知识扩展:如何优化 Gossip 协议中的重复消息问题? 总结 前言 虽 ...

  3. 分布式体系结构之非集中式结构

    前言 上文介绍了分布式体系结构中的集中式结构,目前很多云上的管理都采用了集中式结构,但是这种结构对中心服务器性能要求很高,而且存在单点瓶颈和单点故障问题.为了解决这个问题,分布式领域中又出现了另一经典 ...

  4. springcloud分布式事务解决方案_搞懂分布式技术18:分布式事务常用解决方案

    本文转载自 http://linkedkeeper.com 本文内容参考网络,侵删 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 http ...

  5. 分布式技术原理(七):分布式计算

    目录 分布式计算 什么是分而治之? 分治法的原理 MapReduce 工作原理 MapReduce 实践应用 总结 分布式计算模式之Stream Stream 工作原理 分布式计算 Hadoop 这个 ...

  6. 分布式技术原理与实战45讲--06 加餐1:如何准备一线互联网公司面试?

    本课时我们来讲讲如何准备一线互联网公司面试. 互联网技术面试的特点 互联网公司的技术面试有一些侧重点,国内互联网公司和外企的侧重点又有不同.BAT 互联网公司看重项目能力,重点考察语言深度和项目能力, ...

  7. 分布式技术原理与实战45讲--05 第05讲:共识问题:区块链如何确认记账权?

    本课时我们主要讲解"共识问题:区块链如何确认记账权?" 区块链可以说是最近几年最热的技术领域之一,区块链起源于中本聪的比特币,作为比特币的底层技术,本质上是一个去中心化的数据库,其 ...

  8. 分布式技术原理(八):分布式存储

    目录 分布式存储 分布式系统设计原则 CAP准则 CAP策略选择 分布式存储系统三要素

  9. 分布式锁原理——redis分布式锁,zookeeper分布式锁

    首先分布式锁和我们平常讲到的锁原理基本一样,目的就是确保,在多个线程并发时,只有一个线程在同一刻操作这个业务或者说方法.变量. 在一个进程中,也就是一个jvm 或者说应用中,我们很容易去处理控制,在j ...

最新文章

  1. python 数据库查询结果邮件提醒_Python实现的查询mysql数据库并通过邮件发送信息功能...
  2. noip2009 靶形数独
  3. [pytorch、学习] - 4.2 模型参数的访问、初始化和共享
  4. sqlserver存储过程加锁后怎么解锁_MySQL 的加锁处理,你都了解的一清二楚了吗?...
  5. delphi mysql 端口_delphi连接mysql
  6. 3DS更新R4烧录卡内核
  7. 【原创】VBA学习笔记(4)VBA函数 和 worksheetfunction工作表函数
  8. 我的职业规划500字计算机范文,职业生涯规划自我分析(职业生涯规划500字)
  9. 移动终端安全模块技术研究
  10. 强大的头像制作神器微信小程序源码/支持外卖CPS等优惠劵小程序源码
  11. Deltix宣布CryptoCortex与MPC钱包提供商Curv整合
  12. 一些关于 CAD数据库 插入字段
  13. 在word中粘贴的图片为什么显示不完整
  14. Scrapy学习笔记 爬取w3school
  15. 在Linux上部署第一个web项目
  16. moodle 1.9 课程 恢复 2.0
  17. 【自考】又到自考总结季
  18. 不怕神一样的对手就怕猪一样的队友
  19. 互联网周刊:新技术驱动下的市场渠道创新
  20. 12v转±5v、3.3v电路

热门文章

  1. 基于python的人脸识别考勤_face++与python实现人脸识别签到(考勤)功能
  2. 五道口宅男 - Category: Android
  3. 小程序利用云函数发送电子邮件功能详解
  4. python实战项目示例 :揭秘微信朋友圈(调用wxpy库)
  5. linux 中gcc运行命令,Linux gcc命令的具体使用
  6. signaltap使用简记
  7. 怎么去除视频水印软件?
  8. 【快速了解造成游戏过程卡的罪魁祸首】
  9. 代码随想录算法训练营Day02 | LeetCode977.有序数组的平方、209.长度最小的子数组、59.螺旋矩阵II
  10. Sentinel-5P数据介绍与预处理