概要

现代互联网服务通常被实现为复杂的、大规模的分布式系统。

这些应用程序是由软件模块的集合构建的,这些模块可能由不同的团队使用不同的编程语言开发,并且可以跨越多个物理设施的数千台机器。

在这样的环境中,有助于理解系统行为和推理性能问题的工具是非常宝贵的。

在这里,我们介绍了 Google 的生产分布式系统跟踪基础设施 Dapper 的设计,并描述了如何满足我们的低开销、应用程序级透明性以及在超大规模系统上普遍部署的设计目标。

Dapper 与其他跟踪系统(特别是 Magpie [3] 和 X-Trace [12])在概念上有相似之处,但做出的某些设计选择对其在我们的环境中取得成功至关重要,例如使用采样并将仪器限制为 相当少量的公共库。

本文的主要目标是报告我们两年多来构建、部署和使用该系统的经验,因为 Dapper 成功的最重要衡量标准是它对开发人员和运营团队的有用性。

Dapper 最初是一个独立的跟踪工具,但后来发展成为一个监控平台,支持创建许多不同的工具,其中一些工具是其设计者没有预料到的。

我们描述了一些使用 Dapper 构建的分析工具,分享了有关其在 Google 内部使用情况的统计数据,展示了一些示例用例,并讨论了迄今为止学到的经验教训。

1 简介-Introduction

我们构建 Dapper 是为了向 Google 开发人员提供有关复杂分布式系统行为的更多信息。

此类系统特别令人感兴趣,因为大量小型服务器的集合对于互联网服务工作负载来说是一个特别经济高效的平台[4]。

了解这种情况下的系统行为需要观察许多不同程序和机器上的相关活动。

网络搜索示例将说明此类系统需要解决的一些挑战。

前端服务可能会将 Web 查询分发给数百个查询服务器,每个服务器都在自己的索引部分中进行搜索。

该查询还可以被发送到许多其他子系统,这些子系统可以处理广告、检查拼写或查找专门的结果,包括图像、视频、新闻等。

所有这些服务的结果都会有选择地组合在结果页面中; 我们将此模型称为“通用搜索”[6]。

总共可能需要数千台机器和许多不同的服务来处理一个通用搜索查询。

此外,网络搜索用户对延迟很敏感,这可能是由任何子系统的性能不佳引起的。

仅查看总体延迟的工程师可能知道存在问题,但可能无法猜测哪个服务出现问题,也无法猜测为什么它表现不佳。

首先,工程师可能无法准确地知道正在使用哪些服务; 新的服务和部件可能会每周添加和修改,以添加用户可见的功能并改进性能或安全性等其他方面。

其次,工程师不会是每项服务内部的专家; 每一个都是由不同的团队构建和维护的。

第三,服务和机器可能由许多不同的客户端同时共享,因此性能工件可能是由于另一个应用程序的行为造成的。

例如,前端可以处理许多不同的请求类型,或者诸如 Bigtable [8] 之类的存储系统在跨多个应用程序共享时可能是最有效的。

上述场景对 Dapper 提出了两个基本要求:无处不在的部署和持续监控。

普遍性很重要,因为即使系统的一小部分没有受到监控,跟踪基础设施的实用性也会受到严重影响。

此外,应始终打开监视,因为通常情况下,异常或其他值得注意的系统行为很难或不可能重现。

这些要求产生了三个具体的设计目标:

• 低开销:跟踪系统对正在运行的服务的性能影响应该可以忽略不计。 在一些高度优化的服务中,即使很小的监控开销也很容易被注意到,并且可能迫使部署团队关闭跟踪系统。

• 应用程序级透明度:程序员不需要了解跟踪系统。 依赖于应用程序级开发人员的积极协作才能发挥作用的跟踪基础设施变得极其脆弱,并且经常由于仪器错误或遗漏而被破坏,因此违反了普遍性要求。 这在像我们这样的快节奏的开发环境中尤其重要。

• 可扩展性:至少需要在未来几年内处理 Google 服务和集群的规模。

另一个设计目标是跟踪数据在生成后能够快速用于分析:最好在一分钟内

尽管基于数小时前的数据运行的跟踪分析系统仍然非常有价值,但新鲜信息的可用性可以使对生产异常做出更快的反应。

真正的应用程序级透明度,可能是我们最具挑战性的设计目标,是通过将 Dapper 的核心跟踪工具限制在一个由普遍存在的线程、控制流和 RPC 库代码组成的小语料库中来实现的。

通过使用自适应采样,可以使系统具有可扩展性并降低性能开销,这将在 4.4 节中进行描述。

生成的系统还包括用于收集跟踪的代码、用于可视化跟踪的工具以及用于分析大量跟踪的库和 API(应用程序编程接口)。

尽管 Dapper 有时足以让开发人员识别性能异常的根源,但它并不打算取代所有其他工具。

我们发现 Dapper 的系统范围数据通常侧重于性能调查,以便可以在本地应用其他工具。

1.1 贡献总结 Summary of contributions

分布式系统跟踪工具的设计空间已经在之前的许多优秀文章中进行了探讨,其中与 Dapper 关系最密切的是 Pinpoint [9]、Magpie [3] 和 X-Trace [12]。

这些系统往往在其开发的早期阶段就在研究文献中进行描述,然后才有机会清楚地评估重要的设计选择。

由于 Dapper 已经大规模生产和运营多年,我们认为最合适的做法是将本文的重点放在 Dapper 的部署教会了我们什么、我们的设计决策如何发挥作用以及它在哪些方面最有用。

Dapper 作为性能分析工具开发平台的价值,以及其本身的监控工具的价值,是我们在回顾性评估中可以识别的少数意外结果之一。

尽管 Dapper 与 Pinpoint 和 Magpie 等系统共享许多高级思想,但我们的实现在该领域包含许多新的贡献。

例如,我们发现采样对于降低开销是必要的,特别是在高度优化的 Web 服务中,这些服务往往对延迟非常敏感。

也许更令人惊讶的是,我们发现数千个请求中的一个样本就可以为跟踪数据的许多常见用途提供足够的信息。

我们系统的另一个重要特征是我们能够实现的应用程序级透明度。

