0 背景
0.1 Borg简介
Borg是Google内部的大规模集群管理系统,已经延续十余年的时间,大体上与MapReduce、GFS、BigTable、Chubby是同时代的产物,但一直被雪藏,直至今年才发表论文,披露其实现细节。论文链接(http://research.google.com/pubs/pub43438.html)。
Kubernetes被认为是Borg的开源版本(研发团队部分重合、功能简化聚焦、架构类似)。许多研发Borg的工程师现在正投身于Kubernetes系统,并从Borg中汲取经验,不断打磨Kubernetes。这也是我将此篇博客归类到“Docker& k8s”的原因。
0.2 笔者与Borg
笔者所在团队的前辈曾经联合腾讯公司研发过集群管理系统T-Borg(公司内部代号),即Torca的原型。从名字上看,不难推测T-Borg也是类似Borg的集群管理系统。笔者本人从硕士阶段就一直从事云平台/集群管理系统的研发,期间也曾从T-Borg中汲取经验。可以说我一直在关注和学习Borg系统,因此Borg公开发表论文,我也在第一时间关注。
1 Abstract & Introduction
Borg系统为Google进行集群管理,其上运行着十万级别的作业(Job)、数千级别的应用(App)、管理着数万级别的机器(Machine)。
Borg的三大优势:
(1)隐藏资源管理和错误处理的细节,使得用户可以聚焦应用开发;
(2)高可靠性和高可用性,同时支持应用的高可靠和高可用;
(3)在数万节点上有效运行工作负载。
个人解读:这三条优势说的已经比较明确了,如果再总结一下,可以归结为(1)易用性(解决分布式共性问题,屏蔽细节);(2)高可用性;(3)可扩展性(数万节点)和高利用率(有效运行)。这些都是云平台需要解决的核心问题。
Borg不是第一个关注这些问题的系统,却是仅有的几个如此大规模的系统之一。个人解读:这句话说得较为低调,Borg有可能是这个世界上规模最大的集群管理系统。
2 用户视角 The user perspective
Borg的用户是Google的开发人员和系统管理员。用户以作业(Job)的形式向Borg提交工作,其中每个作业(Job)包含着一个或者多个运行相同程序的任务(task)。每个作业(Job)运行在同一个Borg单元(Cell)中,单元(Cell)是一组机器的集合。
2.1 工作负载
Borg Cell中运行着异构的工作负载,主要包括两个部分:服务和批处理作业。服务长时间运行,几乎不会停止,处理短期的、延迟敏感的请求,服务通常是面向终端用户的产品(如Gmail、Google Docs、web search)和一些内部基础设施服务(BigTable,即HBase的Google版本)。批处理作业运行时间在几秒到几天不等;它们对短期性能波动并不敏感。工作负载会在Cell上混合部署。
大部分应用框架已经运行在Borg上多年,包括内部的MapReduce、FlumeJava、Millwhell、Pregel。Google的分布式存储系统也都运行在Borg之上,包括GFS、BigTable、Megastore等。
论文将高优先级的作业成为“生产型作业”(prod),将其他称为“非生产型作业”(non-prod)。大部分长时间运行的服务都是生产型作业,生产型作业被分配了大约70%的CPU资源并占用了大约60%的利用率;同时被分配了大约55%的内存资源并占用了大约85%的内存利用率。
个人解读:可以发现,在Google内部,无论是服务型还是批处理型,绝大多数作业都运行在Borg之上,包括其核心产品和非常重要的基础设施。
2.2 集群和单元Cluster and cells
一个集群Cluster通常包括一个大规模的单元Cell和若干个小规模的用于测试和特殊目的的单元Cell。Cell的规模大约是10k节点,且机器是异构的。
个人解读:集群Cluster包含若干个单元Cell,单元Cell又包含大量的机器Machine。三者关系示意图如下图所示。相比于Cluster和Machine,Cell的概念较为少见。但其实Cell的概念在工程实践中比较常见,很多项目在上线之初将Cluster作为一个整体使用,后来出于某种目的(绝大多数时候是就是为了测试)又将集群分为两部分使用,这样集群就被划分成为一个主体Cell和一个用于测试的小Cell,与论文描述一致。可以说,Cell的概念脱胎于实践,而不是Google拍脑袋想出来的。
2.3 作业和任务Jobs and tasks
一个Borg作业Job具有属性,包括name、owner、task数量等。Job具有约束(constraint)来强制其任务运行在具有特定属性的机器上,例如处理器架构、OS版本、IP地址等。约束可以分为硬约束和软约束,前者是需求,必须满足;后者是偏好,尽量满足。
个人解读:文中提到了约束constraint的概念,在集群调度领域提出约束主要是为了解决任务和集群的异构性挑战。本人在研究和实践中也一直关注约束,并在工程中应用了自己的相关研究成果。这里不做过多展开,如果要进一步了解约束的概念以及软硬约束的区别,可以研读下面两篇论文:Modelingand Synthesizing Task Placement Constraints in Google Compute Cluster(http://research.google.com/pubs/pub36953.html)和Alsched: Algebraic Scheduling of Mixed Workloads in Heterogeneous Clouds(http://www.pdlNaNu.edu/PDL-FTP/CloudComputing/alsched-socc2012.pdf)。
每个任务,可以映射为一个container中的一组Linux程序。Borg绝大部分工作负载不运行在虚拟机中,这主要是由于虚拟机性能开销较大,此外一些硬件不支持虚拟化。
一个任务也具有属性,例如资源需求和task index。Borg程序采用静态链接库,以减少对运行环境的依赖,软件包中主要包括二进制程序和数据文件。
个人解读:在云平台工程实践中,打包和部署是一个难以忽视的重要挑战。Docker基本上就是凭借解决部署问题而风靡开源界的。在Docker出现之前,传统虚拟机(例如KVM、Xen)虽然支持镜像部署,但是性能开销较大;轻量级虚拟化(例如LXC和更底层的cgroups)虽然性能开销小,但是自身不具备镜像,打包部署琐碎繁杂。Docker兼具二者优势,解决了这个问题。
用户向Borg发送RPC用以操作作业。用户可以提交作业、查询作业,也可以改变作业、任务属性。作业Job和任务task的状态图如下图所示。用户可以触发submit、kill和update事务。
个人解读:作业Job和任务task的生命周期管理要基于这个状态图来实现,特别是错误处理,要在状态图的基础上基于事件触发来实现。例如,在我实现的系统中,当任务finish、fail、kill、lost时,系统会根据特定事件进行专门处理。
任务Task在被抢占之前,会收到通知,二者都是通过Unix信号,抢占通过SIGKILL信号,程序直接退出;通知通过SIGTERM信号,可以被处理。这样做主要是为了让程序关闭前清理环境、保存状态、完成当前请求等。
个人解读:云平台需要支持任务“优雅”的退出,Borg不是个例,LXC和Docker也有类似功能。例如,docker stop命令就是优雅退出,其机制与Borg类似;而docker kill则是直接退出。
2.4 Allocs
一个Borg alloc是一台machine上预留资源的集合,可以用于一个或多个任务运行。Alloc被用于为未来的task预留资源,用来在停止和再次启动任务之间保持资源,或者将不同作业Job的任务task聚合在一起运行(例如一个web服务器实例和一个相关的logsaver)。
一个Alloc集合类似一个job:它是在多个机器上预留资源的一组alloc。一旦一个alloc集合被创建,一个或者多个作业Job就可以被提交在其中运行。简而言之,一个task与一个alloc对应,一个job与一个alloc集合对应。
个人解读:Alloc是allocation的简称,应该可以理解为资源分配对应的逻辑单元,或者直接理解为资源单元。初读觉得alloc类似container,但是由于一个alloc可以支持一个或多个task运行,因此alloc实际上更类似Kubernetes中的pod(一个pod中包含若干个container)。另外,Kubernetes中没有Job和Task的概念,我个人感觉Kubernetes中的Service类似于Borg中Job(仅限于服务型Job)的概念,Kubernetes中container类似于Borg中task的概念。当然,仅是类似,说完全相同是不准确的。
2.5 优先级,配额和准入控制 Priority, quota, and admission control
每个作业都有一个优先级,具体形式是一个小的正整数。Borg定义非重叠优先级,包括:monitoring, production, batch, and best effort (also known as testing or free),生产型作业(prod job)包含前两种优先级。
为了避免“抢占洪流”,Borg不允许一个生产型作业区抢占另一个,只允许生产型作业抢占非生产型作业。
资源配额Quota是一组资源量化表达式(CPU、RAM、Disk等),它与一个优先级和一个时间段对应。如果Quota不足,作业提交会被拒绝。
个人解读:在我们的工程实践中,Quota设置没有如此细粒度,但是基本思路一致。有一点不同需要说明一下,在Borg中如果Quota不足会立即拒绝作业提交;但是在我们的系统中,Quota不足并不影响作业提交,作业会进入等待队列,待前序作业运行完毕、Quota充足后,系统再自动调度和运行该作业。起初,我对于Borg的设计不是很理解,拒绝提交不是更“麻烦”吗?后来我思考,我的系统的用户多是不太熟悉分布式的应用开发人员,希望我们尽可能的为他们屏蔽细节,所以我们自动化的进行提交、调度和运行;Borg的用户是Google开发人员,是高水平的程序员,我猜测他们需要了解一些必要的“细节”来进行后续操作。前者更需要便利,后者更需要灵活,用户的不同导致系统设计机制的不同。
2.6 名字服务和监控 Naming and monitoring
仅仅是创建和放置任务是不够的,一个服务的客户端或者其他系统需要找到服务,当服务被重新定位到新机器之后也是如此。为此,Borg创建了一个稳定的Borg名字服务(BNS),用于为每个任务命名。任务名字包括cell name、job name和task number这个三元组。Borg将任务的主机名和端口号写入Chubby中的一致性、高可用文件。Borg也会将作业规模和任务健康信息写入Chubby,负载均衡器LoadBalancer据此来进行路由。
个人解读:名字服务也被称为服务发现,通常使用高可用存储系统(例如Chubby,可以视为Google内部的Zookeeper,当然现在开源界etcd似乎更火)存储任务名字与访问方式endpoint的映射关系。在Borg中,任务名字包括cell name、job name和task number三元组;访问方式endpoint则是hostname和port二元组。客户端或其他系统需要访问服务时,就访问BNS根据任务名字查询访问方式endpoint,以此来“发现”服务。
每个任务会构建HTTP server来发布其健康信息和性能指标,Borg根据URL来监控任务,并重启异常任务。
个人解读:在我们的工程实践中,每个任务(更准确说其实是虚拟机)中都会包含一个代理agent,这个代理开机自启,定时上传心跳,内含健康信息和性能指标。可以说上传内容是类似的,但是通信方式是不同的。我们是虚拟机代理定时主动上传心跳,不过最近感觉“发布信息,由需要该信息的组件来查询”这种服务化的通信方式更为优雅和主流。
3 Borg架构 Borg architecture
一个Borg cell包含一组机器集合、一个逻辑中央控制器Borgmaster、每台机器上都运行的代理进程Borglet。Borg的所有组件都使用C++开发。
个人解读:我们的云平台组件也都使用C++开发,基本属于云平台惯例。不过,由于Docker、Kubernetes、etcd等开源系统都是基于Golang开发,我最近正在学习Golang。
3.1 Borgmaster
Borgmaster包含两类进程:主Borgmaster进程和分离的调度器进程。主Borgmaster进程处理客户端RPC;管理系统中所有对象Object的状态机,包括machines、tasks、allocs;与Borglet通信;提供webUI。
Borgmaster逻辑上一个进程,但是拥有5个副本。每个Borgmaster副本维护cell状态的一份内存副本,cell状态同时在高可用、分布式、基于Paxos的存储系统中做本地磁盘持久化存储。一个单一的被选举的master既是Paxos leader,也是状态管理者。当cell启动或者被选举master挂掉时,系统会选举Borgmaster,选举机制按照Paxos算法流程进行。
个人解读:Borgmaster基于高可用、分布式、基于Paxos的存储系统进行元数据持久化和热Borgmaster备份,以此实现Borg系统的高可用。关于这个基于Paxos的存储系统,在Google内部应该就是Chubby,不过不知道为何这里没有提及,难道还有新的系统?开源界etcd最近比较火,但是etcd没有采用Paxos算法,而是使用更为简单且易于理解的raft。这里还是采用Paxos算法,本人暂时还认识不到替代Chubby的必要性。
Borgmaster的状态会定时设置checkpoint,具体形式就是在Paxos store中存储周期性的镜像snapshot和增量更改日志。
3.2 调度Scheduling
当作业被提交,Borgmaster将其记录到Paxos store中,并将作业的任务增加到等待队列中。调度器异步浏览该队列,并将任务分配给机器。调度算法包括两个部分:可行性检查和打分。
可行性检查,用于找到满足任务约束、具备足够可用资源的一组机器;打分,则是在“可行机器”中根据用户偏好,为机器打分。用户偏好主要是系统内置的标准,例如挑选具有任务软件包的机器、分散任务到不同的失败域中(出于容错考虑)。
个人解读:在我们的学术研究和工程实践中,调度也分为这两个部分。我们也将这个调度流程和约束有机结合起来,可行性检查用于处理硬约束,其中具备足够可用资源也可以看作一种特殊的硬约束;打分用于处理软约束,符合“尽量满足软约束”的原则。
Borg使用不同的策略进行打分。实践中,E-PVN(worst fit)会将任务分散到不同的机器上;best fit,会尽量“紧凑”的使用机器,以减少资源碎片。Borg目前使用一种混合模型,尽量减少“受困资源”。
3.3 Borglet
Borglet是运行在每台machine上的本地Borg代理,管理本地的任务和资源。Borgmaster会周期性地向每一个Borglet拉取当前状态。这样做更易于Borgmaster控制通信速度,避免“恢复风暴”。
为了性能可扩展性,每个Borgmaster副本会运行一个无状态的link shard去处理与部分Borglet通信。当Borgmaster重新选举时,会重新分区。Borgmaster副本会聚合和压缩信息,仅仅向被选举master报告状态机的不同部分,以此减少更新负载。
如果Borglet多轮没有响应资源查询,则会被标记为down。运行其上的任务会被重新调度到其他机器。如果恢复通信,则Borgmaster会通知Borglet杀死已经重新调度的任务,以此保证一致性。
3.4 规模可扩展性Scalability
我们不确定Borg中心化架构的可扩展性极限在哪里;不过到目前为止,每次接近极限,我们都能设法消除它。一个Borgmaster可以管理数千台机器,任务到达率约为每分钟10K个任务。一个忙碌的Borgmaster使用10-14个CPUcore和50GB内存。
个人解读:这句话有点牛啊!对于分布式领域的关键挑战——可扩展性,Google人员说还没有真正遇到过极限。这句话的霸气程度不亚于在酒桌上说“我从来没醉过”(哈哈哈哈),当然最关键的是人家不是嘴炮,人家真的做到了。
为了提升可扩展性,我们将调度器分割为独立进程,这样它可以与Borgmaster并行进行操作。调度器具备多个副本,每个调度器在cell状态的副本上进行操作。它重复以下操作:从被选举master中检索状态变更;更新本地状态副本;进行调度、分配任务;将分配通知被选举master。Master会检查调度器的调度操作,如果发生调度冲突则再下一轮重新调度,否则接受并应用该调度结果。这套机制与Omega一文中的乐观并发控制高度相似。此外,Borg最近支持为不同负载类型使用不同调度器。
个人解读:Omega的核心思想就是采用乐观并发控制,使得调度并行化,解决调度器可能成为集群系统性能瓶颈的问题,提高系统的可扩展性。根据上述描述,Borg多调度器并发调度采用了这种机制。
为了提高系统可扩展性,Borg调度器还作了几种优化,分别是得分缓存(可以将可行性检查和打分结果缓存下来)、等价类(同一job中的task通常具有类似的约束,因此可以将多个任务视为一个等价类)、轻松随机化(在大规模cell中计算所有机器的可行性和得分代价太高,可以随机遍历直到找到一个“足够好”的机器)。
4 可用性 Availability
失败在大规模系统中非常常见。本文列举了Borg提高可用性的例子:
(1)自动重新调度器被驱逐的任务;
(2)为了降低相关失败,将任务分散到不同的失败域中;
(3)限制一个作业中任务的个数和中断率;
(4)限制任务重新调度的速率,因为不能区分大规模机器故障和网络分区;
(5)避免引发错误的任务-机器匹配对;
(6)关键数据持久化,写入磁盘。
5 利用率 Utilization
本节首先通过实验证明,混合部署(prod负载和non-prod负载)比独立部署具有更高的利用率。实验结果下图。
随后,结合实验说明,几种方法可以提高集群利用率,具体包括Cell sharing、Large cell、Fine-grained resource requests和Resource reclamation。前几种方法都比较直观,不做展开。Resource reclamation比较有意思,重点阐述。
一个作业(job)可以定义一个资源上限(resource limit),资源上限用于Borg决定用户是否具有足够的资源配额(quota)来提交作业(job),并且用于决定是否具有足够的空闲资源来调度任务。因为Borg会kill掉一个尝试使用更多RAM和disk空间资源(相比于其申请的资源)的task,或者节流CPU资源(相比于其要求的),所以用户总是申请更多的资源(相比其实际所有的)。此外,一些任务偶尔会使用其所有资源,但大部分时间没有。
个人解读:用户总是会处于“心理安全”和负载高峰波动等原因,申请较多的资源,但大部分时候,任务不会真正使用如此之多的资源。这就造成了资源浪费。
对于可以容忍低质量资源的工作(例如批处理作业),Borg会评估任务将使用的资源,并回收空闲资源。这个整个过程称为资源回收(resource reclamation)。评估过程称为任务预留(task reservation)。最初的预留值与其资源请求一致,然后300秒之后,会慢慢降低到实际使用率外加一个安全边缘。如果利用率超过资源预留值,预留值会快速增长。
这里引用华为钟诚的图片,直观的表明实际使用资源、预留资源、资源上限的关系。
6 隔离 Isolation
包括安全隔离和性能隔离,这里不做展开。
7 相关工作 Related work
资源调度是热点研究问题,具有许多相关研究。论文介绍了许多相关系统,我这里找几个重点系统进行简要分析。
7.1 Mesos
Mesos通过resourceoffer机制,将资源管理和放置功能分开。Mesos的中央资源管理器(Mesos master)负责资源管理,多个框架(例如Hadoop、Spark)负责具体的任务放置(即任务调度)。Borg采用基于请求(request-offer)的机制,而非基于offer的机制,更易于系统扩展。
7.2 YARN
YARN是Hadoop中心化的集群管理器。每个应用都有一个管理器(AM)用于向中央资源管理器(RM)协商请求资源。这种机制类似于2008年以前Borg为Google MapReduce分配资源的机制。
7.3 阿里巴巴Fuxi
Fuxi不是像Borg那样把task调度到合适的machine上,而是“反向调度”,为最近可用的machine匹配任务task。
7.4 Omega
Omega支持并发调度,其机制在前文3.4节已经介绍.
7.5 Kubernetes
建设Borg的许多工程师正在研发Kubernetes,并将Borg的经验应用到Kubernetes之中。
8 经验以及未来工作 Lessons and futurework
介绍了一些经验教训。最后提到Google将继续改进Borg,并将其中的经验应用到Kubernetes之中。
个人解读:Borg是我一直关注和学习的系统,本文介绍了Borg的许多内容,涉及集群管理的方方面面,可以说信息量很大。比如,并发调度在本文中仅用了半节来描述,但是本身却产出了Omega一篇完整的论文。
另外,Google似乎不喜欢对于其核心系统进行开源,比如当年的MapReduce、GFS、BigTable、Chubby等都不是Google自行开源,只是开源社区根据其发表论文自己造的“大轮子”。而且大家推测开源版本在实现上可能与Google版本存在不同程度的差距。目前来看,Google也完全没有开源Borg的苗头,但是Google开源了Kubernetes(同一拨人搞的,并汲取多年经验),所以我们在研读Borg论文之余,也要投身于Kubernetes的实践当中啊!