Skip to content

Borg ​

标签
分布式/集群与运维
字数
10072 字
阅读时间
39 分钟

Borg 是 Google 内部的集群管理器,接管整个应用车队的受理、调度、启动、重启与监控。它解决的问题比"选一台机器"大一层:把集群里所有机器当一台机器来用 —— 屏蔽资源管理与故障处理,让应用开发者只关心自己的程序。

三条收益:屏蔽资源管理与故障处理的细节;以极高可靠性与可用性运行(目标是 99.99%);让机器利用率上去 —— 利用率提升几个百分点就能省下数百万美元。

规模锚点先摆出来:排除测试 cell 后的中位 cell 约 1 万台机器,有些大得多;一个忙碌的 Borgmaster 用 10–14 个 CPU 核、最多 50 GiB 内存;已有若干 cell 的任务到达率超过每分钟 10,000 个。

术语上,cluster 由连接机器的高速数据中心级网络定义;一个 cluster 通常装一个大 cell,可能附带几个小的测试或专用 cell;一个 site 由若干栋楼组成。cell 里的机器在 CPU、内存、磁盘、网络、处理器型号、外部 IP、flash 存储等维度上异构,Borg 负责把这些差异向用户屏蔽。

它同时是 Kubernetes 的直接前身 —— 末尾用一整节说明哪些设计被继承、哪些被明确否掉。

工作负载:两类任务混跑在同一批机器上 ​

cell 跑的是异构负载,主体是两类:

长跑服务批处理任务
时长应当"永不"下线几秒到几天
请求特征短命、延迟敏感(几毫秒到几百毫秒)对短期性能波动不敏感
典型产品Gmail、Google Docs、BigtableMapReduce、Pregel、FlumeJava、Millwheel
优先级归类多数是 prod多数是 non-prod

在一个代表性 cell 里:prod 任务被分配约 70% 的总 CPU、实际使用约 60%;被分配约 55% 的总内存、实际使用约 85%。分配与使用之间的落差是资源回收(见下)的前提。

cell 之间的负载构成不同(取决于主要租户,有些 cell 偏批处理),同一 cell 的构成也随时间变化:批处理任务来了又走,面向终端用户的服务有明显的昼夜规律。

Google 的分布式存储系统 —— GFS 及其后继 CFS、Bigtable、Megastore —— 全部跑在 Borg 上。这个事实决定了 Borg 的故障语义必须足够保守。

三层抽象:job、task、alloc ​

job 是一个程序的 N 个副本:用户提交 job,一个 job 含一个或多个 task,所有 task 跑同一个二进制。job 只在一个 cell 里运行。

task 映射到一台机器上一个容器里的一组 Linux 进程。 绝大多数 Borg 负载不跑在虚拟机里 —— 两个原因:不愿付虚拟化的成本;系统设计时的处理器大量没有硬件虚拟化支持。

每个资源维度(CPU 核、RAM、磁盘空间、磁盘访问速率、TCP 端口)各自独立、细粒度地指定,不设固定尺寸的桶或槽位:CPU 用 milli-core(一个 core 是一个处理器超线程,跨机型做性能归一化),内存与磁盘用字节。

task 用静态链接以减少对运行时环境的依赖,程序打成"二进制 + 数据文件"的 package,安装由 Borg 编排。

update 是一次可回退的非原子事务 ​

用户推一份新配置、再指示 Borg 把 task 更新到新规格。这个动作被当成轻量、非原子的事务,在关闭(提交)之前都可以轻易撤销。更新通常滚动进行,并限制一次更新造成的 task 中断(重调度或抢占)数量,会超出上限的改动直接跳过。

不同改动的破坏性不同:

  • 推新二进制 —— 必然要求重启 task;
  • 加大资源需求或改约束 —— 可能让 task 装不下,于是被停掉并重调度;
  • 改优先级 —— 可以始终不重启、不移动地完成。

被抢占前,task 可以要求先收到 SIGTERM 以便清理、存状态、处理完已在执行的请求并拒绝新请求。实际约 80% 的情况下能收到这个通知,抢占方可以设置延迟上限从而让其更短。

alloc 与 alloc set ​

