概述
Google的Borg系统是一个集群管理工具,在它上面运行着成千上万的job,这些job来自许许多多不同的应用,并且跨越多个集群,而每个集群又由大量的机器构成。
Borg通过组合准入控制,高效的任务打包,超额负载以及基于进程级别性能隔离的机器共享从而实现高利用率。它支持那些高可用的应用,它们的运行时特性能够最小化错误恢复时间,它们的调度策略降低了相关错误发生的可能性。为了简化用户的使用,Borg提供了一个声明工作规范语言,名称服务一体化机制,实时job监控以及一系列用于分析和模拟系统行为的工具。
我们接下来将呈现对于Borg的一些总结,包括系统架构和特性,重要的设计决策,对它的一些策略的量化分析以及在对它十多年的使用中得到的经验教训。
1、简介
这个我们内部叫做Borg的集群管理系统确认,调度,启动,重启以及监视Google运行的所有应用。这篇论文主要用来阐述Borg提供的三大优点:(1)它隐藏了资源管理以及错误处理,因此用户能集中精力开发应用;(2)具有非常高的可靠性和可用性,从而能够支持具有这些特性的应用;(3)能够让我们跨越数以千计的机器有效地运行负载。Borg并不是第一个解决这些问题的系统,但是它是少数能在这么大规模处理这些问题并且还能达到这样弹性和完整性的系统之一。本篇论文围绕着上述主题展开,并且以我们十多年的Borg使用经验得出的一些定性观察结果作为结尾。
2、用户视角
Borg的主要用户是Google的开发者以及运行Google应用和服务的系统管理员(网站可靠性工程师或者简称SRE)。用户以job的形式向Borg提交工作,每个job由运行一个或多个运行相同程序的task组成。每个job运行在一个Borg cell中,并且将一组机器当做一个单元进行管理。本节的剩余部分将主要描述从用户视角来看Borg的一些特性。
2.1、工作负载
Borg运行各种各样的负载,这些负载主要可以分为两类。第一类是长时间运行不能停止的服务并且要求能够处理短暂的,延迟敏感的请求(延迟要求在几微秒到几百毫秒之间)。这些服务主要用于面向终端用户的服务,例如Gmail,Google Docs,web搜索以及内部的一些基础设施服务(例如BigTable)。第二类是通常需要几秒到几天来完成的批处理job,这些job对短时间的性能波动并不是非常敏感。这些负载通常在cell之间混合分布,每个cell随着主要租户以及时间的不同会运行各种不同的应用:批处理类型的job来了又走而许多面向终端用户的job又期望一个能长时间使用的模式。Borg要求能很好地处理所有的情况。我们对一个公开的,具有代表性的Borg负载从2011年五月开始进行了为期一个月的追踪并且已经对它进行了广泛的分析。
在过去的几年里,已经有很多应用框架部署到了Borg上面,包括我们内部的MapReduce系统,FlumeJava,Millwheel和Pregel。它们中的大多数都有一个控制器用于提交一个master job以及一个或多个worker job;其中前两个和YARN中的应用管理器扮演的是相同的角色。我们的分布式存储系统例如GFS以及它的后继者CFS,Bigtable和Megastore都是运行在Borg上面的。
在本篇论文中,我们将高优先级的job称为prod,剩下的则称为non-prod。多数的长时间运行的服务job是prod类型的,而多数的批处理job是non-prod类型的。在一个具有代表性的cell中,prod类型的job分配了大概所有CPU资源的70%并且代表了60%的CPU使用;在内存方面,prod类型的job分配了大概所有内存资源的55%并且代表了大概85%的内存使用。至于分配和使用的区别,我们将在章节5.5再进行说明。
2.2、集群和cell
一个cell中的机器通常属于单个集群,并且由数据中心规模的高性能网络结构连接起来。一个集群通常存在于单个数据中心里,而多个数据中心的集合构成了一个site。一个集群通常包含一个大的cell,也许其中还有一些小规模用于测试或者其他特殊目的的cell。我们总是极力避免单点故障发生。
排除掉用于测试的cell之外,我们中型大小的cell通常包含10k台机器,当然还有更大的cell。从不同的维度:比如大小(CPU,RAM,硬盘,网络),处理器类型,性能以及外部IP和闪存来看,cell是多种多样的。但是Borg通常会通过决定在哪个cell运行task,为它们分配资源,为它们安装程序以及它们的一些依赖,监视它们的健康状况并且在它们崩溃的时候重启它们,从而对用户屏蔽了这些cell之间的差异。
2.3、job和task
Borg的一个job的属性通常包括它的名字,所有者以及它拥有的task的名字。job可以存在一定的约束,从而让它的task运行在有特定属性的机器上,例如特定的处理器架构,操作系统版本,以及外部IP。约束可以分为硬性的或软性的。软性的约束更像是一种优先建议而不是要求。一个job的运行可以推迟到上一个结束之后才开始并且一个job只能运行在一个cell中。
每个task代表了运行在一个容器或者一个物理机器内的一系列Linux进程。Borg的大部分负载并不会运行在虚拟机中,因为我们不想承担虚拟化带来的开销。并且我们在设计系统的时候就假设系统使用的处理器是没有硬件虚拟化支持的。
每个task同样拥有各自的属性,例如资源需求以及task在job里的索引。大多数task属性在同一个job里都是相同的,但是提供了针对具体task的命令行标志之后也能对它们进行重载。每一个资源的维度(例如CPU核数,RAM,磁盘空间,磁盘访问速率,TCP端口等等)都能以很好的粒度被独立地指定,我们并不会强加一个固定大小的bucket或者slot。Borg程序通常都是静态链接的从而能减少对它们运行环境的依赖,并且二进制文件和数据文件都以包的形式组织起来,而它们的安装都是由Borg策划的。
用户通常通过向Borg发送远程过程调用,即利用一些命令行工具,来操作job或者我们的监视系统。大多数的job描述都是用一种声明性的配置语言BCL写成的。BCL是GCL的一个变体,GCL会产生protobuf文件,而BCL会在它之上扩展一些Borg特有的关键词。BCL提供了lambda函数用于计算,应用程序通常利用它来调整对环境的配置。很多的BCL文件都超过了1k行,我们已经积累了成千上万行的BCL文件。Borg的job配置文件和Aurora的配置文件有很多类似之处。下图展示了job和task运行走完它们整个生命周期的过程。
在一个正在运行的job中,用户可以通过推送一个新的job配置文件给Borg并且命令Borg更新task到新的配置,从而改变一些或者全部task的属性。这种轻量级的,非原子化的动作在它关闭前极有可能是不被操作的。更新通常以滚动的方式进行,我们一般会对更新引起的中断(重新调度或抢占)数量进行限制,那些引起中断的数量超过限制的更新会被直接跳过。
一些任务更新(例如推送一个新的二进制文件)总是需要重启task,另一些操作(例如增加资源请求或者改变约束条件)可能让task不再适应当前的机器,因此需要将它停止并且重新调度,而其他的操作(例如改变优先级)则不需要任何重启或者移动task的操作就能完成。
在被SIGKILL抢占之前,task总能通过SIGTERM信号被通知,因此它们有足够的时间去清理,保存状态,结束当前正在执行的请求并且拒绝新的请求。如果抢占器设置了延迟边界的话,实际的信号可能更少一点。事实上,一个通知只有80%的情况会被成功推送。
2.4、allocs
Borg的alloc(allocation的简写)操作是指在一台机器上预留一些资源,从而能够在其上运行一个或者多个资源;这些资源不管是否被使用都是保持被分配状态的。Allocs操作可以被用来保留资源用于未来task的使用,也可以用于在停止以及启动一个task之间保存资源,还可以用于将不同job里的task收集起来,让它们运行在同一台机器中:比如一个web服务器实例以及相关的用于将本地磁盘的服务器URL记录拷贝到分布式文件系统的task。被alloc操作之后的资源和机器中的其他资源是被同等对待的,运行在同一个alloc操作之上的多个task共享其中的资源。如果一个alloc操作必须被重定向到另一台机器上,那么之上的任务就必须随着alloc操作被重新调度。
一个alloc集就像是一个job:它是一系列的alloc操作用于在多台机器上预留资源。一旦一个alloc操作被创建,一个或多个job就能被提交并且运行在它之上。简单起见,我们一般用“task”来代之一次alloc操作或者顶层的task(一个在alloc操作之外的task),而“job”指一个job或者一个alloc操作集。
2.5、优先级,配额以及准入控制
如果出现了超出处理能力的负载怎么办?我们的解决方法是优先级和配额。
每个job都有一个优先级(priority),也就是一个小的正整数。一个高优先级的task可以以牺牲另一个较低优先级的task为代价来获取资源,即使这种牺牲包括抢占或者杀死较低优先级的task。Borg对于不同的使用定义了一种非重叠的优先级带,包括(优先级从高到底排列):监视,生产,批任务以及尽力而为的工作(也可以理解为测试或者免费的工作)。在本篇论文中,prod类型的job都是处于监视或者产品的优先级带中。
通常一个被抢占的task会被重新调度到cell的其他地方,而抢占操作也有可能产生级联影响:比如一个高优先级的task抢占了一个较低优先级的task,而后者又去抢占更低优先级的task,从而不断级联下去。为了防止这种级联事件的发生,我们并不允许同处于生产优先级带的task相互抢占。细粒度的优先级划分在其他一些情况下还是相当有用的:比如MapReduce的master类型的task的优先级要高于它控制的worker,从而提高整个系统的可靠性。
优先级用来表示在一个cell中运行或者等待运行的job的相对重要性。而配额(quota)则表示哪些job能够被调度。配额我们可以理解为在给定优先级下的资源请求向量(CPU,RAM,磁盘等等)。资源请求是指在一段时间内,一般是一个月内,一个用户的job能请求的最大资源数目(比如一个prod请求了20TB的RAM,时间是从现在到七月份,在XX cell中)。配额检查也是准入控制的一部分,而不是调度:一个配额要求未被满足的job是会被立刻拒绝的。
高优先级job的配额通常会比低优先级job的配额花费更多。比如生产优先级的配额会被限制在一个cell真实可获取的资源数量范围内。因此,如果一个用户提交了一个生产优先级的job,并且配额合适,那么就能期待它运行。尽管我们建议用户不要购买多于他们需求的配额,但是许多用户仍然会选择买过量的配额,因为这能保证将来它们应用的用户增长之后不会出现资源的短缺。对于这一点,我们的回应是,处于低优先级的job能拥有更多的配额:每一个处于优先级0的用户拥有无限的资源配额,尽管这很难被真正实施,因为资源被超额认购了。一个低优先级的job可能可以被准入但是也许会一直被挂起,因为请求的资源一直无法得到满足。
配额的分配是在Borg之外进行的并且和我们的物理容量规划密切相关。它们通常反映了不同数据中心配额的价格和可用性。一个用户的job只有在满足了它所在优先级的配额之后才能被准入。配额的使用减少了对像优势资源公平(Dominant Resource Fairness,DRF)这样的策略的使用。
Borg还有一个容量系统,它能给予一些用户以特殊的权限:比如允许管理员删除或修改cell里任意的job,或者运行用户访问限制的内核特性或者Borg的行为,例如在他们的job中禁用资源限制。
2.6、命名以及监控
单单创造并且部署task还是远远不够的,因为一个服务的客户端以及其他系统需要能够找到它们,即使在它们被调度到新的机器上以后。因此,Borg为每个task创造了一个叫”Borg name service“(BNS)类型的名字,这个名字中包含了cell的名字,job的名字以及task的编号。Borg会将task的主机名,端口号以及这个BNS名字写入Chubby里面一个一致的,高可用的文件中,而这个文件通常被我们的RPC系统用于查找task。BNS名字同样被用作task的DNS名字基础,因此对于用户ubar拥有的一个在叫做cc的cell中的一个叫jfoo的job中的第五十个task,我们就可以通过域名50.jfoo.ubar.cc.borg.google.com访问到。同时Borg会在job的大小或者task的健康状况改变时将它们写入Chubby中,之后负载均衡器就能决定将请求路由到什么地方了。
几乎Borg之下运行的每一个task都有一个内置的HTTP server用于发布task的健康状况以及其他许多的性能指标(RPC延迟等等)。Borg会监视健康检查的URL并且会重启那些没有即使回复的task或者直接返回一个HTTP 错误代码。其他数据通过另外一些监控工具进行监控,并且对服务对象级别的违规行为进行报警。
一个叫做Sigma的服务提供了基于web的用户界面,通过它,用户能测试他们所有job或者一个特定cell的健康状况。还可以深入到具体的job或者task中去测试它们的资源相关的行为,详细的日志,执行历史以及它们最终的命运。我们的应用会产生大量的日志:它们会自动地进行轮转从而避免耗尽磁盘的空间,并且会在task退出之后仍然保留一定的时间用于调试。如果一个job不再运行了,Borg会提供一个“为什么被挂起”的注解,而且会附上如何修改job的资源请求以更好地适应cell的指导。我们已经发布了一个符合要求的资源请求指导,从而能够让调度进行地更为顺利。
Borg会记录所有的job提交情况,task事件,以及Infrastore中详细的task执行前的资源使用情况。Infrastore是一个有着类SQL接口的可扩展只读数据存储。这些数据被用作基于使用的计费,调试,系统错误以及长期的容量计划。同时,它们也为Google的集群负载追踪提供了数据。
所有上述的特性能够帮助用户更好地理解,调试Borg和它里面的job的行为,同时也能帮助我们的SRE每个人都能管理许许多多的机器中的一部分。
注:翻译中部分内容可能比较晦涩或者并非十分流畅,欢迎指正
原文地址:http://static.googleusercontent.com/media/research.google.com/zh-CN//pubs/archive/43438.pdf