Docker背后的容器集群管理——从Borg到Kubernetes(一)

2015年4月,传闻许久的Borg论文总算出现在了Google Research的页面上。虽然传言Borg作为G家的“老”项目一直是槽点满满,而且本身的知名度和影响力也应该比不上当年的“三大论文”,但是同很多好奇的小伙伴一样,笔者还是饶有兴趣地把这篇“非典型”论文拜读了一番。

注:本文作者张磊将在8月28日~29日的CNUT全球容器技术峰会上分享题为《从0到1:Kubernetes实战》的演讲,演讲中他将重点剖析Kubernetes的核心原理和实践经验,并分享大规模容器集群管理所面临的问题和解决思路。

1. Borg在讲什么故事

其实,如果这篇论文发表在两三年前,Borg的关注度恐怕真没有今天这么高。作为一篇本质上是关于数据中心利用率的半工程、半研究性的成果,这个领域的关注人群来自大厂的运维部以及系统部的技术同僚可能要占很大的比例。而对于绝大多数研究人员,普通开发者,甚至包括平台开发者而言,Borg论文本身的吸引力应该说都是比较有限的。

不过,一旦我们把Borg放到当前这个时间点上来重新审视,这篇本该平淡的论文就拥有了众多深层意义。当然,这一切绝非偶然,从2013年末以Docker为代表的容器技术的迅速兴起,2014年Google容器管理平台Kubernetes和Container Engine的强势扩张,再到如今由Mesos一手打造的DCOS(数据中心操作系统)概念的炙手可热。容器技术令人咋舌的进化速度很快就将一个曾经并不需要被大多数开发人员关注的问题摆到了台面:

我们应该如何高效地抽象和管理一个颇具规模的服务集群?

这,正是Borg全力阐述的核心问题。

说得更确切一点,当我们逐步接纳了以容器为单位部署和运行应用之后,运维人员终于可以从无休止的包管理,莫名其妙的环境差异,繁杂重复的批处理和任务作业的中稍微回过一点神来,开始重新审视自己手中的物理资源的组织和调度方式:即我们能不能将容器看作传统操作系统的进程,把所有的服务器集群抽象成为统一的CPU、内存、磁盘和网络资源,然后按需分配给任务使用呢?

所以,作为《Docker背后的技术解析》系列文章的特别篇,笔者将和读者一起从Borg出发,结合它的同源项目Kubernetes中尝试探索一下这个问题的答案。

2. Borg的核心概念

同大多数PaaS、云平台类项目宣称的口号一样,Borg最基本的出发点还是“希望能让开发者最大可能地把精力集中在业务开发上”,而不需要关心这些代码制品的部署细节。不过,另一方面,Borg非常强调如何对一个大规模的服务器集群做出更合理的抽象,使得开发者可以像对待一台PC一样方便地管理自己的所有任务。这与Mesos现在主推的观点是一致的,同时也是Borg同PaaS类项目比如Flynn、Deis、Cloud Foundry等区别开来的一个主要特征:即Borg,以及Kubernetes和Mesos等,都不是一个面向应用的产物

什么叫面向应用?
就是以应用为中心。系统原生为用户提交的制品提供一系列的上传、构建、打包、运行、绑定访问域名等接管运维过程的功能。这类系统一般会区分”应用“和”服务“,并且以平台自己定义的方式为”应用“(比如Java程序)提供具体的”服务“(比如MySQL服务)。面向应用是PaaS的一个很重要的特点。

另一方面,Borg强调的是规模二字。文章通篇多次强调了Google内部跑在Borg上的作业数量、以及被Borg托管的机器数量之庞大。比如我们传统认知上的“生产级别集群”在文章中基本上属于Tiny的范畴,而Borg随便一个Medium的计算单元拿出来都是一家中大型企业数据中心的规模(10K个机器)。这也应证了淘宝毕玄老大曾经说过的:“规模绝对是推动技术发展的最关键因素”。

Borg里服务器的划分如下: Site = 一组数据中心(Cluster), Cluster = 一组计算单元(Cell), Cell = 一组机器。 其中计算单元(Cell)是最常用的集群类别。

2.1 Job,Task

既然Borg不关心“应用”和“服务”的区别,也不以应用为中心,那么它需要接管和运行的作业是什么?

Job

Borg文章里对Job的定义很简单,就是多个任务(Task)的集合,而所谓Task就是跑在Linux容器里的应用进程了。这样看起来Job是不是就等同于Kubernetes里的Pod(容器组)呢?

