IT 论文精读 · PAPER 26
Sigelman 等 · Google · 技术报告 2010
2010 年,Google 公开了内部工具 Dapper:你在网页上敲一次搜索、点一次「购买」,背后其实有成百上千台机器接力干活。以前一旦这次请求变慢或出错,没人说得清「到底卡在哪台机器、哪个环节」。Dapper 给每一次请求发一张「全程行程单」,把它一路经过了哪些服务、每一站花了多久,全都串起来记下来。今天所有讲「可观测性」「分布式追踪」的系统——Zipkin、Jaeger、OpenTelemetry——都是照着 Dapper 的样子做的。
大公司的一次请求早就不是「一台服务器算完返回」那么简单了。你搜一个词,前端把它同时分发给几十个后端:一个查网页、一个查图片、一个查广告、一个查拼写纠正……每个后端又各自去问更下面的一串服务。这样一层层散开,一次请求能牵动上千台机器。问题来了:整件事慢了 100 毫秒,是谁拖的后腿?负责前端的工程师看不到后端内部,负责某个后端的人也不知道自己是被谁、在整条链路的哪一环调用的。每个人只掌握一小块拼图,没有一个人能看到一次请求的全貌——这在故障排查时是致命的。
Dapper 的点子是:给每一次进来的请求发一个独一无二的编号,然后让这个编号跟着请求跑遍全程——请求传到哪台机器,编号就带到哪台机器。每一站在开始和结束时都记一笔「我是几点几分接到的、几点几分干完的、我是被谁叫来的」。事后把同一个编号的所有记录收集到一起,就能拼出一棵完整的调用树:谁调了谁、每段花了多久、慢在哪一枝,一目了然。就像给一个跨越十几个部门流转的申请单,全程贴一张统一的追踪条码。
关键的巧思有两个。一是工程师几乎不用改自己的代码:Google 全公司的服务都用同一套底层通信库,Dapper 只在这套公共库里埋了记录的代码,于是所有服务「自动」就被追踪上了。二是不全记、只抽样:请求量太大,条条都记会拖慢系统、也存不下,所以 Dapper 每一千多次请求才完整记一次——对流量巨大的系统,抽这么点样本已经足够看清规律,代价却小到可以忽略。
有了全程行程单,很多以前靠猜的事变得能直接看:一次慢请求,点开它的调用树,就知道是某个下游服务卡了、还是网络等待久了;想知道「我这个服务到底依赖了哪些别人的服务」,把海量行程单一汇总就画得出依赖地图;追查一个跨十几个团队的诡异 bug,不用再挨个团队开会对时间线,一张追踪图摆出来,责任落到哪一段清清楚楚。它把「分布式系统像个黑箱」这件事,变成了「能一层层点开看」。
说句诚实的:抽样是把双刃剑。对海量流量它很管用,但如果你要查的正是那种一万次才出一回的罕见故障,那一次很可能没被抽中、没留下记录——后来的追踪系统为此补了「先全记、发现异常再决定留哪条」的办法。
给每次请求发一个全程带着走的编号,在公共通信库里自动记下它经过的每一站与耗时,事后拼成一棵调用树——工程师不改代码、靠抽样几乎零开销,就能看清一次请求在成千上万台机器间到底走了哪条路、慢在哪里。这套「trace / span / 上下文传递」的模型,成了今天所有分布式追踪与可观测性系统的蓝本。
想看调用树的结构图、采集流水线和开销数字? → 切到精读版
Dapper 是 Google 内部的分布式追踪(distributed tracing)基础设施:给每次请求分配一个全局 trace id 并沿调用链传递,把每段工作记成一个 span(带父子关系与时间戳),事后拼成一棵完整的调用树。靠三条设计原则——对应用透明、开销极低、可全公司规模部署(把埋点做进公共 RPC/线程库 + 抽样采集),它第一次让工程师能从「单次请求」的视角看清一个横跨上千台机器的系统,成了 Zipkin、Jaeger、OpenTelemetry 乃至整个「可观测性」运动的蓝本。
作者是 Benjamin Sigelman、Luiz André Barroso(Google 基础设施的核心人物之一)、Mike Burrows(Chubby 的作者,本站 Paper 20)等,2010 年以 Google 技术报告形式发表。它上承学术界的分布式追踪探索——尤其是 X-Trace(2007)、Magpie、Pinpoint 等把「请求元数据沿链路传递」的思路;但 Dapper 第一次把它做进一家超大规模公司的生产环境、持续运行数年,并诚实交代了工程权衡。下启工业界几乎所有开源追踪系统:Twitter 的 Zipkin、Uber 的 Jaeger,以及统一标准 OpenTracing / OpenTelemetry。
在 Google,一次普通的网页搜索背后可能牵动成百上千台机器上的几十种服务:前端把查询扇出给网页索引、图片、广告、拼写纠正等一排后端,每个后端又各自向更下层的服务发起 RPC。整个系统是许多小团队各自开发、独立部署的服务拼起来的,没有任何一个工程师了解全部环节。
于是当一次请求变慢或出错,排查就成了噩梦:前端工程师只知道「总共花了 300 毫秒」,却看不进任何一个后端内部;某个后端的负责人能看到自己慢了,却不知道自己处在哪条请求链的哪一环、是被谁调用的。每个人手里只有一小块拼图,谁都拼不出一次请求的完整因果链。传统的单机日志、性能计数器,都是「按机器 / 按服务」组织的,唯独缺了最关键的一维:「按单次请求」把跨越所有机器的工作串起来。
已有两条思路。一是黑盒法(black-box):不改代码,纯靠统计各服务的消息日志、用算法去推断谁调了谁——好处是零侵入,坏处是推断不准、需要海量数据。二是标注法(annotation-based):在代码里显式打上追踪标记、让请求带着一个 ID 走——因果关系精确,代价是要改代码。Dapper 选了标注法,但用一个巧劲把「要改代码」这个代价几乎抹平(见下)。
Dapper 把目标压成三条硬约束,后面所有设计都是为满足它们:①对应用透明——工程师不该为了被追踪去改自己的业务代码;②开销极低——追踪本身不能拖慢线上服务,否则没人敢开;③可全公司规模部署——要能覆盖成千上万台机器、常年运行。这三条里最难的是「透明」和「低开销」,它们直接决定了下面两个关键手法。
Dapper 的数据模型很简洁。一次请求的全部工作叫一个 trace,对应一个全局唯一的 trace id。这棵树上的每一段工作(通常是一次 RPC 调用,比如「前端调广告服务」)是一个 span。每个 span 记着自己的 span id、父 span 的 id(parent id),以及所属的 trace id——靠「谁的父亲是谁」,散落各处的 span 就能拼回一棵调用树。
每个 span 内部还带若干标注(annotation):一类是自动打的时间戳,标准的四个是 cs(client send,调用方发出请求)、sr(server receive,被调方收到)、ss(server send,被调方返回)、cr(client receive,调用方收到结果)。有了这四个点,Dapper 就能算出网络在途时间(sr−cs、cr−ss)和服务端处理时间(ss−sr),把一段慢究竟慢在「网络」还是「对方干活」分得清清楚楚。另一类是工程师随手加的业务标注(如「查询命中缓存」),把应用语义也带进追踪里。
怎么做到工程师不改业务代码就被追踪?关键在于 Google 全公司共用同一套底层库——统一的线程管理、异步控制流、以及 RPC 通信框架。Dapper 只在这几个公共库里埋了追踪代码:请求进来时创建根 span、发起 RPC 时自动新建子 span 并把 trace id 塞进 RPC 消息头带给下游、被调方收到后接着往下传。trace id 在一台机器内靠线程本地存储随线程流动,跨机器时搭 RPC 的顺风车传递。业务工程师什么都不用做,就「免费」被追踪上了——这就是「对应用透明」的全部魔法,也解释了为什么它在 Google 能一键铺满全公司,却难以照搬到库不统一的异构环境。
全量追踪有两笔账:一是在请求路径上创建 span、打标注的运行时开销,二是把海量追踪数据写盘、传输、存储的采集开销。后者才是大头。Dapper 用两招把两笔账都压到可忽略:
其一,抽样。不是每次请求都完整追踪,而是按一个很低的概率抽——生产环境常用约 1/1024(一千多次请求才留一次完整记录)。对流量巨大的服务,这么点样本已足够统计出稳定的延迟分布和调用结构,而运行时开销随之几乎归零。(论文里 span 的创建/销毁只是百纳秒量级的操作,再乘上千分之一的抽样,摊到每次请求上微乎其微。)
其二,带外异步采集。被追踪的数据不在请求的关键路径上处理:应用先把 span 写到本地日志文件,再由一个独立的 Dapper 守护进程事后异步把它们捞走、经收集器汇总,最终一次 trace 存成 BigTable 里的一行(每个 span 是一列)。因为收集发生在请求返回之后、且走独立通道,它完全不拖慢线上请求;论文报告这套采集只占用极小的 CPU 与网络(远低于 1%)。工程师则通过一套查询接口(DAPI)和 Web 界面去检索、可视化这些 trace。
Dapper 最有说服力的「结果」不是某个准确率数字,而是它真的在 Google 生产环境跑了起来、还常年好用。论文报告:追踪已覆盖公司几乎所有系统,采集的追踪数据规模巨大;运行时开销小到可忽略——单个 span 的创建与标注是百纳秒级操作,配合抽样后对线上服务的延迟与吞吐几乎无影响;采集守护进程只占极少的 CPU 和不到 1% 的网络。围绕它建起的查询接口 DAPI 与追踪可视化,被大量工程师日常用于性能分析(定位关键路径、找拖慢尾延迟的服务)、推断服务依赖关系、排查跨团队故障。论文用一系列真实使用案例证明:一个透明、抽样、带外的追踪系统,在超大规模下既跑得动、又真有用。
Dapper 定义了分布式追踪的标准范式。它提出的 trace / span / 上下文传递(context propagation)三件套,几乎被后来每一个追踪系统原样继承:Twitter 开源的 Zipkin 直接照 Dapper 建,Uber 的 Jaeger 亦然,再到试图统一各家的 OpenTracing、最终收敛成 CNCF 的 OpenTelemetry 标准——今天你在任何微服务架构里看到的 trace id、span、cs/sr/ss/cr 式的时间标注,血脉都来自这篇报告。更宏观地说,它和「日志、指标」一起,把「可观测性」从一句口号变成了工程实践的三大支柱之一,是现代云原生、微服务运维的思想地基。
1/1024 对高流量服务够用,但要排查「一万次才出一回」的罕见异常或低流量服务,那条关键的 trace 很可能根本没被抽中。Dapper 后来补了「按需强制追踪某请求」的机制;再后来的系统(如基于尾采样 tail-based sampling 的方案)则改成「先全记、发现慢或错再决定留哪条」来针对性弥补。① 一句话:给每次请求发一个全局 trace id 沿调用链传递,把每段工作记成带父子关系的 span,拼成一棵调用树,从「单次请求」视角看清跨上千台机器的系统。
② 痛点:一次请求扇出到成百上千台机器,单机日志按机器组织,没人能拼出一次请求的完整因果链——慢了、错了都不知道卡在哪。
③ 三原则:对应用透明、开销极低、可全公司规模部署——后面所有设计都为满足这三条。
④ 数据模型:trace(一次请求)= span 树;span 带 trace id / span id / parent id + cs/sr/ss/cr 四个时间戳,把耗时拆成网络在途 vs 服务端处理。
⑤ 透明的秘诀:把埋点做进全公司共用的 RPC / 线程 / 控制流公共库,trace id 靠线程本地存储 + RPC 消息头自动传递,业务代码零改动。
⑥ 低开销的秘诀:抽样(约 1/1024)压运行时开销 + 带外异步采集(写本地日志→守护进程→BigTable,一 trace 一行)不占请求关键路径。
⑦ 影响:定义了 trace/span/上下文传递范式,直接催生 Zipkin、Jaeger、OpenTelemetry,是「可观测性」三支柱之一的地基。
⑧ 局限:抽样漏掉罕见事件;透明依赖同构基础设施;因果模型偏 RPC 树、对异步/批处理模糊;看聚合淹没离群;追踪不等于自动诊断根因。