alloc(allocation)是机器上一块被预留的资源,一个或多个 task 在里面运行;资源一旦分配就始终占用,无论是否被使用。它的用途有三种:

  • 为将来的 task 留资源;
  • 在停止与重启某 task 之间保住资源;
  • 把不同 job 的 task 收拢到同一台机器上 —— 典型是 Web 服务器实例加一个配套的 logsaver task(把服务器本地磁盘上的 URL 日志拷进分布式文件系统)。

如果 alloc 必须迁到别的机器,它里面的 task 跟着重调度。alloc set 相当于 job:它是一组在多台机器上预留资源的 alloc,创建之后可以把一个或多个 job 提交进去运行。

准入与优先级:priority 与 quota 的分工 ​

两者解决不同问题:priority 表达正在运行或正在等待的 job 的相对重要性;quota 决定哪些 job 被允许进入调度。

priority 是一个小的正整数。高优先级 task 可以夺走低优先级 task 的资源,包括直接抢占(杀掉)后者。 Borg 划分了互不重叠的优先级带,从高到低:

带说明
monitoring监控
production长跑服务;与 monitoring 合称 prod
batch批处理
best effort又叫 testing 或 free

抢占会级联:高优先级撞掉稍低一点的,后者再撞掉更低一点的。为消除大部分级联,production 带内的 task 不允许互相抢占。细粒度优先级在别处仍然有用 —— 例如 MapReduce 的 master task 跑得比它控制的 worker 稍高,以提高可靠性。

quota 是一个资源量向量(CPU、RAM、磁盘等)绑定一个优先级和一段时间(通常按月),指定用户能一次申请的上限,形如"从今天到七月底,cell xx 里 prod 优先级下 20 TiB 内存"。quota 检查属于准入控制而不属于调度:quota 不足的 job 在提交时立即被拒。

三个配套规则:

  • 高优先级 quota 比低优先级贵;
  • production 优先级的 quota 受 cell 实际可用资源限制 —— 所以一个提交了装得进自己 quota 的 prod job 的用户,可以预期它会跑起来(模掉碎片与约束);
  • 低优先级上超额售卖 quota:每个用户在优先级零上都有无限 quota,虽然资源超售使其经常难以兑现。

对公平调度算法的态度也很明确:quota 的使用减少了对 DRF(Dominant Resource Fairness)这类策略的需要。 另外还有一套 capability 系统给部分用户特殊权限,例如让管理员删除或修改 cell 里任何 job,或让用户访问受限内核特性、关闭自己 job 上的资源估算。

命名:BNS 与写进 Chubby 的文件 ​

task 被创建、被放置之后,服务的客户端与其他系统还得能在它被迁到新机器之后找到它。Borg 为此给每个 task 一个稳定的 BNS(Borg Name Service)名字,含 cell 名、job 名、task 号。

task 的主机名与端口被写进 Chubby 里一个一致且高可用的文件,RPC 系统靠它找到端点。BNS 名字同时是 DNS 名的基础,于是 cell cc 里用户 ubar 的 job jfoo 的第 50 个 task 可通过

50.jfoo.ubar.cc.borg.google.com

访问。job 规模与 task 健康状况变化时也写进 Chubby,供负载均衡器决定往哪转。

几乎每个 Borg task 都带一个内建 HTTP 服务器,发布健康信息与成千上万条性能指标(如 RPC 延迟)。Borg 监控这个健康检查 URL,对不及时响应或返回错误码的 task 重启。运维侧的数字是 每个 SRE 管几万台机器。

诊断工具方面有两处值得记的设计:

  • 用户提交了跑不起来的 job 时,Borg 给出 "why pending?" 注解,并附带如何修改资源需求以更适配 cell 的建议;同时发布所谓"合形的(conforming)resource shape"指南,让 job 更容易被调度;
  • 所有 job 提交与 task 事件、以及逐 task 的资源使用明细都记进 Infrastore —— 一个可伸缩的只读数据存储,通过 Dremel 提供类 SQL 的交互接口。这份数据用于按用量计费、调试 job 与系统故障、长期容量规划,也提供了公开的 Google cluster workload trace。

Borgmaster:五副本、Paxos 与 Chubby 锁 ​

一个 cell 由一批机器、一个逻辑上集中的控制器 Borgmaster、以及每台机器上的 agent Borglet 组成。Borg 全部用 C++ 写。

Borgmaster 实际是两个进程:主进程与一个独立的 scheduler。主进程处理会改状态的客户端 RPC(如建 job)与只读访问(如查 job),管理系统里所有对象(机器、task、alloc)的状态机,与 Borglet 通信,并提供 Web UI 作为 Sigma 的备份。