其实不然。Job映射到Kubernetes中的话,其实等同于用户提交的“应用”,至于这个应用运行了几个副本Pod,每个Pod里又运行着哪些容器,用户并不需要关心。用户只知道,我们访问这个服务,应该返回某个结果,就够了。

举个例子,因为高可用等原因,用户常常会在Kubernetes里创建并启动若干个一模一样的Pod(这个功能是通过Kubernetes的Replication Controller实现的)。这些一模一样的Pod“副本”的各项配置和容器内容等都完全相同,他们抽象成一个逻辑上的概念就是Job。

由于Job是一个逻辑上的概念,Borg实际上负责管理和调度的实体就是Task。用户的submit、kill、update操作能够触发Task状态机从Pending到Running再到Dead的的转移,这一点论文里有详细的图解。值得一提的是,作者还强调了Task是通过先SIGTERM,一定时间后后再SIGKILL的方式来被杀死的,所以Task在被杀死前有一定时间来进行“清理,保存状态,结束正在处理的请求并且拒绝新的请求”的工作。

2.2 Alloc

Borg中,真正与Pod对应的概念是Alloc。

Alloc的主要功能,就是在一台机器上“划”一块资源出来,然后一组Task就可以运行在这部分资源上。这样,“超亲密”关系的Task就可以被分在同一个Alloc里,比如一个“Tomcat应用”和它的“logstash服务”。

Kubernetes中Pod的设计与Alloc如出一辙:属于同一个Pod的Docker容器共享Network Namepace和volume,这些容器使用localhost来进行通信,可以共享文件,任何时候都会被当作一个整体来进行调度。

所以,Alloc和Pod的设计其实都是在遵循“一个容器一个进程”的模型。经常有人问,我该如何在Docker容器里跑多个进程?其实,这种需求最好是通过类似Pod这种方法来解决:每个进程都跑在一个单独的容器里,然后这些容器又同属于一个Pod,共享网络和指定的volume。这样既能满足这些进程之间的紧密协作(比如通过localhost互相访问,直接进行文件交换),又能保证每个进程不会挤占其他进程的资源,它们还能作为一个整体进行管理和调度。如果没有Kubernetes的话,Pod可以使用“Docker in Docker”的办法来模拟,即使用一个Docker容器作为Pod,真正需要运行的进程作为Docker容器嵌套运行在这个Pod容器中,这样它们之间互不干涉,又能作为整体进调度。

另外,Kubernetes实际上没有Job这个说法,而是直接以Pod和Task来抽象用户的任务,然后使用相同的Label来标记同质的Pod副本。这很大程度是因为在Borg中Job Task Alloc的做法里,会出现“交叉”的情况,比如属于不同Job的Task可能会因为“超亲密”关系被划分到同一个Alloc中,尽管此时Job只是个逻辑概念,这还是会给系统的管理带来很多不方便。

2.3 Job的分类

Borg中的Job按照其运行特性划分为两类:LRS(Long Running Service)和batch jobs。

上述两种划分在传统的PaaS中也很常见。LRS类服务就像一个“死循环”,比如一个Web服务。它往往需要服务于用户或者其它组件,故对延时敏感。当然论文里Google举的LRS例子就要高大上不少,比如Gmail、Google Docs。

而batch jobs类任务最典型的就是Map-Reduce的job,或者其它类似的计算任务。它们的执行往往需要持续一段时间,但是最终都会停止,用户需要搜集并汇总这些job计算得到的结果或者是job出错的原因。所以Borg在Google内部起到了YARN和Mesos的角色,很多项目通过在Borg之上构建framework来提交并执行任务。Borg里面还指出,batch job对服务器瞬时的性能波动是不敏感的,因为它不会像LRS一样需要立刻响应用户的请求,这一点可以理解。

比较有意思的是,Borg中大多数LRS都会被赋予高优先级并划分为生产环境级别的任务(prod),而batch job则会被赋予低优先级(non-prod)。在实际环境中,prod任务会被分配和占用大部分的CPU和内存资源。正是由于有了这样的划分,Borg的“资源抢占”模型才得以实现,即prod任务可以占用non-prod任务的资源,这一点我们后面会专门说明。

