Google最近公布了他们在系统基础设施领域的一颗王冠宝石:Borg,一个集群调度器。这促使我重新阅读论论述相同主题的Mesos和Omega的论文。我想这几种系统做一个的对比会很有意思。Mesos因为其开创性的两层调度(two-level scheduling)理念而广受美誉,Omega在此基础上用类似数据库的技术做了改进,Borg可以看作是对所有这些思想的***之作。
1 背景
集群调度器可谓是历史久远,它出现在大数据的概念之前。在高性能计算(HPC)的领域里,关于如何调度成千的核心已经有了丰富的资料,但相比于数据中心调度解决的问题,核心调度的问题范围显然简单很多。而Mesos/Borg等系统要解决的正是数据中心调度。接下来,让我们通过几个维度对比下它们。
1.1 调度中的局部性(Scheduling for locality)
超级计算机可以将存储和计算分离,并用和内存速度差不多的近乎全等分带宽的网络将它们连接起来。这意味着你的任务可以放在集群的任何地方,而不用担心具体位置,因为所有的计算节点都可以相同快速访问到数据。有一些特别优化过的应用会针对网络拓扑进行优化,但是这些情况很少见。
数据中心调度器需要关心具体位置,实际上这是GFS和MapReduce共同设计的所有关键所在。21世纪初的时候,相比于硬盘带宽,网络带宽更加昂贵。所以为了降低成本,设计上会把数据存储和计算任务放到同一个节点上。这是一个主要的调度限制,尽管之前在你能将任务放到任何地方,如今你需要将它放在三个副本的某一副本中。
1.2 硬件的配置
超级计算机通常由同类节点组成,例如他们都有一样的硬件配置。这是因为超级算计通常是一次性采购的:一个实验室获得了数百万的经费用于更新硬件。一些HPC的应用是根据某超级计算机中特定的CPU模型来优化的。新的技术如GPU或者协处理器,会当做一个新的集群推出。
在大数据的领域,集群主要受到存储的限制。因而运维会不断的添加新机架来为集群扩容。这意味着对于节点有着不同的CPU、内存能力、硬盘数量等现象很常见。同时也会添置一些特别如SSD、GPU或者叠瓦式硬盘(shingled drive)。一个单独的数据中心需要大范围的应用,而这一切又会强加额外的调度约束。
1.3 队列管理和调度
当在超级计算机上运行应用时,你需要指定需要的节点数,要提交作业的队列,以及作业的运行时间。队列会对你能请求多少资源和你的任务可以运行多久做不同限制。队列也有一个基于优先级或预留的系统,来决定排序。因为这个任务的时长都知道了,这是一个十分简单的装箱的问题。如果队列很长(通常是这样),并且有相当多的小任务来填充大的任务留下的空间,你可以获得相当高的利用率。我喜欢将这个描述用2D的方式可视化呈现,时间是X轴,资源利用率作为Y轴。
如前文所述,数据中心调度是一个普遍的问题。资源请求的形式可以各种各样,并且可以有更多的维度。任务也没有一个设定好的时间范围,因此预先计划队列很困难。于是乎,我们有了越来越复杂的调度算法,然后调度器的性能变得至关重要。
利用率一般而言是会变得越来越差(除非你是Google;后面还会再提到),但是相比HPC工作量的一个优点是,MapReduce和类似的任务,可以增量调度(incrementally scheduled)而无需成组调度(gang scheduled)。在HPC中,我们须等待请求的N个节点都到位,然后一次性运行任务。MapReduce可以一波一波地运行它的任务,这意味着它仍然可以有效地利用剩余的资源。单一的MR作业也可以基于集群的需求停止或者继续,避免了抢占或资源预留,并且还有助于实现多用户之间的公平性。
2 Mesos
Mesos早于Yarn出现,设计的时候考虑到了最初MapReduce存在的问题。在那时候,Hadoop集群只能运行单一的应用:MapReduce。这就很难运行不符合一个map阶段跟着一个reduce的阶段这种模型的应用。这里***的一个例子是 Spark。先前,你不得不为Spark安装一套全新的worker和master,用来和你的MapReduce的worker和master一起运行。这从资源利用的角度来讲完全不理想,因为它们通常是静态分区的。
Mesos可以为所有集群应用提供一个通用的调度器API来解决了这个问题。MapReduce和Spark变成了不同简单应用,使用的是同一个底层资源分享框架。最简单的方式是写一个中心化的调度器,但是这有一些缺点:
- API的复杂度。我们需要一个API,其得是所有已知的框架调度器API的超集。这是本身就很困难。如何表达资源的请求,同样变得十分复杂。
- 性能。数万的节点和数百万的任务数目不小。特别当调度问题复杂的时候。
- 代码的敏捷性。新的调度器和新的框架不断的出现,这会带来新的需求。
相反的,Mesos引入了新的“两层调度”(two-level scheduling)的概念。Mesos将每应用程序自身的调度工作委派给应用程序,但Mesos仍然负责在应用之间的分派资源,保证整体上的公平性。所以Mesos非常的轻量,只有10K行代码。
“两层调度”是通过一个比较文艺的API叫做“资源发放(resource offer)”的接口来完成的。Mesos会周期地向应用程序调度器“发放”一些资源。这在开始听起来可能很落后(请求从master发到应用?),但是实际上没有那么奇怪。在MR1中,“任务追踪器工作者(TaskTracker worker)”了解各节点上具体的运行情况。当一个“任务追踪器工作者”通过的心跳说某一任务已经完成,“作业追踪器”(JobTracker)会随后选择其他作业来在该“任务追踪器”上运行。调度决策是通过该worker中的一个资源发放来触发的。在Mesos中,资源发放由Mesos的master 而不是slave发出,因为Mesos管理着集群。并没有太大的不同。
“资源发放”就好似一个对部分资源有时限的租约。Mesos依据不同的策略,如优先级、公平分享等向应用发放资源。然后,应用会计算如何使用这些资源,同时告诉Mesos发放的资源中哪些是它需要的。这给了应用很大的灵活性,因为其可以选择暂时先运行一部分的任务,并等待稍后更大块的资源分配(成组分配),或者对任务进行体积调整来适应实际发放的资源情况。因为资源是有时间约束的,这也促使应用尽快的进行调度。
一些问题和它们是如何被解决的:
- 长时间的任务会霸占(hogging)着资源。Mesos能让你为一些占时短的任务保留一些资源,并能在任务超过时间限制后杀掉。这也鼓励使用短线任务,其对公平有利。
- 性能隔离。使用Linux的容器(cgroups)。
- 大型任务的饿死。要获得一个节点的独有的访问权是很困难的,因为一些应用中较小任务会蚕食它。修复方法是可以设置“最小发放大小”。
尚未解决/和解决方案未知的问题:
- 成组调度。我认为这种调度策略若不预先知道任务的长度或者进行抢占是不可能达到高利用率的。在低利用率的情况下不断地囤积资源是可以的,但是可能会导致死锁
- 跨应用的抢占(Cross-application preemption)也并非易事。资源发放API没有办法说“这里是一些低优先级的任务,如果你们有需要我可以停止他们”。Mesos依靠任务的占时短来获得公平。
3. Omega
Omega可以说是Mesos的后继者,并且实际上有一个共同的作者。因为该论文对于其评估使用的是模拟的结果,我怀疑其从来没有在Google进入过生产,并且其中的理念被带入了下一代的Borg。即使对于Google来说,重写API很可能还是改动太大。
Omega将资源发放往前更推进了一步。在Mesos中,资源发放是悲观的(pessimistic)或者叫独占的(exclusive)。如果资源已发放给了一个应用,同样的资源不会发放给另外一个应用,除非该发放的资源超时。在Omega中,资源发放是乐观的(optimistic),每一个应用都发放了所有的可用的资源,冲突是在提交的时候被解决的。Omega的资源管理器,本质上是一个保存着每一个节点的状态关系数据库,并且用不同的乐观并发控制来解决冲突。这样的好处是其大大的提高了调度器的性能(完全的并行,full parallelism)和资源利用率。
不足的的地方是,应用处于一种可以随时独占天下的状态,因为它们允许以任意的速度吞食资源,甚至抢占在其他用户之前。这对于Google来说是可行的,因为他们使用的系统是基于优先级的,并且可以对着他们内部的用户吼。他们的任务量大概分为两种优先级类:高优先级的服务类作业(HBase,web 服务器,长期运行的服务)和低优先级的批量式作业(MapReduce等类似的)。应用允许抢占低优先级的作业,并且可以停留在靠合作式强力取得的范围内,包括提交的作业数,分配的资源大小等。我想雅虎在对着用户吼这种做法上有不同的看法(这显然不是一种可伸缩的做法),但不知为何在Google居然可以。
论文大多数讨论的是这种乐观的分配策略是如何与冲突一起工作的,因为这是一个绕不开问题,这里有一些高层次的观点:
- 服务类型的作业更大型,并且对安放的位置有更严格的需求,因为需要实现容错(分布在不同的机架上)。
- Omega可以伸缩到数十个调度器,但无法伸缩到数百调度器,因为分发完整的集群状态的开销很大。
- 几秒的调度时间是很平常的。他们也和高达数十秒甚至数百秒的调度器做了比较,这正是两层调度真正了不起的地方。不能确定这种情况有多么普遍,或许只对于服务类的工作才这样?
- 集群的资源利用率通常在60%。
- 冲突足够的少见,OCC在实际中可行。在调度器崩溃之前,能达到6倍于他们正常的批量工作量。
- 增量调度非常重要。成组调度因为不断增多的冲突,实现起来十分的昂贵。显然,绝大部分的应用可以很好的适应增量调度,只需要几次部分分配( partial allocations)就能达到他们总体想要的数量。
- 即使对于复杂的调度器来说(每作业几十倍的的额外开销),Omega仍能在合理的等待时间下调度混合的工作量。
- 根据经验,在Omege中实验一个新的MapReduce调度器是十分容易的。
开放式问题
- 在一些情况下,乐观的并发控制会因为高频率的冲突和因重试产生的重复工作而崩溃。似乎他们在实际中不会遇到这个情况,但是我不知道是否在遇到奇形怪状的任务时是否会遇到最坏的场景。这受服务型和批量式的作业的影响么?这个问题是否在实际中是否会进行调优?
- 缺少全局的策略真的可以接受么?如公平,抢占等等。
- 对于不同类型的作业的其调度时间的表现是怎样的?是否有人已经写出十分复杂的调度器了?
4. Borg
这是一个充满生产环境的经验的论文。它针对的工作量,与Omega一样,因为都出自于Google,因而他们很基本点是一样的。
4.1 高层次的
- 任何东西都运行在Borg之中,包含存储系统,如CFS和BigTable等。
- 中等类型的集群包含10k左右的节点,尽管有的要大的多。
- 节点可以十分的异构。
- 使用了Linux的进程隔离(本质上来说是容器),因为Borg出现在现在的虚拟机基础设施之前。效率和启动时间当时十分重要。
- 所有的作业都是静态链接的可执行文件。
- 有非常复杂且丰富的资源定义语言可用。
- 可以滚动升级运行的作业,同时意味着配置和执行文件。这有时需要任务重启,因而容错是很重要的。
- 支持在最终被SIGKILL杀死之前,通过SIGTERM优雅的退出。也可以选择温柔地杀死,但不能保证正确性。
4.2 Allocs
- 资源分配是和进程的存活分开的。一个alloc可以用任务分组(task grouping)或者来在任务重启之间来保持资源。
- 一个alloc集(alloc set)是一个组分配在不同的机器上的alloc。多个作业可以在同一个alloc里面运行。
- 这其实是一个常见的模式。多进程对于分离担忧和开发是有用的。
4.3 优先级和配额
- 两类优先级的:高和低,分别用于服务类和批量类的。
- 较高优先级的作业能抢占较低优先级的作业。
- 高优先级的任务能互相抢占(防止连串的活锁场景)。
- 配额用于准入控制。用户需要付更高的费用以获得更高优先级的配额。
- 同时也提供了一个空闲的运行在***优先级层,来鼓励高利用率和回填式的工作。
- 这是一个简单易于理解的系统!
4.4 调度
- 两个调度的阶段:找到可用的节点,然后给这些节点打分用于***的安置。
- 可用性很大程度由任务的约束条件来决定
- 打分最主要是根据系统的属性来决定的,像***匹配(best-fit)与最差匹配(worst-fit)的对比,作业混合(job mix),失效域(failure domains),局部性(locality)等等。
- 一旦最终节点被选中,Borg会在必要时进行抢占。
- 因为需要将依赖局部化( localizing dependencies),一般的调度时间在25秒左右。下载执行文件占了80%的时间。这种局部化操作很重要。分发执行文件时用到了Torrent和树协议。
4.5 伸缩性
- 中心化并没有成为一个不可能的性能瓶颈。
- 每分钟数万节点,上万任务的调度频率。
- 通常Borgmaster使用10-14核和50GB左右的内存。
- 架构已经逐渐越来越多进程化(multi-process),参考Omega和两层调度。
- Borgmaster虽然为单主,但是一些责任有进行分片:如来自worker的状态的更新,只读的RPC等。
- 一些显而易见的优化:缓存机器的评分,每任务类型一次性地计算可用性,在做调度的决策的时候不要尝试获得全局的***性。
- 反对增大cell的主要观点是隔离操作的失误和错误的上串。架构保持良好的伸缩性。
4.6 使用率
- 他们的主要指标是cell compaction(细胞压缩),或者叫能最满足一组任务所需要最小的集群。本质上即盒包装(box packing)。
- 从这些获得了的大的提升:不隔离任务量或用户,有大的共享的集群,细粒度的资源请求。
- 在一个每Borglet的基础上乐观的过量使用(Optimistic overcommit )。Borglet能做资源评估,并且回填非生产(non-prod)的工作。如果这个评估不正确,杀死非生产的工作。内存是非弹性化的资源。
- 分享不会大的影响CPI(CPU干扰,CPU interface),但是我比怀疑对于存储这一块的影响。
4.7 吸取的教训
这里列举的问题在Kubernetes里面已经修复了,这是他们的一个开放的开源的容器调度器。
坏处
- 能调度多作业的工作流而不是单作业会很好,有利于跟踪和管理。这也需要更灵活的方式来指带工作流的组件。这通过添加任意的键值对到每一个任务,并且让用户可以方便用户查询得到解决。
- 每一个机器一个IP。这带一个机器上端口的冲突,和复杂的绑定和服务发现。这能通过Linux的命名空间,IPv6,SDN得到解决。
- 复杂的定义语言。太多的疙瘩需要解开,这让一个普通用户很难上手。需要一些在自动决定资源需求方面的工作。
好处
- Allocs 真是好极了。允许助手服务能方便的跟主要task放在一起。
- 内置的负载均衡和命名功能十分的有用。
- 指标,调试和Web界面,对于用于用户解决自己的问题十分的有用。
- 中心化向上扩展的很好,但是需要分散到多个进程里面去。Kubernetes从头做这个,意味在不同的调度器组件之间一个干净的API。
5 ***的话
看起来YARN需要借鉴Mesos和Omega的设计,才能使伸缩达到10K的节点规模。相比于Mesos和 Omega,YARN仍然是一个中心化的调度器,常常像一个稻草人一样在人们需要对Mesos和Omega进行类比的时候搬出来。Borg特别提到了需要进行分片以获得伸缩性。
要获得高资源利用率,又不牺牲SLO(Service Level Objective,服务水平目标),隔离性就至关重要。这会浮现到应用的层面,在这一层应用自身需要进行容忍时延的设计。想一想BigTable中 tail-at-scale的请求复制。最终,这也会涉及到硬件开销与软件开销成本的权衡。在低资源利用率下运行可以绕过这个问题。或者你可以迎难而上,通过OS的隔离机制,资源预估和对工作量及调度器的不断调优来解决这个问题。对Google来说,拥有那样的规模,足够多的硬件,需要聘一大批内核的开发者是相当合情合理的。而且幸运的是,这些开发者已经帮我们解决了这些问题。
我不确定对Google工作量的设想是否适用更普通的场景。对优先级分类,保留资源和抢占对于Google来说奏效,但我们的客户几乎都使用公平分享调度器(fair share scheduler)。Yahoo使用容量调度器( capacity scheduler)。Twitter也使用公平调度器( fair scheduler)。我并没有听说过需要结合优先级+资源保留的调度器( priority + reservation scheduler)的需求。
***,运行大型的共享集群,这种我们预想在Google中的上演的状况,在我们的客户中却很难遇到。我们的客户中虽然也有使用成千的节点的,但它们实际上是分散到了成百节点上的pod里面去的。把不同的用户和应用分到不同的集群,也仍然是常见的做法。集群通常在硬件上也是同构的,但我想这种情况会很快改变。