Architecture/分布式系统 · #observability#distributed-tracing#dapper#论文

Dapper 论文精读:Google 分布式追踪系统的设计取舍

2021.08.28 3 min 1.3k
// 目录 · contents

Dapper 是 Google 2010 年发表的一篇关于分布式追踪系统的论文,全名《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》。这篇论文不长,但信息密度很高,后来 Twitter 的 Zipkin、Uber 的 Jaeger 都直接受它影响。我在团队引入 Jaeger 之前认真读了一遍,做一些记录。

为什么需要分布式追踪?

论文开头说的很直白:现代互联网服务的一个请求可能经过数百个微服务,任何一个环节出问题都可能影响用户体验,但传统的日志和指标监控很难回答”这个慢请求慢在哪里”这个问题。

举个实际场景:用户投诉某个接口偶发超时,你看指标发现 P99 确实高,但 P50 正常。日志里有几百条信息但关联不起来。这时候如果有追踪系统,可以直接找到某次超时请求的完整链路,看出是下游哪个服务耗时异常。这是 Dapper 要解决的核心问题。

核心数据模型:Trace Tree

Dapper 的数据模型很简洁:

  • Trace:一次完整的请求链路,由全局唯一的 TraceId 标识
  • Span:链路中的一个操作单元,有开始时间和持续时间
  • Annotation:Span 上打的时间戳标记,如 cs(client send)、cr(client receive)、ss(server send)、sr(server receive)
1
2
3
4
5
6
7
8
9
10
Trace [TraceId=abc123]
├── Span: 前端请求 [SpanId=1]
│ ├── cs=0ms, cr=180ms
│ ├── Span: 用户服务 [SpanId=2, ParentSpanId=1]
│ │ ├── cs=5ms, cr=30ms
│ │ └── Span: 数据库查询 [SpanId=4, ParentSpanId=2]
│ │ └── cs=8ms, cr=25ms
│ └── Span: 订单服务 [SpanId=3, ParentSpanId=1]
│ └── cs=35ms, cr=170ms ← 慢在这里
└── 总耗时 180ms

每个 Span 通过 SpanId + ParentSpanId 组成树结构,还原出请求的调用关系和时序。

三个关键设计目标

论文花了大量篇幅讨论三个目标,以及为了满足这些目标做出的具体取舍。

1. 低开销(Low Overhead)

Google 的要求是性能损耗小到开发者不需要关心追踪开关。为此 Dapper 用了两个手段:

异步写入:Span 数据不走请求链路,单独由后台线程写入本地日志,再由 Dapper Daemon 异步上报到中央存储。这样即使追踪系统本身出了问题,也不影响业务。

采样(Sampling):这是核心权衡。全量追踪开销太大,Dapper 默认只采样 1/1024 的请求。论文里有个关键论断:高流量服务下,1/1024 的采样率已经足够发现大多数性能问题,因为慢请求的比例相对稳定,采样后依然能捕捉到。

采样还有个细节:采样决定在 trace 的入口(第一个 Span)就做出,然后通过上下文传播到所有下游。这样保证一条 trace 要么被完整采样,要么完全不采。不然会出现一条 trace 只有部分 Span 的情况,没有分析价值。

2. 对应用透明(Application-Level Transparency)

开发者不需要手动埋点,追踪逻辑封装在公共 RPC 库里,新服务接入只需要用标准的 RPC 库。

这一点在 Google 内部做得到,是因为他们控制了底层 RPC 框架。在实际落地时,如果团队用了多种 RPC 框架(gRPC、HTTP、消息队列),每种都需要单独的 instrumentation,透明度就没那么高了。

Jaeger 和 OpenTelemetry 后来的解法是通过 Java Agent 做字节码插桩,自动拦截常用框架(Dubbo、HttpClient、MySQL 驱动等),效果接近透明。

3. 可扩展性

Dapper 的存储用的是 Bigtable,按 TraceId 分区,每条 trace 数据保存两周。查询时按 TraceId 或者特定维度(服务名、接口名、耗时范围)检索。

论文提到他们每天收集约 1TB 的采样追踪数据,存储约 8 万亿个 Span。这个量级解释了为什么采样率和存储设计要这么精细。

从论文到实际落地

读完论文之后,我们选了 Jaeger(而不是 Zipkin)作为追踪系统,原因是: - Jaeger 是 Go 写的,资源占用小 - 原生支持 Cassandra 和 Elasticsearch 作为存储后端 - 对 Kubernetes 支持更好

接入过程中遇到的最大问题不是技术的,而是采样率配置

我们最开始把采样率设成了 100%(全量),结果 Jaeger 的 Collector 被打爆了,写入 Elasticsearch 的速度跟不上请求量,然后 Span 数据开始大量丢失。更糟糕的是,这个丢失是静默的,界面上 trace 还能查到,但很多 trace 缺少中间的 Span。

后来改为头部采样(1%)+ 对错误请求强制采样,正常查的时候 trace 完整了,出问题的请求也基本能抓到。这个策略和 Dapper 论文里描述的基本一致。

还有一个实践经验:追踪系统的价值在使用频率上。工具搭好之后,得推动团队在排查问题时真的去用它。我们起初遇到问题还是习惯先看日志,后来规定排查慢接口问题必须先拿追踪图看一眼,慢慢大家才真正用起来。

论文原文

Dapper, a Large-Scale Distributed Systems Tracing Infrastructure

推荐直接读原文,不长,大概 30 页,核心内容在第 2、3、4 节。

作者 · authorzt
发布 · date2021-08-28
篇幅 · length1.3k 字 · 3 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论