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

阅读数:13614 2015 年 7 月 15 日 00:56

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

感谢郭蕾对本文的审校。

给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。

评论

发布