它在逻辑上是一个进程,实际复制五份。 每个副本在内存里持有一份 cell 大部分状态的拷贝,同时状态被记录在副本本地磁盘上的、高可用的、基于 Paxos 的存储里。每个 cell 选出一个 master,它同时担任 Paxos leader 与状态修改者,处理所有改变 cell 状态的操作(提交 job、终止某机器上的 task)。master 用 Paxos 选出 —— cell 启动时选一次,选出的 master 失效时再选;选出后它取一把 Chubby 锁,好让其他系统能找到它。

  • 选举与 fail-over 通常约 10 秒,但大 cell 里可能长达一分钟,因为要重建一部分内存状态;
  • 副本从故障里恢复后,从其他更新的 Paxos 副本动态重新同步状态。

checkpoint 与 Fauxmaster ​

Borgmaster 在某时刻的状态叫 checkpoint,形式是周期快照加上 Paxos 存储里的一份变更日志。checkpoint 有四种用途:

  1. 把 Borgmaster 状态恢复到过去的任意一点 —— 例如恢复到接受某个触发 Borg 软件缺陷的请求之前,以便调试;
  2. 极端情况下手工修;
  3. 建立持久的事件日志供日后查询;
  4. 离线模拟。

第四种用途由 Fauxmaster 支撑:一个高保真模拟器,能读 checkpoint 文件,内部是完整的生产 Borgmaster 代码、只把面向 Borglet 的接口打桩。它接受改状态机与执行操作的 RPC(如"调度所有 pending 的 task"),并让模拟的 Borglet 重放 checkpoint 文件里记录的真实交互,于是可以像对着活的 Borgmaster 一样交互、逐步观察过去真实发生过的状态变化。它还用于容量规划("这类新 job 能装下多少个")和变更前的 sanity check("这个改动会驱逐掉重要 job 吗")。

调度:feasibility 与 scoring 两段 ​

job 提交后,Borgmaster 把它持久记录进 Paxos 存储,并把它的 task 放进 pending 队列。scheduler 异步扫描这个队列(它主要按 task 而不是 job 操作),在存在满足约束且资源足够的机器时把 task 分配出去。

扫描从高优先级到低优先级,同一优先级内用轮转保证用户间公平、避免被一个大 job 挡住队头。

调度算法分两段:

阶段做什么
feasibility checking找出满足 task 约束、且有足够"可用"资源的机器集合 —— "可用"包含已分配给可被驱逐的低优先级 task 的资源
scoring在可行机器里挑一台

scoring 会考虑用户指定的偏好,但主要由内建判据驱动:

  • 最小化被抢占 task 的数量与优先级;
  • 优先选那些已经有该 task 所需 package 副本的机器;
  • 把 task 散布到不同的供电与故障域;
  • 打包质量 —— 包括把高低优先级的 task 混在同一台机器上,好让高优先级在负载高峰时能扩张。

"worst fit" 与 "best fit" 的取舍 ​

Borg 早期用 E-PVM 的变体打分:跨异构资源生成一个成本值、最小化放置 task 时的成本变化。实际效果是把负载摊到所有机器上、为高峰留出余量,代价是碎片增加,特别是需要占满整机的大 task —— 这套被叫做 "worst fit"。

另一端是 "best fit",尽量把机器填满。它让一部分机器对用户 job 保持空虚(但仍跑存储服务),于是放大 task 很容易;代价是用户或 Borg 对资源需求的任何错估都会被惩罚 —— 这对突发型负载不利,对批处理尤其糟,因为批处理倾向于把 CPU 需求报低以便容易调度、再机会主义地用掉空闲资源:20% 的 non-prod task 申请不到 0.1 个 CPU 核。

现行模型是混合式,目标是减少 stranded resource —— 因为机器上另一种资源已被占满而无法使用的资源。它比 best fit 的打包效率好约 3–5%。

机器选定后若可用资源仍不够,Borg 从低到高抢占 task 直到够用,并把被抢占的 task 放回 scheduler 的 pending 队列,而不是迁移或休眠(例外:为 Google Compute Engine 用户提供虚拟机的 task 会被迁移)。

启动延迟与 package ​

