今天我们来讨论分布式资源调度。
为什么需要资源调度
从计算机演化说起
我们知道,计算机的出现很大程度上是为了分担人类的工作的。所以,整个计算机体系架构的演化的过程,都离不开对任务与资源这两个因素的考虑。如何利用最少的资源,运行最多的任务,且耗时最短,这是一直以来伴随我们以及科学家的难题。对于单机系统来说,从最早的单道程序设计技术、到多道程序设计技术、到现在的多核并行架构,解决方案正在逐步进化,也就是我们最直观的感受:计算机处理任务变快了。
从操作系统进程调度到分布式资源调度
我们可以类比一下操作系统的概念。相比于分布式资源调度,操作系统其实就是一种微观上的资源调度。我们把任务与任务相关的一系列上下文(包括程序代码与数据),统统抽象为进程。进程就是任务。在单核CPU架构下,由于只有一个CPU核,所以我们只能同时对一个任务进行处理。
但是,我们的任务数量不只会有1个,而是会远远超出CPU的数量,即“僧多粥少”的局面,所以,操作系统的进程调度算法出现了,比如时间片轮转调度算法,即一段时间内,CPU快速在多个任务之间快速切换、交替执行,故对每个任务内部来说,好像自己在独占CPU一样,这就是所谓的并发。除此之外,还有高优先级、高响应比、多级反馈队列调度等等任务调度算法,都在力图在单核CPU基础上解决这个问题。但是,我们一定不会满足,为了追求更高程度的并发,即在同一时刻允许多个任务同时运行。所以,多核CPU就这样诞生了,即实现了所谓的并行。那么同理,分布式系统也同样需要这种资源与任务的调度机制,来协调资源与任务之间的关系。
分布式系统存在的意义之一,就是解决单体架构执行任务时的性能瓶颈,所以,我们找来了一堆机器,来分担原来一台机器上的计算任务。但是,资源是多了,但是我们要如何利用呢?这里面就涉及到资源如何公平公正的分给每一个计算任务,让整个集群合理的利用硬件资源,短时、高效、公平的完成一系列的计算任务,而不至于某个任务被饿死或者撑死。所以,需要一个宏观上的“操作系统”,来合理的将无穷多个计算任务,分配到m个集群节点的计算资源上去执行。这,就是为什么需要分布式资源调度机制。
单体调度
对于操作系统进程调度来说,资源只有一份,那就是当前操作系统所在的计算机硬件资源;而任务有很多,资源:任务 = 1:N的关系,操作系统在进行任务调度之前,只需要收集它所在的计算机的硬件资源即可。而对于一个分布式系统中的集群,计算资源分布在多个节点上,任务还是有很多,他们之间是M:N的关系。所以,分布式系统的工作稍微复杂些,它需要收集所有节点上的资源信息而非仅仅一个节点,然后对所有收集来的资源信息做一个统筹规划。
什么是单体调度
如果让我们自己设计一个调度系统,我们自然会想到之前讲过的“分布式经典架构”中的集中式架构,由一个节点全权负责资源分配与任务调度。这,其实就是单体调度。单体调度模块称为“Scheduler”或“单体调度器”。所有的资源请求和任务调度都通过这个中心节点来进行。集中式调度器的常见模型,如下图所示。:
我们看到,master节点会收集每个节点的节点状态并交给master中的cluster state模块。这个节点状态就是指集群的计算资源的分布情况,而这个cluster state模块一般是一种内存数据库。然后,橙色方框Scheduling logic会到cluster state中查询集群资源的分布情况,然后根据分布情况执行自己的调度逻辑,进而将任务分配到各个节点上去执行。
我们可以看到,单体调度器拥有全局资源视图和全局任务,可以很容易地实现对任务的约束并实施全局性的调度策略。目前很多集群管理系统采用了单体调度设计,比如Google Borg、Kubernetes等。Kubernetes的架构经过我们之前的学习,相信你已经很熟悉了,下面我们来介绍Borg的调度架构,由于Kubernetes吸收了许多Borg的先进理念,说不定你会在Kubernetes的架构中看到许多Borg的影子。下面我们来以Borg为例,介绍一下它的单体调度实现。
单体调度的典型实现 — Borg
Borg是谷歌内部的大规模集群管理系统。有了之前的理论基础,我们直接上Borg的架构图:
我们看到,Borg主要由BorgMaster与Borglet构成。BorgMaster是整个集群的大脑,Borglet代表集群中的节点在这里,我们主要关注BorgMaster中的调度器Scheduler组件,它负责任务的调度,当用户提交一个作业给 BorgMaster 后,BorgMaster 会把该作业保存起来,并将这个作业的所有任务加入等待队列中。调度器异步地扫描等待队列,将任务分配到满足作业约束、且有足够资源的计算节点上。那么,Borg调度器是如何快速找到满足任务资源需求的那个机器呢?这个算法主要分为两个阶段:
- 可行性检查,把不满足资源需求的机器过滤(Borglet)
- 评分,从可行的机器中选择一个合适的机器(Borglet)
可行性检查
首先看可行性检查阶段,这个很好理解。假如当前任务需要8G内存的资源,而某个机器的内存总数低于8G,那么这台机器则会被无情的过滤掉。
评分
接下来就会进入到评分阶段,既然现在的所有机器已经符合要求了,是不是我们随便找一台机器把任务分了就完事了呢?其实不是。我们可以想想,大概有如下两种方案:
- 将任务尽可能平均的分配到各台机器上。
- 优先把任务都塞到某一台机器上,直到这一台塞不下了,再塞第二台机器,以此类推;
第一种方案被称为“最差匹配算法“,第二种方案被称为”最佳匹配算法“。我们分析一下这两种方案的优缺点:
最差匹配算法
- 优点:由于各台机器上负载较均衡,所以不会造成机器长时间高负载运行,比较适合小型任务较多的场景
- 缺点:它会导致每个机器都有少量的无法使用的剩余资源,会存在较多碎片
最佳匹配算法
- 优点:由于任务会优先塞满第一台机器,那么其余大部分机器都是没有任务的,所以对需要资源多的大型任务的执行非常友好,适合大型任务较多的场景
- 缺点:不利于有突发负载的应用,而且对申请少量 CPU 的批处理作业也不友好,因为这些作业申请少量 CPU 本来就是为了更快速地被调度执行,并可以使用碎片资源。还有一个问题,这种策略有点类似“把所有鸡蛋放到一个篮子里面”,当这台服务器故障后,运行在这台服务器上的作业都会故障,对业务造成较大的影响
单体调度的优缺点
优点
- 单体调度器有所有节点的资源信息,而且任务也由单体调度器来分发,所以它拥有全局的视野,对整体资源的把控较为准确
- 单体调度系统的状态同步比较容易且稳定,这是因为资源使用和任务执行的状态被统一管理,降低了状态同步和并发控制的难度。
缺点
- 单体调度器承担任务过重,很容易达到单点瓶颈
- 调度算法只能全部内置在核心调度器当中,因此调度框架的灵活性和策略的可扩展性不高。
- 单体调度存在单点故障的可能性。
两层调度
在单体调度架构中,中央服务器的性能会限制调度的效率,这个很好理解,但为什么会限制支持的任务类型呢?
简单地说,这是因为不同的服务具有不同的特征,对调度框架和计算的要求都不一样。比如说,你的业务最开始时只有批处理任务,后来发展到同时还包括流式计算任务。这两种计算任务的资源与调度需求各不相同,所以我们的调度器需要适配每一种任务,为每一个类型的任务设计不同的资源分配与调度策略,所以单体调度框架会随着任务类型增加而变得越来越复杂,最终出现扩展瓶颈。
为了解决以上单体调度的问题,一种方法就是另起一层,分担中央服务器的任务,将任务调度与适配放到我们刚才说的具体的二层调度器中,一层调度器不再去适配每一种任务的资源与调度需求。也就是说,一层调度器只负责资源管理和分配,二层调度器负责任务与资源的匹配。这就是我们所的两层调度架构。
总结一下,在两层调度中,中央调度器从整体上收集节点资源信息,并进行资源的管理与分配,将资源分配到第二层调度器;再由第二层调度器负责将资源与具体的任务配对。所以,第二层调度可以有多个调度器,以支持不同的任务类型:
看到这里大家可能还是不明白一层调度器这个资源分配,到底是分配了什么。我们用一个例子来详细讲解一下:
两层调度的典型实现 — Mesos
Mesos也是一个大型分布式集群资源管理框架。既然是资源管理,所以Mesos只负责集群底层资源的管理和分配,并不涉及任务调度与管理等功能。所以,Mesos如果要实现类似Borg那样的资源与任务管理,还需要上层框架的配合。
Mesos本身实现的调度器为第一层调度,负责资源管理,然后将第二层任务调度,交给框架完成。所以,Mesos是一个典型的两层调度架构:
这里我们所说的二层调度的框架,是运行在Mesos上,是负责任务管理与调度的组件,比如 Hadoop、Spark等,每个框架有他们自己的任务调度器,用于调度并完成不同的任务,比如批处理任务、实时分析任务等。框架主要由调度器(Scheduler)和执行器(Executor)组成,调度器可以从 Master 节点获取集群节点的信息 ,执行器在Slave节点上执行任务。
在Mesos中,分配资源的过程叫做Resource Offer机制。Mesos Master主动将节点空闲资源,以类似发放Offer的方式发给每个框架,如果框架需要则使用,不需要则还回。也就是说,通过 Resource Offer 机制,第一层调度器将资源主动分配给第二层调度器,然后第二层调度进行具体的任务匹配,从而实现了任务调度与资源管理的分离。
总结一下,Mesos Master通过资源分配算法决定给各个Framework提供多少资源,而Framework则决定接受哪些资源,以及哪些任务使用这些资源运行。这样一来,一个两层调度架构就实现了。
但是两层调度的一个问题是,由于第二层调度只能获得部分资源视图,并没有单体调度掌控全局的能力。因此无法实现全局最优调度。为了解决这个问题,共享状态调度机制出现了。
共享状态调度
为了解决单体调度的扩展瓶颈问题,以及两层调度只能获得部分资源视图的问题,我们想,那么让两层调度器也能够看到所有节点的状态不就可以了,即我们要一个地方来存储所有节点的状态就OK了。这,就是共享状态调度。
共享状态调度结合了单体调度掌控全局的特点,以及两层调度职责分离的优势。通过将单体调度器分解为多个调度器,且每个调度器都有全局的资源状态信息,从而实现最优的任务调度,提供了更好的可扩展性。其架构如下:
共享状态调度的典型实现 — Omega
Omega是Google的第二代集群管理系统,Omega 在设计时参考了 Borg 的设计,吸收了Borg 的优点,并改进了其不足之处。
Omega中以Cell为单位来管理集群,它是一个集群中的节点集合。比如集群中有10个节点,那么我们可以把其中的2个节点称为一个Cell。在Omega中,由于需要共享每一个Cell的资源状态,那么需要一个共享存储空间,来共享每个Cell的状态。其架构图如下:
我们看到,为了提高性能、并且能更方便的查到共享的集群状态数据,每个Cell都会从中心的State Storage同步一份数据到每个Cell内部。这样一来,Omega 就有效地解决了两层调度中 Framework 只拥有局部资源,无法实现全局最优的问题。
总结
那么这几种资源调度方案字哪种场景下使用更好呢?
- 在小规模的集群应用场景,我们应该尽量采取单体调度模型。这是因为这种模型设计和使用都比较简单,调度器容易对整个系统的状态有全面的把握。
- 在规模较大的集群中,可以考虑使用双层调度器设计。 双层调度器调度策略的编写较为复杂,而且框架调度器并没有全局的资源视野,调度准确率较低。随着新一代共享状态调度器的发展,未来可能会慢慢退出舞台。
- 共享状态调度器因为集合了单体调度与两层调度的优点,以及适应多种任务调度需求、灵活性高、可扩展性强的特点,正渐渐变成主流。
最后我们把讲过的三种调度方式做一个比较:
下期预告
【分布式系统遨游】分布式计算
关注我们
欢迎对本系列文章感兴趣的读者订阅我们的公众号,关注博主下次不迷路~
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。