我们的仪器被限制在软件堆栈中足够低的水平,即使是像谷歌网络搜索这样的大规模分布式系统也可以在没有额外注释的情况下被追踪。

虽然这更容易实现,因为我们的部署环境具有一定程度的同质性,但我们这样做的结果表明了实现这种透明度的一些充分条件。

2 Dapper 中的分布式跟踪

分布式服务的跟踪基础设施需要记录有关代表给定发起者在系统中完成的所有工作的信息。

例如,图 1 显示了具有 5 台服务器的服务:一个前端 (A)、两个中间层(B 和 C)以及两个后端(D 和 E)。

F1

当用户请求(在本例中为发起者)到达前端时,它会向服务器 B 和 C 发送两个 RPC。

B 可以立即响应,但 C 需要后端 D 和 E 的工作才能回复 A,而 A 又响应原始请求。

此请求的一个简单但有用的分布式跟踪是每个服务器发送和接收的每条消息的消息标识符和时间戳事件的集合。

已经提出了两类解决方案来聚合这些信息,以便可以将所有记录条目与给定的发起者(例如图 1 中的 RequestX)、黑盒和基于注释的监控方案相关联。

黑盒(Black-box)方案 [1,15,2] 假设除了上述消息记录之外没有其他信息,并使用统计回归技术来推断该关联。

基于注释(Annotation-based)的方案 [3,12,9,16] 依赖应用程序或中间件使用全局标识符显式标记每个记录,将这些消息记录链接回原始请求。

虽然黑盒方案比基于注释的方法更易于移植,但由于它们依赖于统计推断,因此需要更多数据才能获得足够的准确性

显然,基于注释的方法的主要缺点是需要对程序进行检测

在我们的环境中,由于所有应用程序都使用相同的线程模型、控制流和 RPC 系统,因此我们发现可以将检测限制为一小组公共库,并实现对应用程序开发人员有效透明的监控系统。

我们倾向于将 Dapper 跟踪视为嵌套 RPC 树。

然而,我们的核心数据模型并不局限于我们特定的 RPC 框架; 我们还跟踪 Gmail 中的 SMTP 会话、来自外部世界的 HTTP 请求以及对 SQL 服务器的出站查询等活动。

正式地,我们使用树(trees)、跨度(spans)和注释(annotations)对 Dapper 跟踪进行建模。

2.1 追踪树和跨度

在 Dapper 跟踪树中,树节点是基本工作单元,我们将其称为跨度。

边指示跨度与其父跨度之间的关系(casual relationship)。

然而,无论它在更大的跟踪树中的位置如何,跨度也是带时间戳的记录的简单日志,这些记录对跨度的开始和结束时间、任何 RPC 计时数据以及零个或多个特定于应用程序的注释进行编码,如第 2.3 节中所述。

我们在图 2 中说明了跨度如何形成较大迹线的结构。

F-2

Dapper 记录每个跨度的人类可读的跨度名称,以及跨度 ID 和父 ID,以便重建单个分布式跟踪中各个跨度之间的因果关系。

没有父 ID 创建的 Span 称为根 Span。

与特定跟踪关联的所有跨度也共享一个公共跟踪 ID(图中未显示)。

所有这些 id 都是概率上唯一的 64 位整数。

ps: 这里的 id 如果只是唯一,那么算法会更加灵活一下。

在典型的 Dapper 跟踪中,我们期望为每个 RPC 找到一个跨度,并且每个额外的基础设施层都会为跟踪树添加一个额外的深度级别。

图 3 提供了典型 Dapper 跟踪范围中记录的事件的更详细视图。

F-3

这个特定的跨度描述了图 2 中两个“Helper.Call”RPC 中较长的一个。

Span 的开始和结束时间以及任何 RPC 计时信息均由 Dapper 的 RPC 库工具记录。

如果应用程序所有者选择使用自己的注释来增强跟踪(如图中的“foo”注释),这些注释也会与跨度数据的其余部分一起记录。

需要注意的是,一个跨度可以包含来自多个主机的信息; 事实上,每个 RPC 跨度都包含来自客户端和服务器进程的注释,这使得双主机跨度成为最常见的跨度。

由于客户端和服务器上的时间戳来自不同的主机,因此我们必须注意时钟偏差。

在我们的分析工具中,我们利用了这样一个事实:RPC 客户端总是在服务器接收请求之前发送请求,反之亦然,对于服务器响应也是如此。

这样,我们就有了 RPC 服务器端跨度时间戳的下限和上限。

2.2 检测点(Instrumentation points)

Dapper 几乎完全依赖于一些通用库的检测,能够遵循分布式控制路径,应用程序开发人员几乎零干预:

• 当线程处理跟踪的控制路径时,Dapper 将跟踪上下文附加到线程本地存储。

跟踪上下文是一个小型且易于复制的跨度属性(例如跟踪和跨度 ID)容器。

• 当计算延迟或异步时,大多数Google 开发人员使用通用控制流库来构造回调并将其安排在线程池或其他执行器中。

Dapper 确保所有此类回调存储其创建者的跟踪上下文,并且在调用回调时此跟踪上下文与适当的线程关联。

通过这种方式,用于跟踪重建的 Dapper id 能够透明地遵循异步控制路径。

• 几乎所有 Google 进程间通信都是围绕单个 RPC 框架构建的,并绑定了 C++ 和 Java。

我们已经对该框架进行了检测,以定义所有 RPC 的跨度。

对于跟踪的 RPC,跨度和跟踪 ID 从客户端传输到服务器。

对于基于 RPC 的系统(例如 Google 广泛使用的系统),这是一个重要的检测点。

我们计划随着非 RPC 通信框架的发展和找到用户群而对其进行检测。

Dapper 跟踪数据与语言无关,生产中的许多跟踪组合了来自用 C++ 和 Java 编写的流程的数据。

在第 3.2 节中,我们讨论了我们在实践中能够实现的应用程序透明度水平。

2.3 注释(Annotations)

上述检测点足以导出复杂分布式系统的详细跟踪,使核心 Dapper 功能可用于其他未经修改的 Google 应用程序。

然而,Dapper 还允许应用程序开发人员使用附加信息来丰富 Dapper 跟踪,这些信息可能有助于监视更高级别的系统行为或帮助调试问题。