对比Kubernetes,我们可以发现在LRS上定义上是与Borg类似的,但是目前Kubernetes却不能支持batch job:因为对应的Job Controller还没有实现。这意味着当前Kubernetes上一个容器中的任务执行完成退出后,会被Replication Controller无条件重启。Kubernetes尚不能按照用户的需求去搜集和汇总这些任务执行的结果。

2.4 优先级和配额

前面已经提到了Borg任务优先级的存在,这里详细介绍一下优先级的划分。

Borg中把优先级分类为监控级、生产级、批任务级、尽力级(也叫测试级)。其中监控级和生产级的任务就是前面所说的prod任务。为了避免在抢占资源的过程中出现级联的情况触发连锁反应(A抢占B,B抢占C,C再抢占D),Borg规定prod任务不能互相抢占

如果说优先级决定了当前集群里的任务的重要性,配额则决定了任务是否被允许运行在这个集群上。

尽管我们都知道,对于容器来说,CGroup中的配额只是一个限制而并非真正割据的资源量,但是我们必须为集群设定一个标准来保证提交来任务不会向集群索要过分多的资源。Borg中配额的描述方法是:该用户的任务在一段时间内在某一个计算单元上允许请求的最大资源量。需要再次重申,配额一定是任务提交时就需要验证的,它是任务合法性的一部分。

既然是配额,就存在超卖的情况。在Borg中,允许被超卖的是non-prod的任务,即它们在某个计算单元上请求的资源可能超出了允许的额度,但是在允许超卖的情况下它们仍然有可能被系统接受(虽然很可能由于资源不足而暂时进入Pending状态)。而优先级最高的任务则被Borg认为是享有无限配额的。

与Kubernetes类似的是,Borg的配额也是管理员静态分配的。Kubernetes通过用户空间(namespace)来实现了一个简单的多租户模型,然后为每一个用户空间指定一定的配额,比如:

apiVersion: v1beta3
kind: ResourceQuota
metadata:
  name: quota
spec:
  hard:
    cpu: "20"
    memory: 10Gi
    pods: "10"
    replicationcontrollers: "20"
    resourcequotas: "1"
    services: "5"

到这里,我们有必要多说一句。像Borg、Kubernetes以及Mesos这类项目,它们把系统中所有需要对象都抽象成了一种“资源”保存在各自的分布式键值存储中,而管理员则使用如上所示的“资源描述文件”来进行这些对象的创建和更新。这样,整个系统的运行都是围绕着“资源”的增删改查来完成的,各组件的主循环遵循着“检查对象”、“对象变化”、“触发事件”、“处理事件”这样的周期来完成用户的请求。这样的系统有着一个明显的特点就是它们一般都没有引入一个消息系统来进行事件流的协作,而是使用“ectd”或者“Zookeeper”作为事件系统的核心部分。

2.5 名字服务和监控

与Mesos等不同,Borg中使用的是自家的一致性存储项目Chubby来作为分布式协调组件。这其中存储的一个重要内容就是为每一个Task保存了一个DNS名字,这样当Task的信息发生变化时,变更能够通过Chubby及时更新到Task的负载均衡器。这同Kubernetes通过Watch监视etcd中Pod的信息变化来更新服务代理的原理是一样的,但是由于使用了名为“Service”的服务代理机制(Service可以理解为能够自动更新的负载均衡组件),Kubernetes中默认并没有内置名字服务来进行容器间通信(但是提供了插件式的DNS服务供管理员选用)。

在监控方面,Borg中的所有任务都设置了一个健康检查URL,一旦Borg定期访问某个Task的URL时发现返回不符合预期,这个Task就会被重启。这个过程同Kubernetes在Pod中设置health_check是一样的,比如下面这个例子:

apiVersion: v1beta3
kind: Pod
metadata:
  name: pod-with-healthcheck
spec:
  containers:
    - name: nginx
      image: nginx
      # defines the health checking
      livenessProbe:
        # an http probe
        httpGet:
          path: /_status/healthz
          port: 80
        # length of time to wait for a pod to initialize
        # after pod startup, before applying health checking
        initialDelaySeconds: 30
        timeoutSeconds: 1
      ports:
        - containerPort: 80

这种做法的一个小缺点是Task中服务的开发者需要自己定义好这些/healthzURL和对应的响应逻辑。当然,另一种做法是可以在容器里内置一些“探针”来完成很多健康检查工作而做到对用户的开发过程透明。