task 启动延迟的中位数典型约 25 秒,方差很大。package 安装占了其中约 80%,已知瓶颈之一是写 package 的本地磁盘上的竞争。缓解手段有三层:

  • scheduler 优先把 task 分给已经装好所需 package 的机器(大多数 package 不可变,因此可共享可缓存)—— 这是 Borg scheduler 支持的唯一一种数据局部性;
  • 用树状与 torrent 式的协议并行分发 package;
  • 加上下一节的扩展手段。

扩展性:四招与量化 ​

Borgmaster 的扩展手段里,scheduler 的拆分本身就是一招:scheduler 副本跑在 cell 状态的缓存拷贝上,循环做四件事 —— 从 elected master 取状态变更(含已分配与 pending 的工作)、更新本地拷贝、做一次调度 pass、把分配结果告知 elected master。master 接受并应用这些分配,除非它们不合适(例如基于过期状态),那就留到 scheduler 下一次 pass 重新考虑。 这与 Omega 的乐观并发控制思路相近,并且 Borg 后来加上了"不同负载类型用不同 scheduler"的能力。

除了拆分调度器与把读写 RPC 分到不同线程之外,真正让 scheduler 可扩展的是三招:

招内容依据
score caching缓存打分结果,直到机器或 task 的属性变化(机器上某 task 结束、属性被改、task 需求变化)才失效;忽略资源量上的微小变化以减少失效对一台机器做 feasibility 与 scoring 很贵
equivalence classes同一个 job 里的 task 通常需求与约束相同,于是每类只对一个 task 做 feasibility 与 scoring,而不是对每个 pending task 判每台机器同 job task 同构
relaxed randomization按随机顺序检查机器,直到找到"足够多"的可行机器,再在其中选最优;不是对所有机器算分大 cell 里全算一遍纯属浪费

量化结果:把整个 cell 的负载从头调度一遍,典型需要几百秒;把上面这些技术全禁掉后,超过 3 天仍未结束。 正常运行下,对 pending 队列的一次在线调度 pass 不到半秒。

服务目标值也一并给出:这些手段把 UI 的 99 分位响应时间压在 1 秒以内、Borglet 轮询间隔的 95 分位压在 10 秒以内。

Borglet 是每台机器上的本地 agent,职责是:启停 task、task 失败时重启、通过改 OS 内核设置管理本地资源、滚动调试日志、把机器状态报给 Borgmaster 与其他监控系统。

通信方向是Borgmaster 每几秒轮询每个 Borglet,取回机器当前状态并把待办请求发下去。选这个方向有三个理由:Borgmaster 掌握通信速率、不需要显式的流控机制、防止恢复风暴。

link shard 是纯为性能伸缩而加的一层:每个 Borgmaster 副本跑一个无状态的 link shard,负责与一部分 Borglet 通信,分区在每次 Borgmaster 选举后重算。容错上,Borglet 始终上报完整状态,link shard 负责聚合与压缩 —— 只把差分报给状态机,以降低 elected master 的更新负载。

故障判定有一条很短的链路:Borglet 连续几次不响应轮询,它的机器即被标记为 down,它当时跑的 task 会被重排到别的机器上;通信恢复后,Borgmaster 让该 Borglet 杀掉那些已被重排的 task,以避免重复。另一条方向更重要的性质是:Borglet 与 Borgmaster 失去联系后仍继续正常运行,所以即使 Borgmaster 所有副本都挂了,正在运行的 task 与服务也不倒。

可用性 ​

故障在大规模系统里是常态。 Borg 用来削弱故障影响的手段有一串:

  • 自动重排被驱逐的 task,必要时换机器;
  • 把同一 job 的 task 散布到机器、机架、供电域等故障域,降低相关故障;
  • 限制 task 中断的速率,以及维护活动(如 OS 或机器升级)期间允许同时下线的 task 数量;
  • 用声明式的期望状态表示与幂等的变更操作,于是客户端失败后可以无害地重发任何被遗忘的请求;
  • 对变得不可达的机器限速寻找新位置 —— 因为无法区分大规模机器故障与网络分区;
  • 避免重复那些会导致 task 或机器崩溃的 task::machine 配对;
  • 对写进本地磁盘的关键中间数据,反复重跑 logsaver task 来恢复,即使它挂靠的 alloc 被终止或迁走 —— 用户可以设置系统重试多久,几天是常见的。

一个关键设计特征是:Borgmaster 或某个 Borglet 挂掉时,已在运行的 task 继续运行。 但把 master 保持在线仍然重要,因为 master 挂着时新 job 不能提交、已有 job 不能更新,失败机器上的 task 也不能重排。

