Dapper
Dapper 是 Google 生产环境的分布式追踪基础设施。它回答的问题很具体:一次用户请求在跨越几千台机器、几十个服务之后,时间花在了哪一层、哪一台机器上。
典型的例子是 universal search。一个前端服务可能把一次网页查询分发给几百个查询服务器(各自在自己的索引分片里搜),同时再发给一批处理广告、拼写检查、图片、视频、新闻的子系统,结果被有选择地合并进结果页。处理一次这样的查询,总共可能涉及数千台机器和许多不同的服务。 这些服务对延迟敏感,而延迟可能来自任何一个子系统的劣化。只看总延迟的工程师知道出了问题,却猜不出是哪个服务、也不知道它为什么表现差,原因有三条:
- 他并不精确知道哪些服务在用 —— 每周都有新服务被加进来或改掉,既为了加用户可见的功能,也为了改进性能或安全;
- 他不是每个服务的专家 —— 每个服务由不同团队构建与维护;
- 服务与机器被许多客户端同时共享,所以一个性能异常可能来自另一个应用 —— 前端处理的请求类型很多,而 Bigtable 这样的存储系统要在跨多个应用共享的前提下才划算。
由此得到两个前提要求与三个设计目标。
两个前提要求与三个设计目标
两个要求:
| 要求 | 理由 |
|---|---|
| 无处不在的部署 | 只要系统里有一小块没被监控,追踪基础设施的用处就会严重受损 |
| 持续开启的监控 | 异常或值得注意的系统行为往往难以复现,甚至根本复现不了 |
三个设计目标:
- 低开销 —— 追踪系统对被监控服务的性能影响应可忽略。在一些高度优化的服务里,连很小的监控开销都很容易被注意到,进而可能迫使部署团队把它关掉。
- 应用级透明 —— 程序员不应该需要感知追踪系统的存在。一个依赖应用开发者主动配合才能工作的追踪基础设施极其脆弱,常因埋点 bug 或遗漏而失效,从而违背"无处不在"这条要求;在快节奏的开发环境里这一点尤其重要。
- 可伸缩 —— 至少要在未来几年里撑住 Google 服务与集群的规模。
附加目标:追踪数据在生成后能尽快供分析,理想是一分钟以内。分析几小时前的旧数据仍有价值,但拿到新鲜信息才能更快响应生产异常。
应用级透明是其中最难的一项目标,达成方式是把核心埋点限制在一小撮无处不在的线程、控制流与 RPC 库代码里。可伸缩与低开销则借助自适应采样达成(见后)。
Dapper 有时足以让开发者定位一处性能异常的来源,但它不打算取代所有其他工具。实际用法通常是:Dapper 给出的系统级数据先把调查聚焦,再由其他工具在本地深入。
数据模型:trace tree、span 与 annotation
把"属于同一个发起者"的记录聚到一起,此前有两类方案:
| 方案 | 做法 | 代价 |
|---|---|---|
| 黑盒(black-box) | 假设除消息记录外没有任何额外信息,用统计回归推断归属 | 更可移植,但因为依赖统计推断,需要多得多的数据才能达到足够准确度 |
| 基于注解(annotation-based) | 由应用或中间件显式给每条记录打上把消息关联回原始请求的全局标识 | 需要埋点 |
Dapper 走的是基于注解的路线,但在自家环境里做到几乎透明 —— 因为所有应用都用同一套线程模型、控制流与 RPC 系统,于是埋点可以限制在一小组公共库里。
数据模型由三样东西构成:
- trace tree —— 一个 trace 是一棵树;树节点是基本工作单元,叫 span;边表示一个 span 与它的父 span 之间的因果关系。
- span —— 脱离整棵树看,它本身就是一份带时间戳记录的简单日志,记录该 span 的起止时间、任何 RPC 计时数据、以及零或多个应用特定的 annotation。每个 span 还带一个可读的名字、一个 span id 与一个 parent id,用来重建同一条 trace 内各 span 之间的因果关系。没有 parent id 的 span 叫 root span;同一条 trace 的所有 span 共享一个 trace id。这些 id 全都是概率唯一的 64 位整数。
- annotation —— 见下节。
典型 trace 里每个 RPC 对应一个 span,每加一层基础设施就给 trace 树加一层深度。
一个 span 可以包含来自多台主机的信息:事实上每个 RPC span 都同时含客户端与服务端进程的 annotation,所以双主机 span 是最常见的一类。因为客户端与服务端的时间戳来自不同机器,必须留意时钟偏斜。分析工具利用了一个事实来给服务端的时间戳定界:RPC 客户端总是先发出请求、服务端才收到;响应方向同理反过来 —— 于是服务端那侧的时间戳有了上下界。
核心数据模型并不局限在自家 RPC 框架上:Gmail 的 SMTP 会话、来自外部世界的 HTTP 请求、发往 SQL 服务器的出站查询都会被追踪。
三个埋点位置
Dapper 能跟随分布式控制路径、且几乎不需要应用开发者介入,靠的是几乎完全依赖对少数公共库的埋点:
- 线程局部存储 —— 当一个线程处理被追踪的控制路径时,Dapper 把一份 trace context(一个小而容易拷贝的容器,装 span 属性如 trace id 与 span id)挂到线程局部存储上。
- 公共控制流库 —— 当计算被延迟或转成异步时,多数开发者用同一个控制流库构造回调、并投进线程池或其他执行器。Dapper 保证这类回调都存下创建者的 trace context,并在回调被调用时把它关联回合适的线程。 于是用于重建 trace 的 id 能透明地跟随异步控制路径。
- RPC 框架 —— 几乎所有进程间通信都建在同一套 RPC 框架上(同时有 C++ 与 Java 绑定)。这套框架被埋点,于是所有 RPC 都被 span 包住,span id 与 trace id 随被追踪的 RPC 从客户端传到服务端。对 Google 这类 RPC 密集的系统,这是必需的埋点位置。
Dapper 的追踪数据与语言无关:生产环境里很多 trace 同时包含 C++ 与 Java 进程写的数据。
annotation:应用侧的可选增强
核心埋点已足以推出复杂分布式系统的详细 trace,让未经修改的应用也能用上 Dapper 的核心能力。在此之外,开发者还可以用简单 API 给 trace 加带时间戳的 annotation:
// C++
const string& request = ...;
if (HitCache())
TRACEPRINTF("cache hit for %s", request.c_str());
else
TRACEPRINTF("cache miss for %s", request.c_str());// Java
Tracer t = Tracer.getCurrentTracer();
String request = ...;
if (hitCache())
t.record("cache hit for " + request);
else
t.record("cache miss for " + request);两条约束值得记:
- 每个 span 上的 annotation 总量有一个可配置的上限,用来防止意外的过度记录;
- 应用级 annotation 无论应用怎么用,都无法挤掉结构性的 span 或 RPC 信息。
除纯文本 annotation,Dapper 还支持一类 key-value annotation,用来维护计数器、记录二进制消息、以及在一个进程内随被追踪的请求传递任意用户数据。这类 annotation 的用途是在分布式 trace 的上下文里定义应用特定的等价类。
采样:运行时自适应 + 收集期二次采样
低开销是 Dapper 的核心设计目标:服务运营者不会愿意部署一个价值还没被证明、却对性能有明显影响的新工具;同时也不希望开发者因为担心额外开销而不敢用 annotation API。除把基础埋点开销做到尽可能小之外,控制开销的进一步手段是只记录全部 trace 里的一部分。
运行时采样:从固定概率改为按速率自适应
第一版对所有进程使用统一的采样概率,平均每 1024 个候选采 1 条。这个简单方案对高吞吐在线服务有效 —— 关注的事件绝大多数仍会以足够高的频率出现从而被采到。但低流量的工作负载在这个采样率下会漏掉重要事件,而它们本来能承受更高的采样率;为这类系统覆盖默认采样率正是 Dapper 想避免的那种人工介入。
于是在部署的是自适应采样:它不由统一的采样概率参数化,而由期望的每单位时间采样 trace 数参数化。于是低流量工作负载自动提高采样率,极高流量的自动降低采样率以把开销压在控制内。实际使用的采样概率会随 trace 一起记录下来,便于围绕 Dapper 数据分析工具准确核算 trace 频率。
收集期采样:按 trace id 整条取舍
运行时采样是为了让应用侧开销不易察觉;另一个约束来自仓库侧 —— Dapper 团队还要控制写进中央仓库的总数据量:
- 生产集群目前每天产生超过 1 TB 的采样 trace 数据;
- 用户希望 trace 数据在最初被记录后至少保留两周;
- 高比例采样会把收集器逼到 Dapper Bigtable 仓库写入吞吐上限的不适位置。
做法是在收集系统里再加一层采样,并且利用"一条 trace 的所有 span 虽然可能散布在几千台不同主机上、却共享同一个 trace id"这个事实:
对收集系统里见到的每个 span,把关联的 trace id 哈希成一个标量
。若 小于收集采样系数,保留并写入 Bigtable;否则丢弃。
因为采样决策只依赖 trace id,采到或丢掉的都是整条 trace,而不是 trace 里的单个 span。这个配置项的实际收益是:只要改一个配置文件里的参数,就能立刻调整全局写入速率,收集管线的运维因此简单很多。
为什么不能只留一个采样参数:无法快速调整所有已部署二进制里的运行时采样配置。所以选的策略是让运行时采样率产出略多于能写进仓库的量,再用收集期的第二个系数把写速率卡住 —— 需要放大或缩小全局覆盖与写速率时,只改这一处即可。
采样到底会不会毁掉分析
对高吞吐服务,采样率低到 0.01% 也不妨碍大多数重要分析:如果一种值得注意的执行模式会在这种系统里出现,它就会出现成千上万次。反过来,每秒只有几十次请求而不是几万次的低流量服务完全负担得起追踪每个请求 —— 这正是转向自适应采样率的动因。
收集管线:三段与 out-of-band
收集与记录是三段:
- span 数据被写入本地日志文件;
- 由每台生产主机上的 Dapper daemon 与收集基础设施拉取;
- 写入若干个区域性的 Dapper Bigtable 仓库。
一条 trace 被放成 Bigtable 的一行,每一列对应一个 span。 Bigtable 对稀疏表布局的支持在这里很有用 —— 单条 trace 的 span 数可以是任意多。
延迟数字:trace 数据收集的中位延迟(从被埋点的应用二进制到中央仓库)不到 15 秒;98 分位本身随时间呈双峰 —— 约 75% 的时间里 98 分位不到两分钟,另外约 25% 的时间里它会涨到几小时。
为什么采集走带外
Dapper 的记录与采集是**带外(out-of-band)**的,即不把 trace 数据放进 RPC 响应头里回传。两个原因互不相关:
① 带内采集会影响应用的网络动态。 在 Google 的许多大型系统里,含几千个 span 的 trace 并不罕见;而 RPC 响应 —— 即便处在这样的大 trace 靠近根的位置 —— 往往还不到 10 KB。这种情况下带内的 Dapper trace 数据会淹没应用数据、并扭曲后续分析的结果。
② 带内采集假设所有 RPC 完美嵌套。 但很多中间件系统会在自己所有后端返回最终结果之前就把结果返回给调用者,带内采集无法处理这类非嵌套的分布式执行模式。
安全与隐私
记录一部分 RPC 载荷信息本会让 trace 更丰富 —— 分析工具或许能从载荷数据里找出解释性能异常的模式。但载荷数据在一些情形下含有不应向未授权的内部用户(包括做性能调试的工程师)披露的信息。安全与隐私是不可谈判的,所以 Dapper 存 RPC 方法的名字,不记录任何载荷数据。载荷替换为应用级 annotation 提供的 opt-in 机制:开发者自行决定把哪些数据关联到 span 上供日后分析。
还有两类设计者未曾预料的安全收益:
- 通过追踪公开的安全协议参数,可以用来监控应用是否满足了正确的认证或加密等级;
- 可以用来验证基于策略的系统隔离是否如预期执行 —— 例如带敏感数据的应用没有与非授权的系统组件交互。
这类度量比源码审计给出的保证更强。
部署覆盖与运行时库
Dapper 已作为生产追踪系统运行两年多。覆盖可以从两个维度看:
- 能产生 trace 的生产进程比例(即链接了带 Dapper 埋点的运行时库的进程)—— 由于不产生任何追踪信息的进程对 Dapper 不可见,精确比例难以确定,但考虑到带埋点的库无处不在,估计几乎每个 Google 生产进程都支持追踪;
- 运行 Dapper 采集 daemon 的生产机比例 —— daemon 是基础机器镜像的一部分,因而几乎存在于 Google 的每台服务器上。
无法正确跟随控制路径的情况确实存在,通常源于使用了非标准的控制流原语,或源自 Dapper 把因果关系错误地归到无关事件上。为此提供了一个简单的库让开发者手工控制 trace 传播作为兜底。目前有 40 个 C++ 应用与 33 个 Java 应用需要手工传播 —— 相对以千计的总数是很小的一部分。 另有极少数程序使用未被埋点的通信库(例如裸 TCP socket 或 SOAP RPC),因此不支持追踪。
追踪可以作为生产安全措施被关闭。 事实上它在早期默认是关的,直到团队对它的稳定性与低开销建立起信心。Dapper 团队会不定期审计那些被服务所有者关掉追踪的配置变更:这类变更很少,且通常出于对监控开销的担心,而到目前为止所有这些变更在进一步调查与实测实际开销之后都被回退了 —— 实际开销是微不足道的。
运行时库的体量很小,这是它能被链进海量应用的前提:埋点部分 C++ 不到 1000 行代码、Java 不到 800 行;key-value annotation 的实现再加 500 行。抛开轻量不谈,这段代码还必须稳定健壮 —— 它被链进数量巨大的应用,维护与修 bug 都很困难。
annotation 的实际用法
程序员用应用特定 annotation 的方式主要有两种:当成一种分布式调试日志文件,或者按某个应用特定特征给 trace 分类。后者的例子是:所有 Bigtable 请求都带上所访问的表名。
- 目前 70% 的 Dapper span 与 90% 的 Dapper trace 至少含一条应用指定的 annotation;
- 41 个 Java 应用与 68 个 C++ 应用为了更好理解服务内部的 span 内活动而加了自定义 annotation;
- 采用 annotation API 的 Java 开发者迄今为止每个 span 打的 annotation 比 C++ 同行多。一个推测是他们负载更靠近终端用户 —— 这类应用常处理更杂的请求、控制路径相对复杂。
开销量化
开销分三块:生成、收集、对生产负载的实际影响。生成是其中最关键的一段,因为收集与分析在紧急情况下更容易被关掉。
生成开销(2.2 GHz x86 服务器上实测)
| 操作 | 平均耗时 |
|---|---|
| 创建并销毁 root span | 204 ns |
| 创建并销毁非 root span | 176 ns |
| 在一个未被采样的 span 上加 annotation | 9 ns(一次线程局部查找) |
| 在一个被采样的 span 上加字符串字面量 annotation | 40 ns |
root span 与非 root span 的差值,就是为 root span 分配一个全局唯一 trace id 的成本。
对本地磁盘的写入是运行时库里最贵的操作,但它的可见开销小得多,因为每次磁盘写会合并多次日志文件的写操作、并且相对被追踪的应用异步执行。即便如此,日志写活动仍可能对高吞吐应用的性能产生可感知的影响,尤其是在所有请求都被追踪的时候。
收集开销
在一个不现实地重的压测基准下,Dapper daemon 进程的最坏 CPU 使用率如下:
| 进程数 | 每进程数据速率 | daemon CPU 使用(单核占比) |
|---|---|---|
| 25 | 10K/s | 0.125% |
| 10 | 200K/s | 0.267% |
| 50 | 2K/s | 0.130% |
daemon 从不使用超过一个核的 0.3%,内存占用也很小(落在堆碎片的噪声范围内)。此外它被限制在内核调度器里尽可能低的优先级,以应对高负载主机上的 CPU 争用。
网络侧:仓库里每个 span 平均只对应 426 字节。作为被监控应用网络活动的一小部分,Dapper 的 trace 数据采集占 Google 生产环境网络流量的不到 0.01%。
对生产负载的影响(Web 搜索集群)
以采样比例递减、观察平均延迟与吞吐的变化:
| 采样频率 | 平均延迟变化 |
|---|---|
| 1/1 | +16% |
| 1/2 | +9% |
| 1/4 | +6% |
| 1/8 | +4% |
| 1/16 | +2% |
| 1/1024 | 0% |
结论有三层:
- 吞吐受到的影响不显著(延迟与吞吐的测量误差分别是 2.5% 与 0.15%);
- 但要避免可察觉的延迟劣化,采样确实是必需的;
- 采样频率低于 1/16 的那些档位,延迟与吞吐的代价都落在实验误差之内。
实践中,对高吞吐服务,采样率低到 1/1024 时 trace 数据量仍然足够。把基线开销压到极低很重要,因为它给应用留出了使用全部 annotation API 而不必担心性能惩罚的余量。
降低采样频率还有一个额外收益:数据在被垃圾回收前能在主机本地磁盘上留存更久,给收集基础设施更多弹性。
DAPI:三种访问路径与一个索引的取舍
DAPI(Dapper Depot API) 提供对区域仓库(Depot)里原始 trace 记录的直接访问。DAPI 与 trace 仓库是一起设计的,目标是给仓库里的原始数据暴露一个干净直观的接口。
三种访问方式:
| 方式 | 机制 |
|---|---|
| 按 trace id 访问 | 给定全局唯一 trace id,按需加载任何一条 trace |
| 批量访问 | 借 MapReduce 并行访问数十亿条 trace —— 用户覆盖一个只接收一个 trace 参数的虚函数,框架在用户指定的时间窗内对每条收集到的 trace 调用它一次 |
| 带索引访问 | 仓库只支持一个索引,映射"常用被查询的 trace 特征 → 不同的 trace"。因为 trace id 是伪随机分配的,这是快速取到某个特定服务或某台主机相关 trace 的最好的办法 |
三种方式最终都落到具体的 trace 记录上。因为 trace 被建模为 span 的树,Trace 数据结构就是一个可遍历的 Span 结构树;span 常对应 RPC 调用,此时 RPC 计时信息可用;带时间戳的应用 annotation 也通过 span 结构访问。
索引的选择是 DAPI 设计里最难的一环,因为成本很可观:索引所需的压缩存储只比 trace 数据本身少 26%。最初部署了两个索引 —— 一个按主机、一个按服务名,但主机维度的索引没有获得足够的使用兴趣来证明它的存储成本:用户对单台机器感兴趣时,同时也在关注某个特定服务。于是两者被合并成一个复合索引,可按服务名、主机名、时间戳(按此顺序)查找。
内部使用分三类:
- 3 个常驻的在线 Web 应用;
- 8 个按需从命令行运行的、维护良好的分析工具;
- 约 15–20 个一次性的分析工具 —— 这类难以统计,因为开发者可以构建、运行并放弃它们而不被 Dapper 团队知道。
用户界面与使用量
多数 Dapper 使用发生在交互式 Web UI 里。典型工作流有五步:
- 用户描述关心的服务与时间窗,以及区分 trace 模式所需的任何信息(例如 span 名字),并指定一个与调查最相关的成本指标(例如服务延迟);
- 出现一张大表,列出该服务所有分布式执行模式的性能摘要,用户可任意排序并挑一个细看;
- 选中某个执行模式后,给出它的图形化展示,被考察的服务高亮在中间;
- 对第 1 步选定的成本指标空间分桶,给出该指标上的频率直方图(例子里是近似对数正态的延迟分布),并列出落在不同区间的具体示例 trace;
- trace 检视视图:顶部是全局时间线,子树可交互展开与折叠,连续层级的分布式 trace 树用嵌套彩色矩形表示;每个 RPC span 进一步拆成在服务进程里花的时间(绿)与花在网络上时间(蓝)。用户 annotation 默认不显示,但可以按 span 有选择地加入全局时间线。
面向实时数据的模式下,UI 能直接与每台生产机器上的 Dapper daemon 通信:此时看不到上面那种系统级图,但仍能按延迟或网络特征挑出单条 trace,数据在实时后的几秒内就可用。
使用量:典型工作日约 200 个不同的 Google 工程师使用 Dapper UI,一周内约 750–1000 个不同用户,这个数量级按月稳定(模掉新功能的内部通告)。用户把具体 trace 的链接发给同事很常见,这会带来大量一次性的短时访问。
六类用例
Dapper 在 Google 被广泛使用,既有直接通过 UI 的,也有间接通过编程 API 或建在这些 API 之上的应用的。下面按"基础用法向量"归成六类。
① 开发期使用:AdWords 的 Ads Review 重写
Ads Review 建在一个关键词定向条件与对应文本广告的大型数据库上,新增或修改关键词与广告时要检查是否符合服务政策条款(如不当措辞),由自动化审核系统提效。该团队把 Dapper 从最初的原型阶段一直用到上线与维护,四条收益:
| 维度 | Dapper 起了什么作用 |
|---|---|
| 性能 | 对照请求延迟目标追踪进展、定位易得的优化机会;识别关键路径上不必要的串行请求 —— 这些请求往往来自开发者自己没写的子系统,从而促成后续修复 |
| 正确性 | Ads Review 围绕一个大数据库系统,它同时有只读副本(访问便宜)与读写主库(访问昂贵)。用 Dapper 识别出一批本可发往副本却发往主库的查询;现在可以对直接访问主库的情况做核算,并保证重要的系统不变量 |
| 理解 | Ads Review 的查询扇出到多种系统,包括 BigTable、上述数据库、一个多维索引服务、以及各种 C++ 与 Java 后端。用 trace 评估总查询成本,并促成一次重新设计操作、以最小化对各系统依赖的负载 |
| 测试 | 新代码发布要走 Dapper trace QA 流程,验证系统行为与性能正确;这个流程在 Ads Review 自身代码与支撑库里都发现过一些问题 |
该团队还大量使用 annotation API:用 Guice 这个开源 AOP 框架把重要软件组件标成 @Traced,并给重要子例程的输入输出规模、状态消息、以及其他本来要写进日志文件的调试信息打 annotation。
该团队估计延迟数字改善了约两个数量级。 也有两处 Dapper 不够用:他们希望在交互时间内搜索自己的全部 trace annotation,实际只能跑自定义 MapReduce 或手工看单条 trace;另外 Google 还有其他集中收集通用调试日志的系统,把其中大量数据与 Dapper 仓库整合起来并不平凡。
② 与异常监控集成
Google 有一个持续收集并集中各运行进程异常报告的服务。若异常发生在一条被采样的 Dapper trace 的上下文里,对应的 trace id 与 span id 会作为元数据出现在异常报告里,异常监控服务的前端则提供从具体报告到对应分布式 trace 的链接。Ads Review 团队用这个能力理解异常监控服务发现的 bug 的更大取证语境。这条集成也说明了平台的通用做法:围绕简单唯一 id 导出接口,让 Dapper 能相对容易地接入其他事件监控系统。
③ 处理长尾延迟
debug universal search 这类服务非常困难 —— 涉及的除了几十个子系统,还有几十个工程团队。即便最好、最有经验的工程师也会经常猜错端到端性能差的根因,而 Dapper 能提供事实、把许多重要的性能问题给出确定答案。
一位做长尾延迟调试的工程师写了一个小库,从 DAPI 的 Trace 对象推断分层关键路径,再用这些关键路径结构诊断问题、并给 universal search 的改进排优先级。三项发现:
- 关键路径上瞬时的网络性能退化不影响系统吞吐,但会对离群延迟产生深远影响 —— 大多数慢的 universal search trace 都在关键路径上经历过网络退化;
- 存在许多有问题且昂贵的查询模式,源自服务之间非预期的交互。一旦被识别出来,修正往往容易;但在有 Dapper 之前,识别本身很难;
- 从 Dapper 之外的安全日志仓库取得常见查询,利用 Dapper 的唯一 trace id 与 Dapper 仓库做 join,据此为 universal search 里每个子系统建立"对它而言慢的示例查询"列表。
④ 推断服务依赖
任意时刻,Google 的一个典型计算集群里都驻留着几千个逻辑 job(执行共同功能的一组进程);集群之间也相互依赖。因为 job 之间的依赖关系动态变化,无法只从配置信息推出全部服务间依赖,但公司里许多流程需要准确的服务依赖信息,才能定位瓶颈、规划服务迁移等。Service Dependencies 项目用 trace annotation 与 DAPI 的 MapReduce 接口把依赖判定自动化:
- 能推断单个 job 之间的依赖;
- 也能推断它们对共享软件基础设施的依赖 —— 例如所有 Bigtable 操作都带表名,于是能自动推断出在多种服务粒度上对具名资源的依赖。
⑤ 区分不同服务的网络用量
Google 在网络设施上投入很大。网络运维长期能拿到单台硬件的监控信息,也有自建工具与仪表盘看全局网络利用率,但真出问题时,缺少能把网络负载归因到应用层元凶的工具。
Dapper 并非为链路级监控设计,却很适合做集群间网络活动的应用层分析。 用它建起一个持续更新的控制台,显示集群间流量里最活跃的应用层端点;更进一步,能指向这些昂贵网络请求的因果 trace 根,而不是只看两台对端机器。这个仪表盘用 Dapper API 在不到两周内建成。
⑥ 分层与共享存储系统
Google 的许多存储系统由多层各自独立且复杂的分布式基础设施叠成:App Engine 建在一个可伸缩的实体存储系统之上,这个实体存储系统在一个底层 BigTable 之上暴露某些 RDBMS 功能,而 Bigtable 又同时用 Chubby(分布式锁系统)与 GFS;此外 Bigtable 这类系统作为共享服务被管理,以便简化部署、更好地利用算力。
这类分层系统里判读终端用户的资源消耗模式并不容易。 例子:某个 Bigtable cell 产生的大量 GFS 流量,是主要来自一个用户还是来自多个用户?在 GFS 层,这两种截然不同的使用模式是分辨不出来的;同理,共享服务上的争用在缺少这类工具时也难以调试。
Dapper UI 能跨任意共享服务的各客户端分组并聚合 trace 性能信息,于是共享服务的所有者可以按多种指标给自己的用户排个序 —— 入站网络负载、出站网络负载、或服务请求总时长。
救火:能用在哪,不能用在哪
救火指为处于险境的分布式系统采取的行动。此时使用者需要新鲜数据,且没时间写新的 DAPI 代码或等周期性报表跑完。
- 对正在经历高延迟、甚至在有正常负载的情况下超时的服务,UI 常能定位延迟瓶颈的位置;直连 Dapper daemon 就能轻松取到特定高延迟 trace 的新鲜数据;
- 灾难性故障期间,通常不必看聚合统计,示例 trace 就够确定根因;
- 但共享存储服务是例外:它们在用户活动骤增时需要尽快拿到聚合信息。对事件复盘仍可用聚合数据,但除非对已收集数据的批量分析能在事件开始后 10 分钟内完成,Dapper 对共享存储服务的救火就不够好用。
六条局限
① 合并效应(coalescing)
模型隐含假设各子系统一次只为一个被追踪的请求工作。但有些情况下先缓冲几个请求、再对一批请求做一次操作更省开销 —— 磁盘写合并就是一例。此时:
- 一条被追踪的请求会被归因到一个虚高的、其实不属于它的工作单元;
- 而且若多个被追踪请求被批到一起,因为每个 trace 只依赖一个唯一 trace id,只有其中一个会显得对那个 span 负责。
正在考虑能识别这类情况、并只记录足以区分它们的最少信息的方案。
② 追踪批处理负载
设计瞄准的是在线服务系统,最初目标是理解由用户请求引起的系统行为。MapReduce 这类离线数据密集型负载同样能从更好的性能洞察中受益,但这类情况需要把 trace id 关联到别的有意义的工作单元 —— 例如输入数据里的某个键(或键区间),或一个 MapReduce 分片。
③ 找根因
Dapper 能有效判断系统里哪一部分在变慢,但不总足以找到根因。例如一个请求慢,原因可能不在自身行为,而在于别的请求排在它前面。程序可以用应用级 annotation 把队列长度或过载情况转告追踪系统;若这类效应常见,ProfileMe 提出的成对采样(paired sampling)可能有用 —— 它采两个时间上重叠的请求,观察它们在整个系统里的相对延迟。
④ 记录内核级信息
内核可见事件的详细信息有时对确定根因有用。Google 已有一批工具能追踪或剖析内核执行,但要把那些信息与停留在用户态的 trace 上下文绑起来,很难做到既通用又不打扰系统。正在研究一个折中:从用户态取几个内核级活动参数的快照,关联到活动的 span 上。
⑤ 未预料的用例,以及它们怎么来的
意外用例的数量本身就是一条结论:除前面各节的用例,还包括资源计量系统、检查敏感服务是否符合规定通信模式的工具、以及RPC 压缩策略的分析等。其中一个成因被明确归为设计决策:把 trace 数据仓开放给开发者、并且只通过一个简单的编程接口,从而借到更大社区的创造力。
⑥ 给遗留负载加支持比预期简单
对已经在使用受支持的公共线程、控制流与 RPC 框架的程序,只需用新版库重新编译一次即可获得 Dapper 支持。
相关
- Bigtable —— trace 仓库本身就是 Bigtable:一条 trace 一行、一个 span 一列,稀疏表布局正好容纳任意 span 数;另一处耦合是所有 Bigtable 请求都带所访问表名的 annotation,这成了自动推断服务依赖的依据
- Dynamo —— 分位数视角的两端:Dynamo 把 SLA 定在 99.9 分位并解释为什么均值不够,Dapper 用同一条思路诊断长尾,并给出「关键路径上瞬时的网络退化不影响吞吐、只推高离群延迟」这个结论
- Chubby —— 被追踪的分层存储链里的一环:App Engine → 实体存储系统 → Bigtable → Chubby 与 GFS
- GFS —— 同一条链的另一环。Dapper 里那句"同一个 Bigtable cell 的 GFS 流量来自一个用户还是多个用户,在 GFS 层看不出来",正是分层系统难以做应用层归因的实例
- Borg —— 同一专栏的另一半:Borg 决定任务放在哪、活了没,Dapper 回答一次请求分别在各层花了多少时间
参考
- B. H. Sigelman, L. A. Barroso, M. Burrows, P. Stephenson, M. Plakal, D. Beaver, S. Jaspan, C. Shanbhag. Dapper, a Large-Scale Distributed Systems Tracing Infrastructure. Google Technical Report, 2010.
YJ