除了健康检查,Borg对日志的处理也很值得借鉴。Borg中Task的日志会在Task退出后保留一段时间,方便用户进行调试。相比之下目前大多数PaaS或者类似项目的容器退出后日志都会立即被删除(除非用户专门做了日志存储服务)。

最后,Borg轻描淡写地带过了保存event做审计的功能。这其实与Kubernetes的event功能也很类似,比如Kube的一条event的格式类似于:

发生时间 结束时间 重复次数 资源名称 资源类型 子事件 发起原因 发起者 事件日志 

3. Borg的架构与设计

Borg的架构与Kubernetes的相似度很高,在每一个Cell(工作单元)里,运行着少量Master节点和大量Worker节点。其中,Borgmaster负责响应用户请求以及所有资源对象的调度管理;而每个工作节点上运行着一个称为Borglet的Agent,用来处理来自Master的指令。这样的设计与Kubernetes是一致的,Kubernetes这两种节点上的工作进程分别是:

Master:
apiserver, controller-manager, scheduler
Minion:
kube-proxy, kubelet

虽然我们不清楚Borg运行着的工作进程有哪些,但单从功能描述里面我们不难推测到至少在Master节点上两者的工作进程应该是类似的。不过,如果深入到论文中的细节的话,我们会发现Borg在Master节点上的工作要比Kubernetes完善很多。

3.1 Borgmaster

首先,Borgmaster由一个独立的scheduler和主Borgmaster进程组成。其中,主进程负责响应来自客户端的RPC请求,并且将这些请求分为“变更类”和“只读”类。

在这一点上Kubernetes的apiserver处理方法类似,kuber的API服务被分为“读写”(GET,POST,PUT,DELETE)和“只读”(GET)两种,分别由6443和7080两个不同的端口负责响应,并且要求“读写”端口6443只能以HTTPS方式进行访问。同样,Kubernetes的scheduler也是一个单独的进程。

但是,相比Kubernetes的单点Master,Borgmaster是一个由五个副本组成的集群。每一个副本都在内存中都保存了整个Cell的工作状态,并且使用基于Paxos的Chubby项目来保存这些信息和保证信息的一致性。Borgmaster中的Leader是也是集群创建的时候由Paxos选举出来的,一旦这个Leader失败,Chubby将开始新一轮的选举。论文中指出,这个重选举到恢复正常的过程一般耗时10s,但是在比较大的Cell里的集群会由于数据量庞大而延长到一分钟。

更有意思的是,Borgmaster还将某一时刻的状态通过定时做快照的方式保存成了checkpoint文件,以便管理员回滚Borgmaster的状态,从而进行调试或者其他的分析工作。基于上述机制,Borg还设计了一个称为Fauxmaster的组件来加载checkpoint文件,从而直接进入某时刻Borgmaster的历史状态。再加上Fauxmaster本身为kubelet的接口实现了“桩”,所以管理员就可以向这个Fauxmaster发送请求来模拟该历史状态数据下Borgmaster的工作情况,重现当时线上的系统状况。这个对于系统调试来说真的是非常有用。此外,上述Fauxmaster还可以用来做容量规划,测试Borg系统本身的变更等等。这个Fauxmaster也是论文中第一处另我们眼前一亮的地方。

上述些特性使得Borg在Master节点的企业级特性上明显比Kubernetes要成熟得多。当然,值得期待的是Kube的高可用版本的Master也已经进入了最后阶段,应该很快就能发布了。

3.2 Borg的调度机制

用户给Borg新提交的任务会被保存在基于Paxos的一致性存储中并加入到等待队列。Borg的scheduler会异步地扫描这个队列中的任务,并检查当前正在被扫描的这个任务是否可以运行在某台机器上。上述扫描的顺序按照任务优先级从高到低来Round-Robin,这样能够保证高优先级任务的可满足性,避免“线头阻塞”的发生(某个任务一直不能完成调度导致它后面的所有任务都必须进行等待)。每扫描到一个任务,Borg即使用调度算法来考察当前Cell中的所有机器,最终选择一个合适的节点来运行这个任务。

此算法分两阶段:

第一,可行性检查。这个检查每个机器是所有符合任务资源需求和其它约束(比如指定的磁盘类型),所以得到的结果一般是个机器列表。需要注意的是在可行性检查中,一台机器“资源是否够用”会考虑到抢占的情况,这一点我们后面会详细介绍。

第二,打分。这个过程从上述可行的机器列表中通过打分选择出分数最高的一个。