Borgmaster 在实践中达到 99.99% 的可用性,靠的是三者组合:副本抵抗机器故障、准入控制避免过载、用简单底层工具部署实例以最小化外部依赖。另外,每个 cell 与其他 cell 相互独立,以最小化关联的运维错误与故障传播 —— 这一点(而不是扩展性限制)才是反对更大 cell 的主要论据。

评估方法:cell compaction ​

Borg 的评估难点在于:job 有放置约束、需要应对罕见的工作负载尖峰、机器异构、批处理跑在从服务任务回收来的资源里。所以**"平均利用率"这种指标不够用**。最终选的指标是 cell compaction:

给定一份工作负载,不断移除机器直到负载再也装不下,从而得出它能装进多小的 cell;每一轮都从零重新打包,以免被某个运气不好的配置卡住。

这个定义带来干净的终止条件,也便于自动化比较,避开了合成负载生成与建模的坑。具体做法:

  • 用 Fauxmaster 拿到高保真模拟结果,输入是真实生产 cell 与负载的数据(含全部约束、实际 limit、预留、使用数据);
  • 数据取自 2014-10-01 14:00 PDT 的 Borg checkpoint;
  • 从 cell 总体里先剔除专用、测试与小于 5000 台的 cell,再按大小分层抽样,选 15 个 cell;
  • 为保持机器异构性,随机选要移除的机器;为保持负载异构性,保留全部负载,只去掉与特定机器绑定的服务与存储 task;
  • 大于原 cell 一半的 job,其硬约束改为软约束;允许最多 0.2% 的 task 处于 pending(那些"挑机器挑得厉害"的);
  • 每个 cell 的实验用不同随机种子重复 11 次,图上用误差棒给出所需机器数的 min 与 max,取 90 分位值作为"结果" —— 均值或中位数不符合一个想稳妥装下负载的系统管理员会怎么做。

一个规模细节:实验一度消耗了 20 万个 Borg CPU 核,即便在 Google 的尺度上这也是一笔不小的投入。

五组量化结论 ​

① cell 共享:混跑比分离省 20–30% 机器 ​

几乎所有机器同时跑 prod 与非 prod 任务:共享 Borg cell 里 98% 的机器如此,全部受管机器上是 83%。

如果把 prod 与非 prod 分到不同 cell,中位 cell 要多用 20–30% 的机器。 原因是 prod job 通常为罕见尖峰预留资源,但大部分时间不用这些资源,Borg 把这些未用资源回收给 non-prod,于是整体机器数下降。

多用户共享同理:若按"某用户至少消耗 10 TiB 内存(或 100 TiB)"把其负载分出去单独成 cell,需要 2–16 倍的 cell 数、多 20–150% 的机器。

那么把不相关的用户和 job 类型打包到同一批机器上,会不会因 CPU 干扰反而要多买机器?用 **CPI(cycles per instruction)**做性能干扰的代理指标(CPU 密集型程序 CPI 翻倍即运行时间翻倍)测出来的结果是混合的:

  • CPI 与两个量正相关:同区间内机器的总体 CPU 使用率,以及(大体独立地)机器上的 task 数。给一台机器加一个 task,会让其他 task 的 CPI 涨 0.3%(线性模型拟合);机器 CPU 使用率每升 10%,CPI 涨幅不到 2%。但这些相关性只解释了 CPI 测量方差的 5%,其他因素占主导,例如应用自身差异与特定的干扰模式。
  • 共享 cell 的 CPI 均值 1.58(标准差 0.35)对专用 cell 的 1.53(0.32) —— 共享 cell 的 CPU 性能大约差 3%。
  • 为排除"不同 cell 负载不同"甚至"对干扰更敏感的程序被移到了专用 cell"这类选择偏差,改看在两类 cell 的所有机器上都跑的 Borglet:专用 cell 里 CPI 1.20(0.29),共享 cell 里 1.43(0.45),即专用 cell 里快约 1.19 倍(这会高估轻载机器的影响,略微偏向专用 cell)。

结论:即便取最不利的一项结果,共享仍然划算 —— CPU 的这点降速被机器数减少抵消,而且共享的好处适用于内存、磁盘等所有资源,不只是 CPU。

② 大 cell:细分成小 cell 要更多机器 ​