我们允许用户通过一个简单的 API 定义带时间戳的注释,其核心如图 4 所示。

F-4

这些注释可以具有任意内容。

为了保护 Dapper 用户免受意外的过度记录,各个跟踪范围的总注释量有一个可配置的上限。

无论应用程序行为如何,应用程序级注释都无法取代结构跨度或 RPC 信息。

除了简单的文本注释之外,Dapper 还支持键值注释映射,为开发人员提供更多跟踪能力,例如维护计数器、记录二进制消息以及在进程内传输任意用户定义的数据以及跟踪请求。

这些键值注释用于在分布式跟踪的上下文中定义特定于应用程序的等价类。

2.4 采样(Sampling)

低开销是 Dapper 的一个关键设计目标,因为如果服务运营商对性能有任何重大影响,那么服务运营商将不愿意部署尚未证实价值的新工具,这是可以理解的。

此外,我们希望允许开发人员使用注释 API,而不必担心额外的开销。

我们还发现某些类别的 Web 服务确实对检测开销很敏感。

因此,除了使 Dapper 集合的基本检测开销尽可能小之外,我们还通过仅记录所有跟踪的一小部分来进一步控制开销。

我们在 4.4 节中更详细地讨论了这种跟踪采样方案。

2.5 痕迹收集(Trace collection)

Dapper 跟踪日志记录和收集管道是一个三阶段过程(参见图 5)。

F-5

首先,将跨度数据写入 (1) 到本地日志文件。

然后,Dapper 守护进程和收集基础设施将其从所有生产主机中拉出 (2),最后将其写入 (3) 到几个区域 Dapper Bigtable [8] 存储库之一的单元中。

跟踪被布置为单个 Bigtable 行,每列对应一个跨度。

Bigtable 对稀疏表布局的支持在这里很有用,因为各个跟踪可以具有任意数量的跨度。

跟踪数据收集的中值延迟(即数据从检测的应用程序二进制文件传播到中央存储库所需的时间)小于 15 秒。

随着时间的推移,第 98 个百分位数的延迟本身是双峰的; 大约 75% 的时间,第 98 个百分点的收集延迟小于两分钟,但其他大约 25% 的时间可能会增长到几个小时。

Dapper 还提供了一个 API 来简化对我们存储库中跟踪数据的访问。

Google 的开发人员使用此 API 来构建通用和特定于应用程序的分析工具。

5.1 节包含迄今为止有关其用法的更多信息。

2.5.1 带外跟踪收集(Out-of-band trace collection)

所描述的 Dapper 系统使用请求树本身执行带外跟踪记录和收集。

这样做是出于两个不相关的原因。

首先,带内收集方案(其中跟踪数据在 RPC 响应标头内发送回)可能会影响应用程序网络动态。

在 Google 的许多大型系统中,找到具有数千个跨度的痕迹并不罕见。

然而,RPC 响应(即使在如此大的分布式跟踪的根附近)仍然相对较小:通常小于 10 KB。

在此类情况下,带内 Dapper 跟踪数据会使应用程序数据相形见绌,并使后续分析的结果产生偏差。

其次,带内收集方案假设所有 RPC 都是完美嵌套的。

我们发现有许多中间件系统在它们自己的所有后端返回最终结果之前就将结果返回给调用者。

带内(in-band)收集系统无法解释这种非嵌套分布式执行模式

2.6 安全和隐私考虑

记录一定量的 RPC 有效负载信息将丰富 Dapper 跟踪,因为分析工具可能能够在有效负载数据中找到可以解释性能异常的模式。

然而,在某些情况下,有效负载数据可能包含不应向未经授权的内部用户(包括从事性能调试的工程师)透露的信息。

由于安全和隐私问题是不容协商的,Dapper 存储 RPC 方法的名称,但此时不记录任何有效负载数据。

相反,应用程序级注释提供了一种方便的选择加入机制:应用程序开发人员可以选择将其认为对以后分析有用的任何数据与跨度相关联。

Dapper 还提供了一些其设计者没有预料到的安全优势。

例如,通过跟踪公共安全协议参数,Dapper 用于监控应用程序是否通过适当级别的身份验证或加密来满足安全策略。

Dapper 还可以提供信息以确保按预期实施基于策略的系统隔离,例如承载敏感数据的应用程序不会与未经授权的系统组件交互。

这些类型的测量比源代码审计提供了更大的保证。

3 Dapper 的部署状态

两年多来,Dapper 一直是我们的生产跟踪系统。

在本节中,我们报告系统的状态,重点关注它如何满足我们普遍部署和应用程序级透明度的目标。

3.1 Dapper运行时库

也许 Dapper 代码库中最关键的部分是基本 RPC、线程和控制流库的检测,其中包括跨度创建、采样和记录到本地磁盘。

除了轻量级之外,该代码还需要稳定和健壮,因为它链接到大量应用程序,这使得维护和错误修复变得困难。

核心工具的 C++ 代码少于 1000 行,Java 代码少于 800 行。

键值注解的实现额外增加了 500 行代码。

3.2 生产覆盖范围

Dapper 渗透率可以从两个维度进行评估:可以生成 Dapper 跟踪的生产流程(即与 Dapper 检测运行时库链接的生产流程)的比例以及运行 Dapper 跟踪收集守护进程的生产机器的比例。

Dapper 的守护进程是我们基本机器映像的一部分,使其几乎出现在 Google 的每台服务器上。

很难确定 Dapper 就绪进程的精确比例,因为不生成跟踪信息的进程对于 Dapper 来说是不可见的。

然而,考虑到 Dapper 检测库的普遍存在,我们估计几乎每个 Google 生产流程都支持跟踪。

在某些情况下,Dapper 无法正确遵循控制路径。 这些通常源于使用非标准控制流原语,或者 Dapper 错误地将因果关系归因于不相关的事件。

Dapper 提供了一个简单的库来帮助开发人员手动控制跟踪传播作为解决方法。

目前,有 40 个 C++ 应用程序和 33 个 Java 应用程序需要一些手动跟踪传播,仅占数千个总数的一小部分。

还有极少数程序使用未检测的通信库(例如原始 TCP 套接字或 SOAP RPC),因此不支持 Dapper 跟踪。