这里重点看打分过程。Borg设计的打分标准有如下几种:

  1. 尽量避免发生低优先级任务的资源被抢占;如果避免不了,则让被抢占的任务数量最少、优先级最低;
  2. 挑选已经安装了任务运行所需依赖的机器;
  3. 使任务尽量分布在不同的高可用域当中;
  4. 混合部署高优先级和低优先级任务,这样在流量峰值突然出现后,高优先级可以抢占低优先级的资源(这一点很有意思)。

Borg其实曾经使用过E-PVM模型(简单的说就是把所有打分规则按照一定算法综合成一种规则)来进行打分的。但是这种调度的结果是任务最终被平均的分散到了所有机器上,并且每台机器上留出了一定的空闲空间来应对压力峰值。这直接造成了整个集群资源的碎片化。

与上述做法的相反的是另一个极端,即尽量让所有的机器都填满。但是这将导致任务不能很好的应对突发峰值。而且Borg或者用户对于任务所需的资源配额的估计往往不是很准确,尤其是对于batch job来说,它们所请求的资源量默认是很少的(特别是CPU资源)。所以在这种调度策略下batch job会很容易被填充在狭小的资源缝隙中,这时一旦遇到压力峰值,不仅batch job会出问题,与它运行在同一台机器上的LRS也会遭殃。

而Borg采用的是“混部加抢占”的模式,这种做法集成了上述两种模型的优点:兼顾公平性和利用率。这其中,LRS和batch job的混部以及优先级体系的存在为资源抢占提供了基础。这样,Borg在“可行性检查”阶段就可以考虑已经在此机器上运行的任务的资源能被抢占多少。如果算上可以抢占的这部分资源后此机器可以满足待调度任务的需求的话,任务就会被认为“可行”。接下,Borg会按优先级低到高“kill”这台机器上的任务直到满足待运行任务的需求,这就是抢占的具体实施过程。当然,被“kill”的任务会重新进入了调度队列,等待重新调度。

另一方面Borg也指出在任务调度并启动的过程中,安装依赖包的过程会构成80%的启动延时,所以调度器会优先选择已经安装好了这些依赖的机器。这让我想起来以前使用VMware开发的编排系统BOSH时,它的每一个Job都会通过spec描述自己依赖哪些包,比如GCC。所以当时为了节省时间,我们会在部署开始前使用脚本并发地在所有目标机器上安装好通用的依赖,比如Ruby、GCC这些,然后才开始真正的部署过程。 事实上,Borg也有一个类似的包分发的过程,而且使用的是类似BitTorrent的协议。

这时我们回到Kubernetes上来,不难发现它与Borg的调度机制还比较很类似的。这当然也就意味着Kubernetes中没有借鉴传说中的Omega共享状态调度(反倒是Mesos的Roadmap里出现了类似”乐观并发控制“的概念)。

Kubernetes的调度算法也分为两个阶段:

  • “Predicates过程”:筛选出合格的Minion,类似Borg的“可行性检查”。这一阶段Kubernetes主要需要考察一个Minion的条件包括:
  • 容器申请的主机端口是否可用
  • 其资源是否满足Pod里所有容器的需求(仅考虑CPU和Memory,且没有抢占机制)
  • volume是否冲突
  • 是否匹配用户指定的Label
  • 是不是指定的hostname

“Priorities过程”:对通过上述筛选的Minon打分,这个打分的标准目前很简单:

  • 选择资源空闲更多的机器
  • 属于同一个任务的副本Pod尽量分布在不同机器上

从调度算法实现上差异中,我们可以看到Kubernetes与Borg的定位有着明显的不同。Borg的调度算法中资源抢占和任务混部是两个关键点,这应是考虑到了这些策略在Google庞大的机器规模上所能带来的巨大的成本削减。所以Borg在算法的设计上强调了混部状态下对资源分配和任务分布的优化。而Kubernetes明显想把调度过程尽量简化,其两个阶段的调度依据都采用了简单粗暴的硬性资源标准,而没有支持任何抢占策略,也没有优先级的说法。当然,有一部分原因是开源项目的用户一般都喜欢定制自己的调度算法,从这一点上来说确实是“less is more”。总之,最终的结果是尽管保留了Borg的影子(毕竟作者很多都是一伙人),Kubernetes调度器的实现上却完全是另外一条道路,确切的说更像Swarm这种偏向开发者的编排项目。

