IT 论文精读 · PAPER 59
Verma, Pedrosa, Korupolu, Oppenheimer, Tune, Wilkes · Google · EuroSys 2015
2015 年,Google 第一次系统地公开了 Borg——一套已在它内部默默跑了十几年的系统,Google 几乎所有程序(搜索、Gmail、广告、数据分析)都由它安排在哪台机器上运行;今天广为人知的 Kubernetes,就是照着 Borg 的思路重写并开源的产物。
Borg 干的事一句话说得清:把一栋机房里成千上万台机器,当成一台巨大的机器来分配。你不再说「我要 3 号机和 7 号机」,只说「我要跑 500 份这个程序、每份大概要这么多算力和内存」。
在 Borg 之前(今天很多公司仍然如此):每个团队圈一批属于自己的机器。麻烦有两层:每批都得按最忙的那一刻来买,于是平时大半时间闲着;而且互不借用——广告那批机器半夜闲得发慌,隔壁排队等算力的报表任务也用不上它。
打个比方:一栋写字楼给每家公司分死一层楼,谁都不许用别家的会议室。于是每层都得按「全员开大会」的规模留出会议室——可全员大会一年只开两次。
机器上跑的活分两类,脾气正相反。在线服务(网页搜索、邮箱)得一直开着,慢一点用户立刻有感觉,但忙闲随时段大幅起伏;批处理(跑数据、算报表)一次跑几分钟到几小时、跑完就散,慢一点没人在乎,大不了重跑。
Borg 的核心就是:把这两类活混在同一批机器上跑,让批处理去吃掉在线服务此刻没用上的那部分。回到写字楼的比方:会议室不再分死,谁要用谁登记;但开会分等级——真有客户来了,正在里面开内部脑暴的那伙人得腾地方。
第一,排优先级。机器紧张时,Borg 直接把低优先级的活踢下去、把位置腾给高优先级的;被踢掉的批处理排队等下一轮——反正它能从头重跑。
第二,盯着真实用量,把没用上的收回来。工程师申请资源时几乎总是往多了要,怕不够。Borg 不太信这个数字,它一直观察你实际用了多少,在申请额和实际用量之间划一条带安全余量的线,中间那截空的转手借给批处理;等你用量真涨上来,再把借出去的收回。
第三,机器坏了它自己补。上万台机器每天都有坏的。Borg 一直听着每台机器的心跳,某台失联,上面的活自动在别处重开。
Google 自己算过账:如果把在线服务和批处理拆到不同机器上分开放,中位规模的机房要多买两到三成的机器。更深远的是它定下了一种做法:你只描述你要什么,不指定它在哪儿跑——今天几乎所有的云、所有 Kubernetes 集群用的都是这个思路。诚实说一句代价:这份便宜是批处理任务用「随时可能被踢掉」换来的,写这类程序必须能忍受跑到一半被杀、从头再来。
把整栋机房当一台机器:你只说要什么、不说在哪跑;在线服务与批处理混装在同一批机器上,靠「优先级抢占」和「回收你申请了却没用的额度」把利用率榨上去,机器坏了自动补——省下两三成机器,也定义了后来 Kubernetes 的样子。
想看 Borg 的架构图、调度器怎么在上万台机器里挑一台、以及那些实测数字? → 切到精读版
Borg 是 Google 的集群管理系统:用户提交一份声明式的作业规格(要跑几份、每份要多少 CPU 与内存、有什么约束),Borg 在一个由上万台机器组成的 cell 里替它选机器、启动、监控、失败重启。它靠优先级抢占把延迟敏感的在线服务与吞吐型批处理混装在同一批机器上,再靠资源回收把「申请了却没真用」的额度借给低优先级任务,从而在保住在线服务可用性的前提下大幅拉高机房利用率。这是 Google 十余年生产经验的首次系统披露,也是 Kubernetes 的直接设计蓝本。
cgroups 给每个程序框定能用多少 CPU 和内存。它不像虚拟机那样各带一个操作系统,开销小得多,但隔离也没那么彻底。作者是 Abhishek Verma、Luis Pedrosa、Madhukar Korupolu、David Oppenheimer、Eric Tune、John Wilkes,来自 Google,论文发在 EuroSys 2015。写这篇时 Borg 已在生产环境跑了十余年——GFS、MapReduce、Bigtable 这些名篇里的系统,本身都是跑在 Borg 上的任务。同代的外部对手是 Mesos(2011)与 YARN(2013);Google 内部还有个把单体主控改成「共享状态 + 乐观并发」的实验性后继 Omega(2013)。一年后同一批人写了 Borg, Omega, and Kubernetes(2016)复盘十年教训——而 Kubernetes 正是把 Borg 的模型开源重写的产物。
Google 要在同一批机器上养活两类性格相反的负载。在线服务(搜索、Gmail、广告投放)长期运行、对尾延迟敏感,但流量有昼夜峰谷;批处理(索引构建、日志分析、MapReduce 作业)从几秒到几天不等,跑完即退,只在乎吞吐。
旧做法是按用途把机器切成互不相干的几摊:每摊按自己的峰值配容量,于是每摊都长期闲置一大块,闲置的那块又借不给隔壁。更麻烦的是,机器故障在万台规模下是常态而非意外——若要人工去补、去重启、去改配置,运维根本跟不上。
于是真问题有三条,且互相拉扯:怎么把利用率提上去;提利用率就得混装,而混装会让在线服务被邻居干扰,怎么保住它的可用性;以及怎么让几千名工程师不必知道机器的存在就能把程序跑起来、跑得住。
Borg 的第一个决定是把接口收窄成一份声明式规格:一个 job 声明「跑多少份、每份要多少毫核 / 内存 / 磁盘、需要什么样的机器」,然后交出去。Borg 负责选机器、装包、启动、监控存活、失败重启、机器挂了换一台重开。「我的程序在哪台机器上」从此不再是工程师需要回答的问题。
配套两个小零件让这件事真正可用。一是 alloc(allocation 的缩写):在一台机器上预留一块资源,可以往里塞多个互相配合的 task(比如主程序 + 一个上传日志的边车进程),它们随预留一起搬家——这正是后来 Kubernetes 里 pod 的原型。二是 BNS(Borg Name Service):每个 task 拿到一个稳定的名字(存在 Chubby 里),别人按名字找它,不必知道它此刻落在哪台机器、哪个端口——没有这层命名,「任务随时会换机器」就是灾难而不是特性。
架构本身很朴素:一个 cell 里有一个逻辑上的 Borgmaster(实为 5 个副本,用 Paxos 保存 cell 状态并选主,内含调度器)和每台机器上的小代理 Borglet。Borgmaster 每隔几秒轮询一次每个 Borglet,取回现状、下发待办;轮询而不是让 Borglet 主动上报,是为了让主控自己掌握流量节奏,免得恢复时被上万台机器同时冲垮。而主控失联时 Borglet 继续照看已在跑的任务,所以主控短暂不可用不波及正在服务的程序。
提高利用率的主发动机,是把在线服务和批处理装到同一批机器上。做法是给每个 task 一个优先级,优先级归入几个不重叠的档(由高到低大致是:监控 → 生产 → 批处理 → 尽力而为)。生产(prod)指的就是那些不能挂的在线服务,其余归为非生产。
规则很直白:机器上装不下时,高优先级的 task 可以把低优先级的挤下去——后者被杀掉、退回待调度队列。有一条重要例外:生产级 task 之间不互相抢占,否则一次抢占会引发「你踢我、我踢他」的级联。
光有优先级还不够,否则人人都把自己的活标成最高级。于是配上配额(quota):某个团队在某个优先级上、某段时间内最多能申请多少资源,在提交时做准入控制;高优先级的配额更贵、更难拿,最低优先级则近乎不限量(代价是它随时会被抢占)。优先级管运行时谁让谁,配额管提交时谁能要多少——两件事必须分开,否则「优先级」就会沦为一场谁也不肯先让步的通货膨胀。
混装还不够,因为工程师申请资源时几乎总是往多了要——宁可多买保险,也不愿半夜被内存不足叫醒。于是一台机器上「已被申请走」与「真正在被使用」之间,长期空着一大截。
Borg 的办法是把一个 task 的资源分成三条线:limit(申请上限)是你要的、也是收费依据;usage(实际用量)是 Borg 持续观测到的真实消耗;中间那条 reservation(预留)由 Borg 按近期用量估出来、再加一层安全余量。reservation 与 limit 之间的那截被判定为「你大概率用不上」,回收去借给非生产任务。用量一涨、预留跟着抬高,借出去的立刻收回——代价由能忍受被杀的低优先级任务承担。论文报告:约两成的负载跑在这类回收出来的资源上。
调度分两步。第一步可行性筛选:哪些机器能装下这个 task。「够不够」的口径按优先级而不同——生产任务按各邻居声明的 limit 算,非生产任务按估出来的 reservation 算,这正是「超卖」得以发生的地方。第二步打分,在可行的机器里挑一台:策略是两种思路的混合——摊开(负载铺散,给流量突增留余地)与塞满(填实一台,减少碎片);纯摊开会落得谁也装不下大任务,纯塞满则一有峰值就出事。
难点在于规模:一个 cell 上万台机器,有的 cell 任务到达率超过每分钟一万个,逐台算分根本来不及。Borg 用三招压下来:缓存分数(机器没大变就复用上次的分)、等价类(同一 job 的几百个 task 需求一样,只算一次)、放松的随机化(随机扫一部分、够用就停)。三招换的是同一件事:用「够好」换「算得完」。另有个容易被忽略的实测结论:task 从被调度到跑起来中位约 25 秒,其中约 80% 花在装软件包上——所以 Borg 优先挑已装过该包的机器。真实系统里,瓶颈常常不在你以为的那个算法上。
把互不相识的程序塞进同一台机器,隔离就是命门。Borg 用基于 cgroups 的容器而非虚拟机——省下每虚拟机一份操作系统的开销,代价是隔离没那么彻底。资源分两类对待:可压缩的(CPU、磁盘 I/O 带宽)超了就限速压慢;不可压缩的(内存、磁盘空间)没法压慢,超了只能杀。
可用性则靠一整套习惯做法:被驱逐的任务自动重新调度;同一 job 的副本刻意分散到不同故障域(不同机架、不同供电),免得一次断电全灭;限制同时中断的速率。论文还提到一个很实用的东西——Fauxmaster:高保真的模拟主控,喂进真实 cell 状态离线重放,用来调试、做容量规划、验证「换个策略会怎样」。下一节那些「省了多少机器」的结论,正是靠它算出来的。
论文的评估方法本身值得学:cell 压缩(cell compaction)——把某个真实 cell 的负载喂给 Fauxmaster,不断拿掉机器、每次从头重装填,直到再也装不下;得到的最小机器数就是这份负载的「真实成本」。改一条策略重算一次,两者之差就是这条策略值多少台机器。
第一,它把「集群即计算机」从口号变成了有生产证据的工程事实:十余年、上万台机器规模的运行记录,证明「工程师只声明需求、不接触机器」这条路走得通,而且省下的是两三成机器这种量级的钱。「混装 + 优先级抢占 + 按实测用量超卖回收」自此成了所有集群调度器的默认议题。
第二,它是 Kubernetes 的设计蓝本。alloc 变成了 pod,「声明式规格 + 主控持续把现实拉向期望状态」成了 K8s 的核心循环,Borglet 对应 kubelet。今天绝大多数「云原生」实践——容器、声明式配置、控制平面与数据平面分离、优先级与抢占——都能在这篇里找到原型。
① 一句话:用户只提交「要什么」的声明式规格,Borg 在一个上万台机器的 cell 里替它选机器、启动、监控、失败重开——把一栋机房当一台机器来分配。
② 痛点:按用途把机器切成互不相干的几摊,每摊按峰值配、闲置又借不出去;万台规模下故障是常态,人工运维跟不上。
③ 机制一,混装:在线服务与批处理放同一批机器,用优先级档与抢占保证前者优先;生产级任务之间不互相抢占,以免级联。
④ 机制二,回收:limit(申请)/ usage(实测)/ reservation(实测+余量)三条线,reservation 与 limit 之间的空档借给低优先级任务;约 20% 负载跑在回收资源上。
⑤ 调度:先可行性筛选、再打分(摊开与塞满的混合);靠缓存分数、等价类、放松随机化把上万台的选择压到算得完;启动中位 25 秒,八成时间在装包。配套零件有 alloc(pod 的原型)、BNS 命名、cgroups 隔离。
⑥ 结果(cell 压缩法测得):分开放生产与非生产要多 20–30% 机器;资源请求粗粒度取整要多 30–50%;大 cell 优于小 cell;中位 cell 约 1 万台。
⑦ 影响与局限:它是 Kubernetes 的直接蓝本(alloc→pod、声明式控制循环、kubelet);但作者自陈 job 是唯一分组手段、一机一 IP 是设计失误,容器隔离不彻底,且收益高度依赖 Google 的同质化环境,公有云未必照搬得动。