把负载随机置换后轮转分配到若干小 cell,结果确认用小 cell 会显著多要机器。这也解释了 Google 为什么造大 cell:既为了跑大计算,也为了减少资源碎片。

③ 细粒度请求:按 2 的幂分桶要多 30–50% 资源 ​

CPU 请求以 milli-core 计、内存与磁盘以字节计,用户确实用足了粒度:需求分布上没有明显的"甜点"值,CPU 与内存请求之间也没有明显关联(除 90 分位及以上内存请求比以往文献略大)。

把 prod job 与 alloc 的 CPU 核与内存 limit 向上取整到最近的 2 的幂(CPU 从 0.5 核起、RAM 从 1 GiB 起),中位情况下要多 30–50% 的资源:上界来自把整台机器分给那些在压缩前放大 4 倍后仍装不下的大 task,下界来自允许这些 task 进 pending。这个数比文献里报告的约 100% 开销低,原因是这里支持了 4 种以上的桶、且允许 CPU 与 RAM 容量独立伸缩。

④ 资源回收:reservation 的衰减与两类任务的不同口径 ​

job 可以指定 resource limit(每个 task 应获资源的上界)。limit 有两个用途:判断用户 quota 是否够准入;判断某台机器是否有足够空闲资源放下这个 task。用户倾向于申请多于实际使用的资源,因为 Borg 通常会杀掉试图用超申请量内存或磁盘的 task、或把 CPU 限流到申请值;另一些 task 只是偶尔需要用到全部资源(高峰时段、或应对拒绝服务攻击时)。

资源回收的做法是:估算 task 会实际用多少,把剩下的回收给能忍受较低质量资源的工作(如批处理)。估算值叫 task 的 reservation,由 Borgmaster 每几秒用 Borglet 采集的细粒度使用信息算出。

reservation 的演化规则值得完整记下:

  • 初值等于 resource request(也就是 limit);
  • 300 秒之后(为了跨过启动瞬态)缓慢向"实际使用量 + 安全裕度"衰减;
  • 一旦实际使用超过 reservation,立刻快速上调。

scheduler 对 prod task 用 limit 算 feasibility,所以 prod 从不依赖被回收的资源、也不暴露在超售之下;对 non-prod task 则用现有 task 的 reservation 算,好让新 task 能排进被回收的资源里。 如果估算错了、机器在运行时不敷(即便所有 task 都没超 limit),Borg 杀或限 non-prod task,绝不杀 prod。

在一个中位 cell 里,约 20% 的工作负载跑在被回收的资源上。 一个生产 cell 上做过一周三档参数的实验:

周设置结果
第 1、4 周baselinereservation 与使用之间的空隙最大
第 2 周aggressive(缩小安全裕度)reservation 明显贴近使用量,OOM 事件率略升
第 3 周medium(介于两者之间)居中

净值评估后,medium 参数被推广到其他 cell。 一个相关的优先级细节:超出内存 limit 的 task 在需要资源时会最先被抢占,无论它的优先级 —— 所以 task 超内存 limit 的情况很少见;反之 CPU 可以被随时限流,所以短期尖峰把使用推过 reservation 相对无害。

⑤ 隔离:appclass、可压缩与不可压缩 ​

规模先摆出来:50% 的机器跑 9 个以上 task;90 分位的机器约 25 个 task、约 4500 个线程。 共享提高利用率,但必须有机制阻止 task 互相干扰,安全与性能两方面都要。

安全隔离的主机制是 Linux chroot jail。早期为了支持远程调试,会向用户自动分发(并收回)ssh 密钥,只在机器正在跑该用户的 task 期间给他访问权;对多数用户,这已被 borgssh 取代 —— 它与 Borglet 协作,构造一个跑在该 task 的同一 chroot 与 cgroup 里的 shell,访问被收得更紧。外部软件由 AppEngine 与 Compute Engine 用 VM 与安全沙箱跑,每个托管的 VM 作为一个 Borg task 跑在一个 KVM 进程里。

性能隔离走过一段弯路:早期 Borglet 只有很原始的资源隔离执行 —— 事后检查内存、磁盘空间与 CPU 周期,对用超内存/磁盘的 task 直接终止,对用超 CPU 的激进施加 Linux CPU 优先级。但流氓 task 仍然太容易影响同机其他 task 的性能,于是有些用户把资源请求灌水以减少 Borg 能与自己共调度的 task 数,从而降低利用率(资源回收能捞回一部分,但因为安全裕度捞不回全部),最极端的情况下用户申请专用机器或专用 cell。