此外,还有一个非常重要的因素不得不提,那就是Docker的镜像机制。Borg在Google服役期间所使用的Linux容器虽然应用极广且规模庞大,但核心功能还是LXC的变体或者强化版,强调的是隔离功能。这一点从它的开源版项目lmctfy的实现,以及论文里提到需要考虑任务依赖包等细节上我们都可以推断出来。可是Docker的厉害之处就在于直接封装了整个Job的运行环境,这使得Kubernetes在调度时可以不必考虑依赖包的分布情况,并且可以使用Pod这样的“原子容器组”而不是单个容器作为调度单位。当然,这也提示了我们将来进行Docker容器调度时,其实也可以把镜像的分布考虑在内:比如事先在所有工作节点上传基础镜像;在打分阶段优先选择任务所需基础镜像更完备的节点。

如果读者想感受一下没有镜像的Docker容器是什么手感,不妨去试用一下DockerCon上刚刚官宣的runc项目(https://github.com/opencontainers/runc)。runc完全是一个libcontainer的直接封装,提供所有的Docker容器必备功能,但是没有镜像的概念(即用户需要自己指定rootfs环境),这十分贴近lmctfy等仅专注于隔离环境的容器项目。

3.3 Borglet

离开了Borgmaster节点,我们接下来看一下工作节点上的Borglet组件,它的主要工作包括:

启停容器,进行容器失败恢复,通过kernel参数操作和管理OS资源,清理系统日志,收集机器状态供Borgmaster及其他监控方使用。

这个过程中,Borgmaster会通过定期轮询来检查机器的状态。这种主动poll的做法好处是能够大量Borglet主动汇报状态造成流量拥塞,并且能防止“恢复风暴”(比如大量失败后恢复过来的机器会在同段一时间不停地向Borgmaster发送大量的恢复数据和请求,如果没有合理的拥塞控制手段,者很可能会阻塞整个网络或者直接把master拖垮掉)。一旦收到汇报信息后,充当leader的Borgmaster会根据这些信息更新自己持有的Cell状态数据。

这个过程里,集群Borgmaster的“优越性”再次得到了体现。Borgmaster的每个节点维护了一份无状态的“链接分片(link shard)”。每个分片只负责一部分Borglet机器的状态检查,而不是整个Cell。而且这些分片还能够汇集并diif这些状态信息,最后只让leader获知并更新那些发生了变化的数据。这种做法有效地降低了Borgmaster的工作负载。

当然,如果一个Borglet在几个poll周期内都没有回应,他就会被认为宕机了。原本运行在整个节点上的任务容器会进入重调度周期。如果此期间Borglet与master的通信恢复了,那么master会请求杀死那些被重调度的任务容器,以防重复。Borglet的运行并不需要依赖于Borgmaster,及时master全部宕机,任务依然可以正常运行。

与Borg相比,Kubernetes则选择了方向相反的状态汇报策略。当一个kubelet进程启动后,它会主动将自己注册给master节点上的apiserver。接下来,kubelet会定期向apiserver更新自己对应的node的信息,如果一段时间内没有更新,则master就会认为此工作节点已经发生故障。上述汇报信息的收集主要依赖于每个节点上运行的CAdvisor进程,而并非直接与操作系统进行交互。

事实上,不止kubelet进程会这么做。Kubernetes里的所有组件协作,都会采用主动去跟apiServer建立联系,进而通过apiserver来监视、操作etcd的资源来完成相应的功能。

举个例子,用户向apiserver发起请求表示要创建一个Pod,在调度器选择好了某个可用的minion后apiserver并不会直接告诉kubelet说我要在这个机器上创建容器,而是会间接在etcd中创建一个“boundPod”对象(这个对象的意思是我要在某个kubelet机器上绑定并运行某个Pod)。与此同时,kubelet则定时地主动检查有没有跟自己有关的“boundPod”,一旦发现有,它就会按照这个对象保存的信息向Docker Daemon发起创建容器的请求。

这正是Kubernetes设计中“一切皆资源”的体现,即所有实体对象,消息等都是作为etcd里保存起来的一种资源来对待,其他所有协作者要么通过监视这些资源的变化来采取动作,要么就是通过apiserver来对这些资源进行增删改查。

所以,我们可以把Kubernetes的实现方法描述为“面向etcd的编程模式”。这也是Kubernetes与Borg设计上的又一个不同点,说到底还是规模存在的差异:即Kubernetes认为它管理的集群中不会存在那么多机器同时向apiserver发起大量的请求。这也从另一个方面表现出了作者们对etcd响应能力还是比较有信心的。

3.4 可扩展性

这一节里与其说在Borg的可扩展性,倒不如说在讲它如何通过各种优化实现了更高的可扩展性。

首先是对Borgmaster的改进。最初的Borgmaster就是一个同步循环,在循环过程中顺序进行用户请求响应、调度、同Borglet交互等动作。所以Borg的第一个改进就是将调度器独立出来,从而能够同其他动作并行执行。改进后的调度器使用Cell集群状态的缓存数据来不断重复以下操作:

  • 从Borgmaster接受集群的状态变化
  • 更新本地的集群状态缓存数据
  • 对指定的Task执行调度工作
  • 将调度结果告诉Borgmaster

这些操作组成了调度器的完整工作周期。

其次,Borgmaster上负责响应只读请求和同Borglet进行交互的进程也被独立出来,通过职责的单一性来保证各自的执行效率。这些进程会被分配在Borgmaster的不同副本节点上来进一步提高效率(只负责同本副本节点所管理的那部分Worker节点进行交互)。

最后是专门针对调度器的优化。

缓存机器的打分结果。毕竟每次调度都给所有机器重新打一次分确实很无聊。只有当机器信息或者Task发生了变化(比如任务被从这个机器上调度走了)时,调度器缓存的机器分数才会发生更新。而且,Borg会忽略那些不太明显的资源变化,减少缓存的更新次数。

划分Task等价类。Borg的调度算法针对的是一组需求和约束都一样的Task(等价类)而不是单个Task来执行的。

随机选择一组机器来做调度。这是很有意思的一种做法,即Borg调度器并不会把Cell里的所有机器拿过来挨个进行可行性检查,而是不断地随机挑选一个机器来检查可行性,判断是否通过,再挑选下一个,直到通过筛选的机器达到一定的数目。然后再在这些通过筛选的机器集合里进行打分过程。这个策略与著名的Sparrow调度器的做法很类似。

这些优化方法大大提高了Borg的工作效率,作者在论文中指出在上述功能被禁掉,有些原来几百秒完成的调度工作需要几天才能完全完成。

4. 可用性

Borg在提高可用性方面所做的努力与大多数分布式系统的做法相同。比如:

  • 自动重调度失败的任务
  • 将同一Job的不同任务分布在不同的高可用域
  • 在机器或者操作系统升级的过程中限制允许的任务中断的次数和同时中断的任务数量
  • 保证操作的幂等性,这样当客户端失败时它可以放心的发起重试操作
  • 当一台机器失联后,任务重调度的速度会被加以限制,因为Borg不能确定失联的原因是大规模的机器失败(比如断电),还是部分网络错误。
  • 任务失败后,在一段时间内在本地磁盘保留日志及其他关键数据,哪怕对应的任务已经被杀死或者调度到其他地方了

最后也是最重要的,Borglet的运行不依赖于master,所以哪怕控制节点全部宕机,用户提交的任务依然正常运行。

在这一部分,Kubernetes也没有特别的设计。毕竟,在任务都已经容器化的情况下,只要正确地处理好容器的调度和管理工作,任务级别高可用的达成并不算十分困难。

至此,论文的前四章我们就介绍完了。通过与Kubernetes的实现作比较,我们似乎能得到一个“貌合神离”的结论。即Kubernetes与Borg从表面上看非常相似:相同的架构,相似的调度算法,当然还有同一伙开发人员。但是一旦我们去深入一些细节就会发现,在某些重要的设计和实现上,Borg似乎有着和Kubernetes截然不同的认识:比如完全相反的资源汇报方向,复杂度根本不在一个水平上的Master实现(集群VS单点),对batch job的支持(Kubernetes目前不支持batch job),对于任务优先级和资源抢占的看法等等。

这些本来可以照搬的东西,为什么在Kubernetes又被重新设计了一遍呢?在本文的第二部分,我们将一步步带领读者领悟造成这些差异的原因,即:资源回收和利用率优化。敬请关注。

作者简介

张磊,浙江大学博士,科研人员, VLIS lab云计算团队技术负责人、策划人

参考文献

  • http://research.google.com/pubs/pub43438.html
  • https://github.com/googlecloudplatform/kubernetes

你可能感兴趣的:(Docker背后的容器集群管理——从Borg到Kubernetes(一))