如果认为重要的话,可以将 Dapper 支持添加到这些应用程序中。

作为生产安全措施,可以关闭 Dapper 跟踪。

事实上,它在早期默认是关闭的,直到我们对其稳定性和低开销建立了信心。

Dapper 团队偶尔会进行审核,以查找服务所有者关闭跟踪的配置更改。

这种变化很少见,通常源于对监控开销的担忧。

迄今为止,所有这些变化都已在进一步调查和测量实际管理费用后恢复,但实际管理费用并不重要。

3.3 跟踪注释的使用

程序员倾向于使用特定于应用程序的注释作为一种分布式调试日志文件或通过某些特定于应用程序的功能对跟踪进行分类。

例如,所有 Bigtable 请求都用正在访问的表的名称进行注释。

目前,70% 的 Dapper 跨度和 90% 的 Dapper 跟踪至少有一个应用程序指定的注释。

41 个 Java 和 68 个 C++ 应用程序添加了自定义应用程序注释,以便更好地了解其服务中的跨度内活动。

值得注意的是,到目前为止,采用注释 API 的 Java 开发人员在每个跨度上所做的注释比 C++ 开发人员更多。

这可能是因为我们的 Java 工作负载往往更接近最终用户; 这类应用程序通常处理更广泛的请求组合,因此具有相对复杂的控制路径。

4 管理跟踪开销

跟踪系统的成本表现为被监视系统由于跟踪生成和收集开销而导致的性能下降,以及存储和分析跟踪数据所需的资源量。

尽管有人可能会说,有价值的跟踪基础设施可能值得牺牲性能,但我们相信,如果基线开销明显可以忽略不计,那么最初的采用将得到极大的促进。

在本节中,我们将介绍主要 Dapper 检测操作的开销、跟踪收集的开销以及 Dapper 对生产工作负载的影响。

我们还描述了 Dapper 的自适应跟踪采样机制如何帮助我们平衡低开销的需求和代表性跟踪的需求。

4.1 跟踪生成开销

跟踪生成开销是 Dapper 性能足迹中最关键的部分,因为在紧急情况下可以更轻松地关闭收集和分析。

Dapper 运行时库中跟踪生成开销的最重要来源是创建和销毁跨度和注释,并将它们记录到本地磁盘以供后续收集。

根跨度的创建和销毁平均需要 204 纳秒,而非根跨度的相同操作需要 176 纳秒。

不同之处在于为根跨度分配全局唯一跟踪 ID 的额外成本。

如果不对跨度进行采样以进行跟踪,则附加跨度注释的成本几乎可以忽略不计,其中包括 Dapper 运行时中的线程本地查找,平均约为 9 纳秒。

如果对其进行采样,则使用字符串文字注释跟踪(非常类似于图 4 中所示的内容)平均花费 40 纳秒。

这些测量是在 2.2GHz x86 服务器上进行的。

写入本地磁盘是 Dapper 运行时库中最昂贵的操作,但其可见开销大大减少,因为每个磁盘写入都会合并多个日志文件写入操作,并相对于跟踪的应用程序异步执行。

然而,日志写入活动可能会对高吞吐量应用程序性能产生明显影响,特别是在跟踪所有请求的情况下。

我们在第 4.3 节中量化了 Web 搜索工作负载中的这种开销。

T-2

4.2 跟踪收集开销(Trace collection overhead)

读取本地跟踪数据也会干扰正在监视的前台工作负载。

表 1 显示了基于不切实际的重负载测试基准的 Dapper 守护进程的最坏情况 CPU 使用情况。

该守护进程在收集期间不会使用超过生产机器一个核心的 0.3%,并且内存占用非常小(在堆碎片的噪音范围内)。

我们还将 Dapper 守护进程限制为内核调度程序中尽可能低的优先级,以防负载较重的主机中出现 CPU 争用。

Dapper 对网络资源的消耗也很小,我们存储库中的每个跨度平均仅对应 426 字节。

作为我们正在监控的应用程序中网络活动的一小部分,Dapper 跟踪数据收集只占 Google 生产环境中不到 0.01% 的网络流量。

4.3 对生产工作负载的影响(Effect on production workloads)

高吞吐量在线服务需要使用大量机器来处理每个请求,因此高效跟踪的要求最高; 它们往往会生成最大量的跟踪数据,同时它们对性能干扰也最敏感。

在表 2 中,我们使用我们的网络搜索集群作为此类服务的示例; 当我们改变采样轨迹的比率时,我们测量了 Dapper 对平均延迟和吞吐量的性能影响。

我们看到,虽然对吞吐量的影响不是很大,但为了避免明显的延迟下降,跟踪采样确实是必要的。

然而,与低于 1/16 的采样频率相关的延迟和吞吐量损失都在实验误差范围内。

在实践中,我们发现当使用低至1/1024的采样率时,对于大容量服务仍然有足够的跟踪数据。

保持基线 Dapper 开销极低非常重要,因为它为应用程序提供了一些余地,可以使用注释 API 的全部范围,而不必担心性能损失。

使用较低采样频率的另一个好处是允许数据在被垃圾收集之前在主机的本地磁盘上保存更长时间,这为收集基础设施提供了更大的灵活性

4.4 自适应采样 Adaptive sampling

归因于任何给定进程的 Dapper 开销与每单位时间处理样本的跟踪数量成正比。

Dapper 的第一个生产版本对 Google 的所有流程使用统一的采样概率,为每 1024 个候选者平均一个采样跟踪。

这个简单的方案对于我们的高吞吐量在线服务是有效的,因为绝大多数感兴趣的事件仍然很可能经常出现以被捕获。

然而,较低的流量工作负载可能会在如此低的采样率下错过重要事件,同时容忍更高的采样率和可接受的性能开销。

此类系统的解决方案是覆盖默认采样率,这需要我们在 Dapper 中试图避免的手动干预。

我们正在部署自适应采样方案,该方案不是通过均匀采样概率来参数化,而是通过每单位时间采样轨迹的期望速率来参数化。

这样,流量较低的工作负载会自动提高采样率,而流量非常高的工作负载则会降低采样率,从而使开销保持在可控范围内。