现在所有 Borg task 都跑在基于 Linux cgroup 的资源容器里,由 Borglet 操纵容器设置,控制力大幅提升因为内核在环里。即便如此,内存带宽、L3 缓存污染这类底层资源干扰仍偶有发生。

appclass 与两类资源 ​

为应对过载与超配,task 带一个 appclass。最重要的区分是**延迟敏感(LS)**与其余(合称 batch):

  • LS task 用于面向用户的应用与要求快速响应的共享基础设施服务;最高优先级的 LS task 能一次让 batch task 饿几秒。

第二组区分按资源性质:

资源类型例子耗尽时的动作
可压缩(compressible)CPU 周期、磁盘 I/O 带宽这些资源基于速率、可以降低服务质量而不杀 task —— Borglet 直接限流(偏向 LS),于是短期尖峰不必杀掉任何 task;若情况不改善,Borgmaster 会把一个或多个 task 从机器上移走
不可压缩(non-compressible)内存、磁盘空间不杀 task 就回收不了 —— Borglet 立刻从低到高终止 task,直到剩下的 reservation 能被满足

Borglet 里有一个用户态控制环,按**预测的未来用量(prod task)或内存压力(non-prod)**给容器分配内存;处理内核抛出的 OOM 事件;在 task 试图分配超过内存 limit、或超配的机器真的耗尽内存时杀掉 task。Linux 的 eager file-caching 让实现显著复杂化,因为需要精确的内存记账。

几个具体机制:

  • LS task 可以预留整个物理 CPU 核,从而阻止其他 LS task 使用这些核;batch task 允许跑在任何核上,但相对 LS task 只分到极小的调度份额;
  • Borglet 动态调整贪婪 LS task 的资源上限,确保它们不会让 batch task 饿上几分钟,必要时选择性地施加 CFS 带宽控制 —— 这里份额(shares)不够用,因为存在多个优先级层次;
  • 标准 Linux CFS 需要大量调优才能同时支持低延迟与高利用率:Borg 版本的 CFS 使用扩展的 per-cgroup 负载历史、允许 LS task 抢占 batch task、并在一个 CPU 上有多个 LS task 就绪时缩小调度量子;
  • 少数对延迟要求特别紧的应用会谨慎使用 cpuset 把 CPU 核直接分配出去。

task 被允许消耗到自己的 limit;对 CPU 这类可压缩资源,多数 task 被允许超出 limit 以利用空闲资源 —— 只有 5% 的 LS task 关掉了这个行为(大概是为了更好的可预测性),batch task 里不到 1% 关掉。

使用空闲内存默认关闭,因为它增加 task 被杀的概率;即便如此,10% 的 LS task 主动打开,79% 的 batch task 也打开了 —— 因为这是 MapReduce 框架的默认设置。这与被回收资源的结论互补:batch task 愿意机会主义地用掉空闲内存和被回收的内存,大多数时候奏效,偶尔会在 LS task 急需资源时被牺牲掉一个。

经验教训:慎用与沿用 ​

三个警示 ​

① 把 job 当作 task 的唯一分组机制太受限。 Borg 没有一等的手段把整个多 job 服务当作一个实体管理,也无法引用"相关的服务实例"(例如 canary 与 production 两条轨)。用户的权宜之计是把服务拓扑编码进 job 名,再写更高层的管理工具去解析名字。另一端,无法引用 job 的任意子集,导致滚动更新与 job 伸缩的语义受限。

Kubernetes 的应对是否掉 job 这个概念,改用 **label(任意键值对,可挂在系统里任何对象上)**来组织调度单元 pod。想要 Borg job 的等价物,就挂一个 job:jobname label;想要别的分组(service、tier、release-type)也表达得出来。操作通过 label 查询来选择作用对象。

② 一台机器一个 IP 地址带来一串麻烦。 Borg 里一台机器上的所有 task 共用宿主机的单一 IP、因而共享端口空间,后果是:Borg 必须把端口当资源调度;task 必须预先声明需要几个端口、并接受启动时被告知用哪几个;Borglet 必须强制端口隔离;命名与 RPC 系统必须同时处理端口与 IP。

