你好,我是徐文浩。
从GFS这样的分布式文件系统,到MapReduce这样的数据批处理系统;从Bigtable这样的分布式KV数据库,到Spanner这样全球部署的强一致性关系数据库;从Storm这样只能做到“至少一次”的流式系统,到Dataflow这样真正做到“流批一体”的统一数据处理系统。在过去的30多讲里,我和你一起看过了各式各样的大数据系统。
在研究这些大数据系统的时候,我们其实有一个假设。这个假设,就是其中的每一个系统,都需要占用一组独立的服务器。而在一个完整的大数据体系中,我们既需要有GFS这样的文件系统,也需要MapReduce/Spark这样的批处理系统,还需要Bigtable这样的KV数据库、Hive这样的数据仓库、Kafka这样的消息队列,以及Flink这样的流式系统。这样一算,我们需要的服务器可真不算少。
但是,当我们采购了很多服务器,搭建起了一系列的大数据系统,我们又会发现这些服务器在很多时候负载不高,显得非常浪费。因为我们在采购服务器的时候,需要根据平时的峰值流量来确定服务器数量。比如,像Kafka这样的消息队列,肯定是在早晚高峰,和中午用户比较多的时候,流量比较大,到了半夜流量就比较小。如果我们高峰时间的CPU占用要有60%,那么在低谷时刻,可能只有10%。
如果就只是采购服务器的硬件成本,那还好,毕竟服务器我们已经采购完了。但是,对于一个数据中心来说,硬件成本只是一小部分,最大的一头在电力成本。根据艾瑞发表的「2020年中国数据中心行业研究报告」,数据中心的运营成本,有超过50%是电力成本。所以,能不能尽量少用一点服务器,就变成一个很有价值的问题了。
一个很自然的想法,就是对我们服务器的使用进行“削峰填谷”,我们让原本在高峰时间运行的一些任务,挪到半夜这样的低谷时段去。这个思路,对于离线进行数据分析的任务,当然很容易做到,所以一般来说,我们的Hadoop或者Spark集群的CPU整体利用率往往很高。
但是对于Kafka、Dataflow这样提供近实时服务的数据系统,我们是没办法把峰值时段的任务,也挪到第二天半夜才计算的。所以,一个新的想法自然也就冒出来了,那就是,我们是不是可以把MapReduce这样的离线分析任务,也放到Dataflow的集群上运行呢?在半夜Dataflow没有什么流量的时候,我们完全可以把这部分服务器的资源用起来。
所以,我们需要有一个办法,能够让我们的各种大数据系统混合编排在同一批服务器上。
事实上,这个混合编排并不局限于大数据系统,我们的业务系统也可以一并考虑进来,毕竟半夜里,我们业务系统的访问量肯定也很小,对应的服务器资源一样是闲置着的。
正是这么一个朴素的、尽量利用服务器资源的思路,就催生了Google的Borg系统,并最终从中进化出了Kubernetes这个开源的容器编排系统。如今,几乎所有的公司都在把系统往容器化的方向迁移,Kubernetes也成为了云原生系统的标准。那么今天,我就带你一起来看看,这篇Kubernetes的老祖宗的论文《Large-scale cluster management at Google with Borg》是怎么一回事儿。
通过这节课的学习,希望你能先对Borg系统的基础概念、系统架构有所认识和了解。之后,我会再花一节课时间,来具体为你讲解Borg是如何进行资源的调度分配,以及评估整个系统的性能的。
好了,废话少说,让我们接着往下学习吧。
把不同类型的程序,部署在同一台服务器上,我们面临的第一个问题,就是这两个程序会竞争资源。比如,我们在同一台服务器上,既部署了Kafka的Broker进程,也部署了Bigtable的Tabet Server进程。那么,当我们在流量高峰的时候,Kafka传输的流量占满了整个网络带宽之后,我们的Tablet Server就无法对外提供服务了,所有的请求都会因为超时而失败。
要解决这个问题,本质上,我们需要把服务器的资源拆开来,然后把对应的一组应用程序隔离出来,只允许它们去使用服务器的一部分资源。而这个解决方案,就是Linux下的CGroup功能。CGroups的全称叫做Linux Control Group,它可以限制一组Linux进程使用的资源。具体来说,它主要能做到这样几点:
这么一看你就清楚了,有了CGroups,我们就可以把Kafka相关的进程分成一组,然后交给CGroups去限制它占用的内存、CPU、硬盘I/O和网络I/O,然后Tablet Server分成另一组,也限制了对应使用的资源。这样,我们就可以在一个服务器上,同时运行两个不同系统的程序,而且不用相互干扰了。
对于CGroups的具体功能,你可以去这里看看简单的一些运行示例,有个直观的体会。事实上,Borg里部署的一个个的任务,其实就是基于CGroups,封装好的LXC的容器。如果你有兴趣的话,也可以去仔细研究一下LXC和CGroups相关的知识。
我们的各种系统的进程都被封装进了一个个的LXC容器,这样我们就可以通过一个集群管理系统,把不同的LXC容器分配到不同的服务器上运行,这个集群管理系统,就是我们的Borg。
一个Borg的集群被称之为一个Cell,通常被部署在一个数据中心里。而在Google,一个中等规模的Cell通常有1万台机器。
一个集群有这么多服务器,意味着Borg的“混合部署”,并不是简单地把两三个应用放在一起,让它们能够错开各自的性能峰值,而是把一个数据中心里,成千上万个应用混合部署在一起。而且,这么多的服务器,往往是分多个批次采购的,也就意味着服务器的配置,比如CPU、内存、硬盘等等可能是各不相同的。
而Borg,需要对于各个应用的开发人员,做到隐藏所有的资源管理细节,让应用不需要关心它们具体会被部署在什么样的硬件配置上。而且,即使当前部署的硬件故障了,Borg也需要能够立刻快速恢复,把对应的容器另外寻找一台服务器,部署上去。
当然,在Borg里,部署的并不是一个裸的CGroup,而是一个个的Tasks。用户会向Borg提交一个个的Job。这些Job里,既有像长期驻留的服务,也有一次性运行批处理任务。一个Job其实就是一个二进制的程序,这个Job程序会被部署到一台或者多台服务器上。每个服务器上实际运行的进程就叫做Tasks。
这个其实我们在MapReduce的论文讲解里就看到过了。提交的MapReduce的Job,就是先提交给了Borg,然后Job对应的Map和Reduce的Tasks,其实就是由Borg分配到不同的服务器上,实际执行的。
那么,在这1万台机器的集群里的Tasks,有的是提供在线服务的,对于服务的响应延时有要求;有的只是一个离线任务,只要能够执行完成就好。而我们为了尽可能充分利用硬件资源,就会遇到某一个时间点,很多Tasks都想要使用资源但是资源不够用了的情况。
针对这个问题,Borg采用了配额和优先级的机制。
和我们操作系统里的进程类似,Borg里的Job都有一个优先级,从高到低分别是监控(Monitoring),生产(Production),批处理(Batch)和尽最大努力(Best Effort)这四种,同一种类的优先级下,还能根据一个整数的priority参数,来区分不同任务的优先级大小。Job提交之后部署到具体的服务器上,就会变成一个个Task,这些个Task也就继承了Job上的优先级的属性。
生产类型的Tasks,可以去抢占批处理Tasks的资源,但是生产类型的Tasks互相抢占资源是不允许的,因为大家都要提供在线服务。不过,这个时候问题来了,像MapReduce这样的Job,自然应该归在批处理Job里,对应的Task按理也都是随时会被抢占的批处理Task。
但是,我们同样会有很多MapReduce的Job,需要在规定的时间范围内完成。比如,我们可能每天半夜里都会利用MapReduce,来进行过去一天的报表计算,我们需要这些任务在早上8点大家都来上班之前完成。但是,如果批处理的Task,随时会被生产类型的Task抢占,我们就有可能无法保障这一点。
为了解决这一类的问题,Borg有一种叫做alloc的机制。alloc是一组可以预留的资源,不管这个服务器资源是否被用到了,这个资源我们始终会分配给alloc。这样,对于MapReduce类型的任务,我们可以始终通过alloc,在我们的Borg集群里预留一部分资源,这些资源是其他生产类型的Job抢不走的。
在Borg里,你可以向一个alloc集合来提交你的Job,这个Job对应拆分出来的Task,就会使用alloc预留的资源运行,这些资源是不会被其他的生产Task抢占走的。
好了,我们之前已经看过了太多的分布式系统,相信你想一想也能差不离地猜出Borg的整体架构是怎么样的了。
Borg是一个典型的Master-Slave类型的系统。一个Borg Cell通常由这样几部分组成。
首先是用户界面,你可以通过配置文件、命令行以及浏览器内的Web界面,向Borg发起请求,所有这些请求都会统一发送给Borg的Master。和我们之前看过的MapReduce和Hive一样,Borg的UI界面里,同样有一个对于所有Job和Task的监控系统。每一个直接部署执行的Task,都会内置一个HTTP服务器,你通过浏览器,就能拿到Job和Task的各种监控指标和运行日志,这个系统在Borg里叫做Sigma。
然后就是一个Master集群,为了保障高可用性,Master的所有数据当然是通过Paxos协议来维护多个同步复制的副本的。它一样,也是通过Checkpoint来建立快照,通过日志记录所有的操作,整个Master的集群里,也有一个选举出来的master中的master。而这个master中的master,则是通过获取一个Chubby里的锁来确保唯一性。
这一系列的设计是一个典型的Master-Slave分布式系统的设计,我们之前在GFS/Bigtable这样的系统里已经反复看到过了。Master集群只负责管理所有的元数据,以及和外部发来的RPC请求。这里的元数据,包括集群里所有对象的状态,也就是集群本身的资源,也就是有哪些Borglet服务器,有哪些Job、Task以及Allocs。然后,它也要和Borglets通信,确保自己了解整个集群的状态。
不过,实际Task分配给哪个服务器执行,却不是由Master来决定的。Borg把这部分职责单独从Master里剥离出来,给了一个叫做Scheduler的服务器。当一个Job提交给Master之后,Master会把它变成一个个有待调度的Tasks,然后加入到一个队列里。而我们的Scheduler则会异步遍历这些Tasks,当它判断我们的集群有足够的资源,可以满足整个Job的需求的时候,它就会把Task分配到Slave服务器里。
Slave服务器,就是我们实际负责去运行Task的服务器。每个Slave服务器上,都会有一个叫做Borglet的进程。Master并不会直接和每个Slave服务器上的Task进程通信,而只是和Borglet进程通信。
Borglet进程会负责启动和停止Task,如果Task失败了就重启。因为Borg的单个集群,要有1万台服务器,所以,并不是由Master一台台机器来主动拉Slave里的信息的。而是由Borglet上报对应的信息,Master只会定期轮询Borglet,获取这些Slave服务器的状态。
而因为有1万台之多的服务器,Master集群里虽然选出了一个master(本质上是leader),Borg还是会把所有的Slave机器分片,然后让每一个master的副本,都负责一部分Borglet的通信。然后,这个副本会把Borglet上报的最新信息,和Master已经知道信息的差(Diff),交给Master里的master,以减少这一台特定机器的负载。
可以看到,从整个集群的架构来说,Borg并没有什么特殊之处。Master-Slave的架构我们在MapReduce/Bigtable里都已经见过。对外提供命令行、Web的一系列的UI,我们在Hive里面也见过。每个Slave服务器里,通过一个Borglet进程来管理,我们在Megastore的Coordinator里还是见过。
其实,分布式系统的架构设计都是大同小异的。不过,对于Borg来说,最有挑战的一点,还是在于如何去分配和调度一个个的Task,以及如何衡量一个混合部署了成百上千个Job的系统的性能。而这些问题,我们会在下节课里进行解答。
其实Borg系统整体并不难理解。为了尽可能地利用服务器资源,我们需要能够让不同性质系统的进程部署到一整个大集群里,通过一个集群管理系统来统一管理。而为了做到这一点,我们先要能够对同一台服务器上的一组进程打包起来,进行资源的限制和权限的隔离,这个功能是来自于Linux内核的CGroup。
在有了CGroup之后,Borg系统的整体架构并不复杂,是一个典型的Master-Slave系统。它通过Paxos实现Master的高可用性和一致性,通过Borglet来管理每个服务器上的Tasks,而具体的Tasks,则不需要依赖Borg本身的Master和Borglet来执行。这一系列的设计,和我们之前看过的那么多的分布式系统,并没有本质上的区别。
在任务的调度上,Borg是把任务按照优先级,从高到低分成了监控任务、生产任务、批处理任务,以及尽最大努力完成的任务。生产级的任务可以抢占非生产级的任务,而Borg也添加了预留资源的alloc机制,使得MapReduce这样都是批处理的任务,也都能长期保留一些资源,确保任务能够按时完成。
我们可以看到,在整体架构上,Borg并没有什么出人意料的思路。不过在实践的工程上,能够管理1万台集群的服务器,的确让人叹为观止。可惜Google并没有把Borg这样的系统开源出来,我们也就没有办法深入到代码层面去一探究竟了。
不过,好在在论文里,Google还是对他们如何管理调度集群里的任何资源,以及如何评估混合部署,为我们的系统带来的性能影响进行了说明。而这些,也是我们下节课的主题。
Borg能够有效运转的前提,是通过Linux下的CGroups,对我们运行的程序进行资源层面的限制。你可以去看看美团技术团队的博客中,关于CGroups的介绍,也可以看看酷壳上的这篇博客,可以手把手体验一下CGroups的这些功能。
Borg的一个中等规模的Cell就会有1万台服务器。你能想一下,比起100台服务器的集群管理系统,1万台服务器会给我们带来哪些额外的挑战吗?
欢迎在留言区分享你的思路,也欢迎把今天的内容分享给更多的朋友。
评论