实际使用的采样概率与迹线本身一起记录; 这有助于在围绕 Dapper 数据构建的分析工具中准确计算跟踪频率。

4.5 应对激进采样 Coping with aggressive sampling

Dapper 新用户经常想知道低采样概率(对于高流量服务通常低至 0.01%)是否会干扰他们的分析。

我们在 Google 的经验使我们相信,对于高吞吐量服务,积极的采样不会妨碍最重要的分析。

如果一个值得注意的执行模式在此类系统中出现一次,那么它就会出现数千次。

流量较小的服务(每秒可能有数十个而不是数万个请求)可以跟踪每个请求; 这就是促使我们决定转向自适应采样率的原因。

4.6 收集过程中的额外采样 Additional sampling during collection

上述采样机制旨在最大限度地减少包含 Dapper 运行时库的应用程序中的可感知开销。

不过,Dapper 团队还需要控制写入其中央存储库的数据总大小,因此我们为此目的进行了第二轮采样。

目前,我们的生产集群每天生成超过 1 TB 的采样跟踪数据。

Dapper 用户希望跟踪数据至少在两个时间内保持可用最初从生产过程中记录下来几周后。

然后,必须权衡增加跟踪数据密度的好处与 Dapper 存储库的机器和磁盘存储成本。

对大量请求进行采样还会使 Dapper 收集器非常接近 Dapper Bigtable 存储库的写入吞吐量限制。

为了保持材料资源需求和累积 Bigtable 写入吞吐量的灵活性,我们在收集系统本身中添加了对额外采样的支持。

我们利用这样一个事实:给定跟踪的所有跨度(尽管它们可能分布在数千台不同的主机上)共享一个公共跟踪 ID。

对于收集系统中看到的每个跨度,我们将关联的跟踪 ID 散列为标量 z,其中 0 ≤ z ≤ 1。

如果 z 小于我们的集合采样系数,我们保留跨度并将其写入 Bigtable。

否则,我们将丢弃它。

通过根据我们的采样决策的迹线 ID,我们可以采样或丢弃整个迹线,而不是迹线中的各个跨度。

我们发现这个额外的配置参数使我们的收集管道的管理变得更加简单,因为我们可以通过更改配置文件中的单个参数来轻松调整全局写入速率。

如果整个跟踪和收集系统只有一个采样参数会更简单,但快速调整所有已部署的二进制文件中的运行时采样配置是不可行的。

我们选择了一个运行时采样率,该采样率产生的数据略多于我们可以写入存储库的数据,并且我们使用收集系统中的辅助采样系数来限制该写入率。

简洁的管道维护变得更加容易,因为我们可以通过对二次采样配置进行一次更改来立即增加或减少我们的全球覆盖范围和写入速率。

5 个通用的简洁工具 General-Purpose Dapper Tools

几年前,当 Dapper 还是原型时,它只有在 Dapper 开发人员的耐心帮助下才能使用。

从那时起,我们迭代地构建了收集基础设施、编程接口和基于 Web 的交互式用户界面,以帮助 Dapper 用户独立解决他们的问题。

在本节中,我们总结了哪些方法有效,哪些方法无效,并提供了有关这些通用分析工具的基本使用信息

5.1 Dapper Depot API

Dapper“Depot API”或 DAPI 可直接访问区域 Dapper 存储库(或“Depots”)中的分布式跟踪记录。

DAPI 和 Dapper 跟踪存储库是协同设计的,DAPI 旨在为这些 Dapper 存储库中包含的原始数据提供干净、直观的界面。

我们的用例建议使用以下三种方法来访问跟踪数据:

通过跟踪 ID 访问:DAPI 可以根据需要加载任何跟踪,因为其具有全局唯一的跟踪 ID。

批量访问:DAPI 可以利用 MapReduce 提供对数十亿 Dapper 跟踪的并行访问。 用户重写接受 Dapper 跟踪作为其唯一参数的虚拟函数,并且框架将为用户指定的时间窗口内收集的每个跟踪调用该函数一次。

索引访问:Dapper 存储库支持单个索引,该索引已被选择来匹配我们常见的访问模式。 该索引从常见请求的跟踪特征(如下所述)映射到独特的简洁跟踪。

由于跟踪 ID 是伪随机分配的,因此这是快速访问与特定服务或主机关联的跟踪的最佳方法。

所有三种访问模式都会引导用户访问不同的 Dapper 跟踪记录。

如前面第 2.1 节所述,Dapper 跟踪被建模为跟踪跨度树,因此跟踪数据结构是单个跨度结构的简单可遍历树。

这些跨度通常对应于 RPC 调用,并且在这些情况下,RPC 计时信息可用。

带时间戳的应用程序注释也可以通过跨度结构访问。

选择合适的自定义索引是 DAPI 设计中最具挑战性的方面。

跟踪数据索引所需的压缩存储仅比实际跟踪数据本身少 26%,因此成本很高。

最初,我们部署了两个索引:一个用于主机的索引,一个用于服务名称的索引。

然而,我们对基于机器的索引没有足够的兴趣来证明其存储成本的合理性。

当用户对单个机器感兴趣时,他们也对特定服务感兴趣,因此我们最终将两者组合成一个复合索引,允许按服务名称、主机和时间戳的顺序进行高效查找。

5.1.1 Google 内部 DAPI 的使用

Google 的 DAPI 使用分为三类:使用 DAPI 的持久在线 Web 应用程序、可以从命令行按需运行的维护良好的基于 DAPI 的工具,以及编写、运行和运行的一次性分析工具。

大部分都被遗忘了。

我们分别知道 3 个基于 DAPI 的持久应用程序、8 个额外的基于 DAPI 的按需分析工具以及大约 15-20 个使用 DAPI 框架构建的一次性分析工具。

很难解释后一类的工具,因为开发人员可以在 Dapper 团队不知情的情况下构建、运行和放弃这些项目。

5.2 Dapper 的用户接口

大多数 Dapper 的使用都是在基于 Web 的交互式用户界面中进行的。

由于空间原因,我们无法展示其中的每个功能,但典型的用户工作流程如图 6 所示。

F-6

1:用户描述他们感兴趣的服务和时间窗口,以及区分跟踪模式所需的任何信息(在本例中为跨度名称)。

