The Dapper Paper
复杂的分布式系统中涉及多个组件间的交互,这些组件往往由多个团队按照不同的规范或风格开发而成。如何快速地理解整个系统的行为,定位存在的性能问题至关重要,而Dapper就是解决这一痛点的基础设施。
原论文:Dapper, a Large-Scale Distributed Systems Tracing Infrastructure
所谓链路追踪,就是对分布式系统在各个组件、各台主机上的性能进行持续监控,让工程师在意识到问题时,能够更方便地把问题定位到某个服务或者某条链路上。要实现这一点,需要达到这些目标:
- 部署范围广:一个只能部署在少部分服务组件上的链路追踪系统是没有意义的,要精准定位问题,就要实现对分布式系统所有组件的监控,这要求设计出的监控工具对服务组件或硬件设备没有太多要求;
- 持续监控:你永远不可能预测到究竟什么时间哪项服务可能出现故障,而且在分布式系统中,很多问题是很难重现的,这就要求我们能够保证保留现场数据进行分析;
- 资源占用少:既然这套系统得部署在每个服务上,那么就必须要求它对原服务的性能影响微乎其微;
- 侵入性低:这样的系统最好不要求开发者对原有服务进行太大改造,如果它和服务代码过于耦合,就很可能出现故障。在一个快速迭代的项目里,开发者很难在完成功能模块的同时兼顾一个外来的监控系统;
- 可扩容:要能够适应未来系统的扩展。
Dapper 的分布式链路追踪

上图展示了一种典型的分布式系统信息流,为了回复用户请求,系统中的五个子模块都需要参与互动,涉及到复杂的组件间通信。一种思路是利用统计学思路,基于大量数据来推测子模块的关联情况;另一种思路是对每一次用户请求和与之相关的子模块通信做全局唯一标记,这样即使只有一次请求,也可以对整个系统进行分析,其缺点是需要对分布式系统进行干涉。Dapper 的开发者发现,在他们的生产环境中,所有应用都采用了一致的 RPC 库、线程模型和控制流,因此要做的改动仅限于一小部分公共库。
Trace 树、Spans、Annotations
结合之前的例子不难发现,一条信息链路可以被视为 RPC(或其它通信方式)嵌套组成的树形结构。 从前端 A 分出 B 和 C 两个分支,C 又会嵌套式地从 D 和 E 获取信息。 在 Dapper 中,这棵树的结点被称为 span,记录了该模块处理一条请求的始末时间等信息。


上面两张图分别展示了 trace 的树形结构和一个 span 所记录的详细信息。
除此以外,Dapper 支持开发者通过特定 API 给 trace 注入额外信息,这种信息不光支持字符串,还支持 kv 信息输入。
Dapper 是如何收集 trace 信息的

Dapper 的信息收集分三步走:
- 被改造过的公共库会将原始 trace 信息写入本机日志文件;
- Dapper 守护进程和生产机器会定期拉取这些日志文件;
- 最终 trace 信息会由 Dapper collector 写入 Bigtable 中。
在 Dapper 的设计里,一个 trace 所包含的 span 是不确定的,而 Bigtable(如前面的博客介绍的那样),正好支持稀疏的表格结构。作者称,从数据生产到落盘的平均耗时能控制在 15 秒以内,但仍有四分之一的情况下该时长会增长到数个小时。(考虑到这个过程中有多次磁盘写入、网络延迟,以及 Bigtable 本身的耗时)
开销控制
Dapper 运行时库的主要开销来自于 span 的创建、销毁,annotation 以及日志落盘。根 span 的创建比子 span 耗时会更长,因为此时还需要分配全局唯一 ID。 磁盘写入的耗时最大,但由于多个日志写入操作会被聚合起来,且写操作相对于应用来说是异步进行的,这部分开销被大大降低了。然而,对于高吞吐的应用来说,这种性能影响仍然是不能忽略的,特别是在每条请求都被追踪的情况下。
Dapper 的开销还与对请求的采样率有关。在早期,Dapper 在每 1024 次进程进行一次采样。在吞吐量高的网络应用中,这种较低的采样率仍然能够确保重要的事件被捕获到,但这种采样模式显然对吞吐量低的情况不友好。因此,在后续迭代中,Dapper 将改为在每单位时间进行采样,且能够配置采样率参数。
在尽量低的采样率的基础上,Dapper 还对最终落盘的 trace 数据进行二轮采样,以控制收集的数据规模。具体而言,同一个 trace 的 span 共享一个 trace ID,经这个 ID 计算的哈希值若低于某个阈值,则将之写入 Bigtable,反之则抛弃,这样一来所有同属于一个 trace 的 span 都会被保留,且只需要调整一个阈值参数,就可以控制整个 Dapper 对 Bigtable 的写入速率。