IT 论文精读 · PAPER 27
Dean & Barroso · Google · CACM 2013
2013 年,Google 的 Jeff Dean 和 Luiz Barroso 写了一篇只有几页的短文《The Tail at Scale》,讲一件做大型网站的人都躲不开的事:为什么系统「平均很快」,用户却总能碰上卡顿。他们的答案不是「把每台机器都修得更快」——那做不到——而是承认机器一定会偶尔慢,再让软件把这种慢兜住。
假设你的机器很争气:每台有 99% 的概率一秒内回话,只有 1% 的时候会打个盹。可现代的一次网页搜索,要同时问一百台机器、等它们全都回话才能拼出结果。这时候——六成以上的请求都会超过一秒。每台都「99 分优秀」,合起来却「六成不及格」。
原因很朴素:你等的不是平均,是最慢那一个。一个人出门总能准时,二十个人的团队几乎不可能同时到齐——只要一个人迟到,全队就得等。规模不是把好运相加,而是把坏运放大。
而那点「打盹」并不是机器坏了,是它总有别的事在身:同机还跑着别人的程序在抢 CPU 和内存;后台程序偶尔要整理积攒的数据;内存里的垃圾要回收,回收时得停下手里的活打扫;机器太热会自动降速;请求还要一层层排队。每一项都只偶尔耽误几毫秒,可只要你同时等着上百台,就总有一台正好在打盹。
工程史上有过一次重要的心态转变:我们不再指望机器不坏,而是假设它一定会坏,靠多存副本、坏了就换一份,让系统照样能用——这叫容错。这篇文章说:对「慢」也照这个办。
最直接的一招叫备份请求:向一台机器发出请求后,如果它迟迟不回,就再向另一台存着同样数据的机器问一遍,谁先答用谁的,再把另一份取消掉。妙处在代价极小——只有那少数几个慢请求才会触发第二问,多发出去的请求还不到百分之几,而最慢那一小撮请求的耗时能从「一两秒」掉到「几十毫秒」。另一招是提前避开:某台机器最近明显反应慢,就先把它从名单里摘出来观察——摘掉一台慢机器,整体反而变快。
后台清理这类必须做的脏活,直觉上应该「大家错开时间做,别撞一起」。文章却说:恰恰要让所有机器同时做。理由一想就通——错开的话,任何时刻总有某台机器正忙着打扫,而你每次都要等所有机器,于是每个请求都被拖一下;全体同时打扫,则只有那一小段时间的请求受影响,其余时间干干净净。
说句诚实的:这些办法多半是用余量换稳定——多问一遍、多留副本,都得有机器闲着才使得出来;集群本来就满负荷时,多发的请求反而会把大家一起拖慢。
在成百上千台机器上,你等的是最慢那一个,所以「偶尔慢一下」会被规模放大成「经常慢」。既然根治不了,就像当年对付「机器会坏」那样去对付「机器会慢」:多问一台、谁快用谁、把脏活集中到同一时刻、把慢机器先摘掉——让系统对长尾容错。
想看放大效应的曲线、对冲/绑定请求的时序图和实测数字? → 切到精读版
在成百上千台机器协同服务一次请求的系统里,单机偶发的延迟抖动会被规模放大成用户可感的常态卡顿——因为响应时间取决于最慢的那个组件。抖动的根源(资源争抢、后台维护、垃圾回收、排队、功耗降频)无法彻底消除,所以应当像当年用容错(fault tolerance)从不可靠零件搭出可靠系统那样,用一套「尾延迟容错」(tail-tolerant)技术从不可预测的组件搭出可预测的服务。它也把行业习惯从「看平均延迟」改成了「盯 99 / 99.9 分位」。
作者是 Jeffrey Dean 与 Luiz André Barroso——Google 基础设施的两位核心人物(Dean 是 MapReduce、Bigtable、Spanner 的作者之一;Barroso 推动了「把整个数据中心当成一台计算机」这一视角),2013 年发表于《Communications of the ACM》。它不是有新算法的研究论文,而是一篇经验提炼式的观点文章,把 Google 十余年做超大规模在线服务的实践压成一个统一框架。它上承 MapReduce(2004)的备份任务——给落后的任务再起一份副本抢跑,正是「对冲」的雏形;也上承 Dapper(2010,本站 Paper 26):先有按单次请求看穿全链路的测量能力,才谈得上治理尾延迟。
先看那个把所有人震住的算术。假设一台服务器有 1% 的概率响应超过一秒——听起来是个相当健康的指标。但如果一次用户请求必须并行问 100 台这样的服务器、并等它们全部返回,那么63% 的用户请求会超过一秒(一台都不打盹的概率只有 0.99¹⁰⁰ ≈ 37%——「每台都顺利」要连中 100 次,很难)。组件级的小概率抖动,被扇出宽度放大成了系统级的常态。
文章还给了一组 Google 真实服务的测量:随机看一个叶子请求,99 分位耗时约 10 毫秒;「等所有叶子都返回」的 99 分位约 140 毫秒;只等 95% 的叶子则降到约 70 毫秒——同一批机器,只因「等多少个」不同,延迟差出好几倍。
关键洞察是:这些慢大多不是故障,而是正常运行的副产品,不可能靠「修好它」消灭——共享资源争抢(同机应用争 CPU 核、缓存、内存与网络带宽)、后台守护进程(平均只吃一点资源,被调度到时造成几毫秒打嗝)、全局资源共享(交换机、共享文件系统)、维护活动(日志合并压缩、索引更新、数据重平衡)、垃圾回收停顿、多层排队(网卡、操作系统、线程池,每层都把分布拉宽还会队头阻塞)、功耗与散热(撞功耗上限降频)。而硬件趋势——更多核、更激进的省电与共享——只会让抖动更大。
全文最值钱的想法是一个类比:工程界早已不再追求「零件不坏」,而是假设零件会坏、用冗余搭出可靠系统——这就是容错。这里说:对延迟也这么干,把「组件偶发地慢」当成一种暂时性故障让软件运行时绕开,于是能用响应时间不可预测的组件拼出响应时间可预测的服务。这个立场叫 tail-tolerant,落法分两路:压小抖动与与抖动共存。
① 区分服务等级 + 高层排队:交互式请求与离线批处理分优先级,队列维护在靠近应用的高层、底层设备队列尽量短——队列越短,调度策略才说得上话。② 拆掉队头阻塞:把大请求切成一串小请求交替执行,它就不会把后面只需几毫秒的请求全堵在门口。
③ 管好后台活动,并让扰动同步化。后台任务要限流、挪到负载低谷;反直觉的是第二句:让全集群的重量级后台活动同时发生,而不是各自错开。因为在「等所有叶子返回」的结构里,错开等于任何时刻都有某台机器在忙,每个请求都被拖一下;同步化把伤害压缩进一个短窗口,其余时间全都干净。
交互式请求只有几十毫秒寿命,来不及慢慢观察调整,只能在这一次请求内部把慢绕开。核心手法是冗余问一遍,但要问得聪明——每个请求都发给所有副本固然能取最快的,代价却成倍。
对冲(hedged request)。先只发给一个副本;若它超过「平常该回的时间」还没回(论文的门槛是预期延迟的 95 分位),就再向第二个副本发一份,谁先答用谁的、然后取消另一个。便宜的原因:只有尾部那 5% 的请求会触发第二问,额外流量只有百分之几,却正好覆盖了最疼的部分。实测:在数据分布于 100 台机器的类 Bigtable 表上读 1000 个键,延迟 10 毫秒后对冲,99.9 分位从 1800 毫秒压到 74 毫秒,多发的请求只有 2%。
绑定请求(tied request)。对冲的缺陷是那段等待窗口本身白等了。绑定更进一步:同时把请求塞进两个副本的队列,并告诉每台机器它的「孪生兄弟」是谁;哪台先把它从队列取出来开工,就立刻给另一台发取消消息。这样等的不是超时,而是「谁先排到队头」——直接吃掉机器间的排队时间差。关键细节:给第二台加一点微小延迟(约两倍平均网络单程时延,如 1 毫秒),免得两台同时开工白干一份。文章报告它能明显压低中位与 99.9 分位,集群同时跑着大规模排序作业争抢磁盘时也依然有效。
有些不均衡持续几秒到几分钟(机器变热、负载倾斜),可以跨请求慢慢调:
「够用就返回」(good enough):搜索不存在唯一正确答案,于是可把时间当成硬约束——到点就用已收到的结果合成答案、不等落后的叶子,必要时主动跳过非核心子系统(如拼写纠正、广告)抢时间,以少许结果质量换延迟可预测。金丝雀请求(canary request):一个诡异请求踩到从未走过的代码路径,可能让所有叶子一起崩溃或陷入病态慢,所以先发给一两台叶子试水、正常返回才向全部扇出——多花一跳延迟,换掉一次全军覆没。
这些技术多要求操作只读或幂等——重复读无害,重复转账则不行。幸运的是更新对尾延迟通常不那么敏感:许多更新可挪出关键路径异步做,很多服务能容忍短暂的不一致窗口;而强一致时用的多数派算法本就自带尾延迟容错,多数副本确认即提交。
这是观点文章,「结果」不是基准分数,而是三组生产系统里的证据:① 放大效应的算术与实测(1% → 63%;单叶子 99 分位约 10 毫秒 vs 等全部约 140 毫秒);② 对冲请求把 99.9 分位从 1800 毫秒降到 74 毫秒、额外请求仅 2%;③ 绑定请求在空闲与磁盘争抢两种条件下都明显压低中位与 99.9 分位。合起来支撑全文论点:尾延迟可测量、可预算,能用很小的冗余换很大的改善。
它首先改变了度量什么。此前服务性能习惯写「平均响应时间」;这篇文章让业界普遍接受:平均值几乎不含信息量,要盯 p99 / p99.9,并把它写进服务的 SLO(服务等级目标)。今天监控面板上那条 p99 曲线,思想源头就在这里。
其次是一整套落地机制:对冲/绑定请求成了标准配件(gRPC 内置对冲重试、Cassandra 的推测性重试、MapReduce 与 HDFS 系的推测执行同源);「延迟留观」在 Envoy 等服务网格里以离群点摘除(outlier detection)的形式存在;微分区是现代分片系统匀负载的常规做法;「够用就返回」是搜索、推荐、feed 的通行惯例。更宏观地,它和 Dapper(先能看见)一起把延迟工程确立成一门手艺:先测分布而非均值,再判断抖动来自源头还是放大,然后在「减小抖动」与「容忍抖动」里选配。
① 一句话:大规模扇出下响应时间取决于最慢的那个组件,单机偶发抖动被放大成常态卡顿;应当像做容错那样,做「尾延迟容错」的软件。
② 放大效应:单机 1% 概率超一秒、并行等 100 台全返回,则 63% 的请求超一秒;实测「等一个叶子」99 分位约 10 毫秒,「等全部」约 140 毫秒。
③ 抖动源头:资源争抢、后台守护进程、全局共享、维护活动、垃圾回收、多层排队、功耗降频——多是正常运行的副产品,无法根除。
④ 压小抖动:区分服务等级 + 高层短队列;长请求切片破队头阻塞;后台扰动同步化(对齐优于错开,因为你每次都要等所有机器)。
⑤ 请求内对策:对冲(超过预期延迟 95 分位再向第二副本补发,谁快用谁)与绑定请求(同时入两队、谁先开工就叫停另一台,吃掉排队时间差)。关键数字:读 1000 个键、10 毫秒后对冲,99.9 分位 1800 毫秒 → 74 毫秒,额外请求仅 2%。
⑥ 跨请求对策与检索特产:微分区、选择性复制(只给热分区加副本)、延迟留观(摘掉变慢的机器——少一台反而更快)、够用就返回、金丝雀请求;写操作因可异步、可容忍不一致、多数派提交而不那么急。
⑦ 局限:经验合集而非理论;用余量换稳定(高负载下重试可能反噬);依赖幂等与可达的取消;掩盖症状而非根治源头。