他们还指定了与其调查最相关的成本指标(在本例中为服务延迟)。

2:出现与给定服务关联的所有分布式执行模式的性能摘要大表。 用户可以根据自己的意愿对这些执行模式进行排序,并选择一种来更详细地查看。

3:一旦选择了单个分布式执行模式,就会向用户呈现所述执行模式的图形描述。 正在检查的服务在图表的中心突出显示。

4:创建与步骤 #1 中选择的成本度量空间的细分相对应的存储桶后,Dapper 用户界面会在该度量空间上呈现一个简单的频率直方图。

因此,在这个示例中,我们可以看到所选执行模式的延迟大致呈对数正态分布。

还向用户呈现属于直方图的不同范围的特定示例轨迹的列表。

在本例中,用户单击第二个示例跟踪,这会将他们带到 Dapper 用户界面中的跟踪检查视图。

5:许多(如果不是大多数)Dapper 用户最终的目标是检查特定的痕迹,希望收集有关系统行为根本原因的信息。 我们没有足够的空间来公正地进行跟踪视图,但它的特点是全局时间线(如顶部所示)以及交互式扩展和折叠子树的能力。 分布式跟踪树的连续层由嵌套的彩色矩形表示。

每个 RPC 跨度进一步细分为服务器进程内花费的时间(绿色)和网络上花费的时间(蓝色)。

用户注释未在此屏幕截图中显示,但它们可以逐个跨度选择性地包含在全局时间线中。

对于寻求实时数据的用户,Dapper 用户界面能够直接与每台生产机器上的 Dapper 守护进程进行通信。

在该模式下,无法查看如上所示的系统级图表,但仍然可以轻松地根据延迟或网络特征选择单独的跟踪。

在这种操作模式下,数据可以在几秒钟内实时获得。

根据我们的日志,大约 200 名不同的 Google 工程师在一个典型的工作日使用 Dapper UI; 因此,在一周内,大约有 750-1000 个不同的用户。

这些数字在每个月以新功能的内部公告为模数时都是一致的。 用户通常会发送指向感兴趣的特定跟踪的链接,这将不可避免地在跟踪检查器中产生大量一次性、短期流量。

6 经验

Dapper 在 Google 得到广泛使用,既直接通过 Dapper 用户界面,也间接通过编程 API 或构建在这些 API 之上的应用程序。

在本节中,我们不会尝试对 Dapper 的每个已知用途进行分类,而是尝试涵盖 Dapper 用途的“基本向量”,以说明哪些类型的应用程序最成功。

6.1 开发过程中使用Dapper

Google AdWords 系统是围绕关键字定位标准和相关文本广告的大型数据库构建的。

当插入或修改新的关键字或广告时,必须检查它们是否符合服务政策条款(例如不适当的语言); 通过自动审核系统使流程变得更加高效。

当需要从头开始重新设计 Ads Review 的一项服务时,团队从第一个系统原型到系统的启动和最终维护,迭代地使用 Dapper。

Dapper 通过以下方式帮助他们改善服务:

Performance 性能:开发人员根据请求延迟目标跟踪进度并确定简单的优化机会。 Dapper 还用于识别关键路径上不必要的串行请求,这些请求通常源自开发人员未自己编写的子系统,并提示团队随后修复它们。

Correctness 正确性:广告审核服务围绕大型数据库系统展开。 该系统同时具有只读副本(访问成本低廉)和读写主服务器(访问成本昂贵)。

Dapper 用于识别许多不必要地向主服务器而不是副本发出查询的情况。

现在可以考虑直接访问主设备的情况并保证重要的系统不变量。

Understanding:Ads Review 查询分布在多种类型的系统中,包括 BigTable、上述数据库、多维索引服务以及各种其他 C++ 和 Java 后端服务。

Dapper 跟踪用于评估总查询成本,并促使人们重新设计操作,以最大限度地减少系统依赖性的负载。

Testing 测试:新代码发布经过 Dapper 跟踪 QA 流程,验证正确的系统行为和性能。 使用此流程发现了许多问题,无论是在广告审核代码本身还是在支持库中。

广告审核团队广泛使用了 Dapper 注释 API。 Guice[13] 开源 AOP 框架用于将重要的软件组件标记为“@Traced”。

跟踪还进一步注释了有关重要子例程的输入和输出大小、状态消息以及其他调试信息的信息,否则这些信息将被发送到日志文件。

Dapper 在某些方面未能满足广告审核团队的要求。

例如,他们希望在交互时间内搜索所有跟踪注释,但必须运行自定义 MapReduce 或手动检查各个跟踪。

此外,Google 还有其他系统可以收集和集中通用调试日志中的信息,集成来自这些系统和 Dapper 存储库的大量数据并非易事。

不过,广告审核团队根据从 Dapper 跟踪平台收集的数据估计,总而言之,他们的延迟时间已改善了两个数量级。

6.1.1 与异常监控集成

Google 维护着一项持续收集和集中运行进程中的异常报告的服务。

如果这些异常发生在采样的 Dapper 跟踪的上下文中,则相应的跟踪和跨度 ID 将作为元数据包含在异常报告中。

然后,异常监视服务的前端提供从特定异常报告到其各自的分布式跟踪的链接。

广告审核团队使用此功能来了解异常监控服务所识别的错误的更广泛的取证背景。 通过导出围绕简单唯一 ID 构建的接口,Dapper 平台可以相对轻松地集成到其他事件监控系统中。

6.2 解决长尾延迟问题 Addressing long tail latency

由于移动部件的数量以及代码库和部署的大小和范围,像通用搜索(前面第 1 节中描述的)这样的调试服务非常具有挑战性。

在这里,我们描述了为减弱通用搜索延迟分布的长尾所做的努力。

Dapper 能够验证有关端到端延迟的假设,更具体地说,验证通用搜索请求的关键路径。

当系统不仅涉及数十个子系统,而且涉及数十个工程团队时,即使是我们最优秀、最有经验的工程师也经常会错误地猜测端到端性能不佳的根本原因。

在这种情况下,Dapper 可以提供急需的事实,并能够最终回答许多重要的性能问题。

一位从事长尾延迟调试的工程师构建了一个小型库,该库可以从 DAPI Trace 对象推断分层关键路径。