Kubernetes 借助 Linux namespace、VM、IPv6 与软件定义网络换了一条路:每个 pod 与 service 各自有 IP,于是开发者能自己选端口而不必让软件适配基础设施挑的端口,基础设施管理端口的复杂度也随之消失。

③ 为 power user 优化,牺牲了 casual user。 BCL 规格列了约 230 个参数,这套丰富的 API 让"随便用用"的用户更难,也约束了它自身的演进。应对办法是在 Borg 之上建自动化工具与服务,由实验来定合适的设置 —— 这得益于容错应用给的试错自由:自动化搞错了是麻烦,不是灾难。

四项沿用 ​

① alloc 有用。 它派生出被广泛使用的 logsaver 模式,以及另一种常见模式:一个简单的 data-loader task 周期性更新 Web 服务器用的数据。alloc 与 package 让这类辅助服务能由独立团队开发。Kubernetes 里对应的抽象是 pod —— 一个或多个容器的资源信封,总被调度到同一台机器上、可以共享资源;Kubernetes 用同一个 pod 里的辅助容器代替 alloc 里的 task,思路一致。

② 集群管理不只是任务管理。 Borg 的主职是管 task 与机器的生命周期,但跑在上面的应用还受益于命名与负载均衡等许多集群服务。Kubernetes 用 service 抽象支持这两者:service 有名字,用 label selector 定义一组动态的 pod;集群里任何容器都能用 service 名连接它,底层由 Kubernetes 自动在匹配的 pod 间做连接级负载均衡,并在 pod 因故障被重调度时跟踪它们的位置。

③ 自省(introspection)是必需的。 Borg 几乎总是"就能用",但出问题后找根因很难。一个重要设计决策是把调试信息暴露给所有用户而不是藏起来 —— Borg 有几千个用户,所以**"自助"必须是调试的第一步**。代价是让废弃特性、改变用户依赖的内部策略更难,但仍是净赚,且找不到现实的替代方案。为处理海量数据,提供了若干层 UI 与调试工具,让用户先快速定位与自己 job 相关的异常事件,再下钻到应用与基础设施本身的详细事件与错误日志。

Kubernetes 复制了其中许多手法:自带 cAdvisor 做资源监控、基于 Elasticsearch/Kibana 与 Fluentd 的日志聚合;master 的状态可被查询出快照;并有一套统一的事件记录机制(pod 被调度、容器失败等)供所有组件使用、对所有客户端可见。

④ master 是分布式系统的内核。 Borgmaster 最初是单体设计,后来逐渐变成坐在一套服务生态中心的内核:调度器与主 UI(Sigma)被拆成独立进程,并加上准入控制、纵向与横向自动伸缩、task 重排、周期性提交(cron)、工作流管理、以及把系统动作归档供离线查询的服务。这些让工作负载与特性集都在扩张的同时没有牺牲性能与可维护性。

Kubernetes 走得更远:核心是一个只负责处理请求与操作底层状态对象的 API server,集群管理逻辑则由小的、可组合的微服务构成、作为这个 API server 的客户端 —— 例如维持期望 pod 副本数(对抗故障)的 replication controller,以及管理机器生命周期的 node controller。

相关 ​

  • Chubby —— 直接依赖两处:Borgmaster 选出后取一把 Chubby 锁好让其他系统找到它;task 的 BNS 名字写进 Chubby 里一个一致且高可用的文件供 RPC 系统解析端点
  • Bigtable —— 上下层关系反过来:Bigtable 跑在 Borg 上(GFS 及其后继 CFS、Bigtable、Megastore 都是 Borg 的租户),所以 Borg 的故障语义直接决定这些存储系统的可用性设计空间
  • BASE 理论 —— 可用性是所需组件可用性的乘积,而 Borg 的做法是把组件从"必需"退成"可选":Borgmaster 或某个 Borglet 挂掉不影响已在运行的 task,只有"提交新 job / 更新已有 job / 重排失败机器上的 task"这三件事需要 master 在线
  • Dapper —— 同一专栏的另一半:Borg 管"任务放在哪、活了没",Dapper 管"一次请求在各层花了多少时间";两者合起来才是集群运维的完整视角

参考 ​

  • A. Verma, L. Pedrosa, M. Korupolu, D. Oppenheimer, E. Tune, J. Wilkes. Large-scale Cluster Management at Google with Borg. EuroSys 2015. DOI 10.1145/2741948.2741964

贡献者 ​

文件历史 ​