然后,这些关键路径结构用于诊断问题并优先考虑通用搜索的预期性能改进。

与 Dapper 的合作带来了以下发现:

• 关键路径上网络性能的瞬时下降不会影响系统吞吐量,但会对异常延迟产生深远影响。 如图 7 所示,大多数缓慢的通用搜索跟踪在其关键路径上都经历了网络性能下降。

F-7

• 由于服务之间的意外交互而导致出现许多有问题且成本高昂的查询模式。 一旦被识别出来,它们通常很容易被纠正,但在 Dapper 面前,识别本身就很困难。

• 常见查询是从 Dapper 外部的安全日志存储库获取的,并使用 Dapper 的唯一跟踪 ID 与 Dapper 存储库结合。 然后,使用此映射来构建示例查询列表,这些示例查询对于通用搜索中的每个单独子系统来说都很慢。

6.3 推断服务依赖关系 Inferring service dependencies

在任何给定时间,谷歌的一个典型计算集群都承载着数千个逻辑“作业”; 执行共同功能的进程集。

当然,谷歌维护着许多这样的集群,事实上我们发现一个计算集群中的工作通常依赖于其他计算集群中的工作集群。

由于作业之间的依赖关系动态变化,因此不可能仅通过配置信息推断所有服务间依赖关系。

尽管如此,公司内的各种流程仍然需要准确的服务依赖性信息,以便识别瓶颈并规划服务移动等。

Google 的“服务依赖关系”项目利用跟踪注释和 DAPI MapReduce 接口来自动确定服务依赖关系。

使用 Dapper 的核心工具和 Dapper 跟踪注释,服务依赖项项目能够推断各个作业之间的依赖关系,以及这些作业使用的共享软件基础设施的依赖关系。

例如,所有 Bigtable 操作都用受影响的表的名称进行标记。

使用 Dapper 平台,服务依赖项团队能够自动推断各种服务粒度的命名资源的依赖项。

6.4 不同服务的网络使用情况

谷歌为其网络结构投入了大量的人力和物力。

毫不奇怪,网络运营商长期以来一直可以访问各个硬件的监控信息,并且构建了自定义工具和仪表板来鸟瞰全球网络利用率。

网络运营商对广域网的整体健康状况有合理的了解,但是,当出现问题时,他们几乎没有工具可以正确地将网络负载归因于应用程序级的罪魁祸首。

尽管 Dapper 不是为链路级监控而设计的,但我们发现它非常适合集群间网络活动的应用程序级分析任务。

Google 能够利用 Dapper 平台构建一个持续更新的控制台,显示集群间网络流量最活跃的应用程序级端点。

此外,使用 Dapper,我们能够指出这些昂贵的网络请求的因果跟踪根,而不是将自己限制在孤立的两台对等机器上。

该仪表板是在 Dapper API 之上在不到 2 周的时间内构建的。

6.5 分层和共享存储系统 Layered and Shared Storage Systems

Google 的许多存储系统由多个独立的复杂分布式基础设施层组成。

例如,Google App Engine[5] 构建在可扩展实体存储系统之上。

该实体存储系统在底层 BigTable 之上公开了某些 RDBMS 功能。

Bigtable 又使用 Chubby[7](分布式锁系统)和 GFS。

此外,像 BigTable 这样的系统作为共享服务进行管理,以简化部署并更好地利用计算资源。

在此类分层系统中,确定最终用户资源消耗模式并不总是那么容易。

例如,来自给定 BigTable 单元的大量 GFS 流量可能主要源自一个用户或多个用户,而在 GFS 级别,这两种不同使用模式之间的差异被掩盖。

此外,如果没有 Dapper 等工具,对此类共享服务的争用也同样难以调试。

第 5.2 节中所示的 Dapper 用户界面可以对任何共享服务的各个客户端的跟踪性能信息进行分组和聚合。

这使得共享服务的所有者可以轻松地根据各种指标(例如入站网络负载、出站网络负载或服务请求所花费的总时间)对用户进行排名。

6.6 使用 Dapper 进行消防(Firefighting with Dapper)

Dapper 对于某些但不是所有的消防任务很有用。

这里的“救火”是指代表处于危险中的分布式系统执行的活动。

通常,负责消防工作的 Dapper 用户需要访问新数据,而没有时间编写新的 DAPI 代码或等待定期报告运行。

对于遇到高延迟或更糟糕的是在正常工作负载下超时的服务,Dapper 用户界面通常可以隔离延迟瓶颈的位置。

通过直接与 Dapper 守护进程通信,可以轻松收集有关特定高延迟跟踪的新数据。

在灾难性故障期间,通常不需要查看汇总统计数据来确定根本原因,示例跟踪就足够了。

然而,像第 6.5 节中描述的共享存储服务需要在用户活动突然激增期间尽快聚合信息。

对于事件事后分析,共享服务仍然可以利用聚合的 Dapper 数据,但在事件发生后 10 分钟内完成对收集的 Dapper 数据的批量分析之前,Dapper 不会像解决消防问题那样有用。

共享存储服务。

7 其他经验教训

尽管迄今为止我们对 Dapper 的体验总体上达到了我们的预期,但仍有一些积极的方面是我们没有完全预料到的。

我们对非预期用例的数量感到特别满意。

除了第 6 节中描述的一些经验之外,这些经验还包括资源核算系统、检查敏感服务是否符合指定通信模式的工具以及 RPC 压缩策略的分析等。

我们将这些意外用途部分归因于通过简单的编程接口向开发人员开放跟踪数据存储的决定,因为这使我们能够利用更大社区的创造力。

向遗留工作负载添加 Dapper 支持也比预期更简单,只需要使用现有库的新版本重新编译使用常见支持的线程、控制流和 RPC 框架的程序。

Dapper 在 Google 内部的广泛使用也为我们提供了有关其一些局限性的宝贵反馈。

下面我们描述了迄今为止我们已经确定的一些最重要的内容。

合并效应:我们的模型隐式假设各个子系统一次将针对一个跟踪请求执行工作。

在某些情况下,在一次对一组请求执行操作之前缓冲一些请求会更有效(磁盘写入的合并就是一个这样的例子)。

在这种情况下,跟踪的请求可能会被归咎于看似庞大的工作单元。

此外,如果将多个跟踪请求一起批处理,则由于我们依赖每个跟踪的单个唯一跟踪 ID,因此只有其中一个会负责跨度。

我们正在考虑可以识别这些案例并记录消除歧义所需的最少量信息的解决方案。

跟踪批量工作负载:Dapper 的设计针对在线服务系统,最初的目标是了解用户向 Google 发出请求所产生的系统行为。

然而,离线数据密集型工作负载,例如那些适合 MapReduce [10] 模型的工作负载,也可以从更好的性能洞察中受益。

在这种情况下,我们需要将跟踪 ID 与其他一些有意义的工作单元相关联,例如输入数据中的键(或键范围)或 MapReduce 分片。

查找根本原因:Dapper 可以有效地确定系统的哪个部分出现速度下降,但并不总是足以找到根本原因。

例如,一个请求可能很慢,不是因为它自己的行为,而是因为其他请求在它之前排队。

程序可以利用应用程序级注释将队列大小或过载情况转发给跟踪系统。

此外,如果这种效应很常见,ProfileMe [11] 中提出的配对采样技术可能会很有用。

它包括对两个时间重叠的请求进行采样,并观察它们在整个系统中的相对延迟。

记录内核级信息:有关内核可见事件的详细信息有时对于确定根本原因很有用。

我们有许多能够跟踪或以其他方式分析内核执行的工具,但将该信息与驻留在用户级别的跟踪上下文联系起来很难以通用且不引人注目的方式完成。

我们正在研究一种可能的妥协解决方案,其中我们从用户级别获取一些内核级活动参数的快照,并将它们与活动跨度相关联。

8 相关工作

分布式系统跟踪领域有大量的工作,其中一些系统主要专注于查明故障,而另一些系统则致力于性能优化。

Dapper 已用于故障发现,但它通常在发现性能问题和提高对大型复杂工作负载行为的总体理解方面更有用。

Dapper 与黑盒监控系统有关,例如 Project5 [1]、WAP5 [15] 和 Sherlock 系统 [2],可以说它们可以通过不依赖运行时库来实现更高程度的应用程序级透明度 仪器仪表。

黑盒方案的缺点是存在一定程度的不精确性,并且在因果路径的统计推断中可能涉及较大的开销。

中间件或应用程序本身基于显式注释的检测可能是分布式系统监控的更流行的方法。

Pip[14] 和 Webmon[16] 是更多依赖应用程序级注释的系统示例,而 XTrace[12]、Pinpoint[9] 和 Magpie[3] 主要关注库和中间件修改。

Dapper 与后一组关系最为密切。

与 Pinpoint、XTrace 和 Magpie 的早期版本一样,Dapper 使用全局标识符将来自分布式系统各个部分的相关事件联系在一起。

与这些系统一样,Dapper 试图通过将检测隐藏在通用软件模块中来消除对应用程序进行注释的需要。

Magpie 通过采用为每个应用程序编写并明确描述事件之间关系的事件模式,放弃了全局 ID 的使用以及正确传播它们的挑战。

我们尚不清楚模式在实践中实现透明度方面有多有效。

X-Trace 的核心注释要求比 Dapper 的更为雄心勃勃,因为不仅在节点边界处收集跟踪,而且在节点内的不同软件层之间传递控制时也收集跟踪。

我们对低开销仪器的严格要求使我们远离了这种模型,并转向创建最小的机制集,使代表给定原始请求完成的所有工作能够捆绑在一起。

仍然可以通过可选的应用程序注释来丰富简洁的跟踪。

9 结论

在本文中,我们介绍了 Google 的生产分布式系统跟踪平台 Dapper,并报告了我们开发和使用它的经验。

Dapper 几乎部署在所有 Google 系统中,并且允许跟踪绝大多数最大的工作负载,而无需任何应用程序级别的修改,并且不会对性能产生明显影响。

主跟踪用户界面的流行证明了 Dapper 对开发人员和运营团队的实用性,并通过用例示例进行了说明,甚至有些用例是其设计者没有预料到的。

据我们所知,这是第一篇报告大型生产分布式系统跟踪框架的文章。

事实上,我们的主要贡献来自于我们对已经运行两年多的系统进行回顾性报告。

例如,我们发现,将最小的应用程序透明跟踪功能与简单的 API 相结合以供程序员增强跟踪的决定是值得的。

我们相信,与之前基于注释的分布式跟踪系统相比,Dapper 实现了更高程度的应用程序级透明度,需要手动干预的少量工作负载就证明了这一点。

虽然我们的计算部署有些不寻常的同质性促进了这一点,但这仍然是一个重大挑战。

最重要的是,我们的设计提出了一些实现应用程序级透明度的充分条件,我们希望这可以帮助其他人为更异构的环境开发解决方案。

最后,通过向内部开发人员开放 Dapper 的跟踪存储库,我们能够创建比 Dapper 团队单独能够单独生产的更多的分析工具,从而极大地利用了设计和实现工作。

致谢

我们感谢 Mahesh Palekar、Cliff Biffle、Thomas Kotzmann、Kevin Gibbs、Yonatan Zunger、Michael Kleber 和 Toby Smith 提供的实验数据和有关 Dapper 体验的反馈。

我们还感谢 Silvius Rus 在负载测试方面提供的帮助。

但最重要的是,我们感谢多年来不断开发和改进 Dapper 的优秀工程师团队;

按出场顺序排列:莎朗·珀尔、迪克·赛茨、罗布·冯·贝伦、托尼·德威特、唐·帕泽尔、奥弗·扎吉塞克、安东尼·扎纳、金香儿、约书亚·麦克唐纳、丹·斯图曼、格伦·威伦、亚历克斯·科伦贝克、布莱恩·麦克巴伦、迈克尔·克莱伯、 克里斯·波维克、布拉德利·怀特、托比·史密斯、托德·德尔、迈克尔·德·罗莎和阿蒂查·穆蒂塔查罗恩。

他们都做了大量的工作,让 Dapper 成为 Google 的日常现实。

参考资料

https://storage.googleapis.com/pub-tools-public-publication-data/pdf/36356.pdf

https://research.google/pubs/pub36356/