1. 介绍

简单研究过全链路的同学想必一定看过google dapper的这篇论文。我这里直接看了中文翻译的,并且对其内容进行了一些总结。具体地址见:Dapper分布式跟踪系统-翻译

2. 为什么使用dapper

文章首先举了一些例子说明了跟踪系统的必要性。尤其当一个用户操作设计大量的服务的时候,跟踪系统可以方便我们定位到底是在哪个服务的调用上产生了问题。

3. 跟踪系统在设计时需要考虑的问题

  1. 低消耗: 由于是7*24的监控,所以开销要小,不影响在线服务
  2. 应用级透明:应用程序不需要关注如何使跟踪系统生效。可以在线程调用、控制流、RPC库中埋点来做到。
  3. 扩展性: 支持更多的服务和更大的集群规模

4. 跟踪系统实现方法

4.1 基本方法

例如下图这样的调用关系:

  1. 黑盒方案:假定需要跟踪的除了上述信息之外没有额外的信息,这样使用统计回归技术来推断两者之间的关系。需要一些额外的数据来获得足够精度。
  2. 基于标注的方案:依赖于应用程序或中间件明确地标记一个全局ID,从而连接每一条记录和发起者的请求。缺点是有代码入侵。

4.2 跟踪树和span

在Dapper跟踪树结构中,树节点是整个架构的基本单元,而每一个节点又是对span的引用。节点之间的连线表示的span和它的父span直接的关系。虽然span在日志文件中只是简单的代表span的开始和结束时间,他们在整个树形结构中却是相对独立的。 这里span是跟踪术结构的基本单元,也表示一小段的时间。下图是5个span在Dapper跟踪树种短暂的关联关系

上图说明了span在一个大的跟踪过程中是什么样的。Dapper记录了span名称,以及每个span的ID和父ID,以重建在一次追踪过程中不同span之间的关系。如果一个span没有父ID被称为root span。所有span都挂在一个特定的跟踪上,也共用一个跟踪id(在图中未示出)。所有这些ID用全局唯一的64位整数标示。在一个典型的Dapper跟踪中,我们希望为每一个RPC对应到一个单一的span上,而且每一个额外的组件层都对应一个跟踪树型结构的层级。

上图给出了一个更详细的典型的Dapper跟踪span的记录点的视图。在图中这种某个span表述了两个“Helper.Call”的RPC(分别为server端和client端)。span的开始时间和结束时间,以及任何RPC的时间信息都通过Dapper在RPC组件库的植入记录下来。如果应用程序开发者选择在跟踪中增加他们自己的注释(如图中“foo”的注释)(业务数据),这些信息也会和其他span信息一样记录下来。

4.3 埋点

  1. 追踪的上下文信息在ThreadLocal中进行存储。
  2. 当计算过程是延迟调用的或是异步的,google通过通用的控制流来回调,确保所有的回调可以存储这次跟踪的上下文信息。当回调函数被触发时,这次跟踪的上下文会与适当的线程关联上。在这种方式下,Dapper可以使用trace ID和span ID来辅助构建异步调用的路径。
  3. google的所有进程通信是建立在一个RPC框架上。在RPC框架本身中来埋点从而定义所有span。
  4. dapper允许用户在Dapper跟踪的过程中添加额外的信息,以监控更高级别的系统行为,或帮助调试问题。我们允许用户通过一个简单的API定义带时间戳的Annotation,核心的示例代码如下图所示。
  5. dapper支持如下图的文本annotation也支持key-value映射的Annotation。如持续的计数器,二进制消息记录和在一个进程上跑着的任意的用户数据等。

4.4 跟踪收集

下图演示了Dapper收集管道:

由上图可知,Dapper的跟踪记录和收集管道的过程分为三个阶段:

  1. span数据写入本地日志文件中。
  2. 然后Dapper的守护进程和收集组件把这些数据从生产环境的主机中拉出来
  3. 写到Dapper的Bigtable仓库中。一次跟踪被设计成Bigtable中的一行,每一列相当于一个span。Bigtable的支持稀疏表格布局正适合这种情况,因为每一次跟踪可以有任意多个span。

Dapper还提供了一个API来简化访问我们仓库中的跟踪数据。 Google的开发人员用这个API,以构建通用和特定应用程序的分析工具。

4.5 带外数据跟踪收集

带外数据:传输层协议使用带外数据(out-of-band,OOB)来发送一些重要的数据,如果通信一方有重要的数据需要通知对方时,协议能够将这些数据快速地发送到对方。为了发送这些数据,协议一般不使用与普通数据相同的通道,而是使用另外的通道。out-of-band是通过其他的链路进行跟踪数据的收集,Dapper的写日志然后进行日志采集的方式就属于out-of-band策略

带内策略:这里指的in-band策略是把跟踪数据随着调用链进行传送。

Dapper跟踪和记录带外数据的原因:
Dapper系统请求树树自身进行跟踪记录和收集带外数据。这样做是为两个不相关的原因。首先,带内收集方案--这里跟踪数据会以RPC响应头的形式被返回--会影响应用程序网络动态。在Google里的许多规模较大的系统中,一次跟踪成千上万的span并不少见。然而,RPC回应大小--甚至是接近大型分布式的跟踪的根节点的这种情况下-- 仍然是比较小的:通常小于10K。在这种情况下,带内Dapper的跟踪数据会让应用程序数据和倾向于使用后续分析结果的数据量相形见绌。其次,带内收集方案假定所有的RPC是完美嵌套的。我们发现,在所有的后端的系统返回的最终结果之前,有许多中间件会把结果返回给他们的调用者。带内收集系统是无法解释这种非嵌套的分布式执行模式的。

4.6 安全和隐私

  1. dapper不记录有效负载数据
  2. 工程师可以自己利用应用程序界别的annotation在span中关联那些为以后分析提供价值的数据。
  3. 通过跟踪公开的安全协议参数,Dapper可以通过相应级别的认证或加密,来监视应用程序是否满足安全策略。例如。Dapper还可以提供信息,以基于策略的的隔离系统按预期执行,例如支撑敏感数据的应用程序不与未经授权的系统组件进行了交互。这样的测算提供了比源码审核更强大的保障。

4.7 Dapper运行库

也许Dapper代码中中最关键的部分,就是对基础RPC、线程控制和流程控制的组件库的植入,其中包括span的创建,采样率的设置,以及把日志写入本地磁盘。除了做到轻量级,植入的代码更需要稳定和健壮,因为它与海量的应用对接,维护和bug修复变得困难。植入的核心代码是由未超过1000行的C++和不超过800行Java代码组成。为了支持键值对的Annotation还添加了额外的500行代码。

4.8 收集耗时数据

数据都是在2.2GHz的x86服务器上采集的:

  1. 根span的创建和销毁需要损耗平均204纳秒的时间,而同样的操作在其他span上需要消耗176纳秒。时间上的差别主要在于需要在跟span上给这次跟踪分配一个全局唯一的ID。
  2. 如果一个span没有被采样的话,那么这个额外的span下创建annotation的成本几乎可以忽略不计,他由在Dapper运行期对ThreadLocal查找操作构成,这平均只消耗9纳秒。如果这个span被计入采样的话,会用一个用字符串进行标注--在图4中有展现--平均需要消耗40纳秒。

下图是Dapper守护进程在负载测试时的CPU资源使用率:

限制了Dapper守护进程为内核scheduler最低的优先级,以防在一台高负载的服务器上发生cpu竞争。

Dapper也是一个带宽资源的轻量级的消费者,每一个span在我们的仓库中传输只占用了平均426的byte。作为网络行为中的极小部分,Dapper的数据收集在Google的生产环境中的只占用了0.01%的网络资源。

4.9 采样率

不同采样率对网络延迟和吞吐的影响:

在实践中,我们发现即便采样率调整到1/1024仍然是有足够量的跟踪数据的用来跟踪大量的服务。使用较低的采样率还有额外的好处,可以让持久化到硬盘中的跟踪数据在垃圾回收机制处理之前保留更长的时间,这样为Dapper的收集组件给了更多的灵活性。

这里建议1/1024这种较低的采样率就足够了。

4.10 Dapper Depot API

Dapper的“Depot API”或称作DAPI,提供在Dapper的区域仓库中对分布式跟踪数据一个直接访问。DAPI和Dapper跟踪仓库被设计成串联的,而且DAPI意味着对Dapper仓库中的元数据暴露一个干净和直观的的接口。我们使用了以下推荐的三种方式去暴露这样的接口:

  1. 通过跟踪ID来访问:DAPI可以通过他的全局唯一的跟踪ID读取任何一次跟踪信息。
  2. 批量访问:DAPI可以利用的MapReduce提供对上亿条Dapper跟踪数据的并行读取。用户重写一个虚拟函数,它接受一个Dapper的跟踪信息作为其唯一的参数,该框架将在用户指定的时间窗口中调用每一次收集到的跟踪信息。
  3. 索引访问:Dapper的仓库支持一个符合我们通用调用模板的唯一索引。该索引根据通用请求跟踪特性(commonly-requested trace features)进行绘制来识别Dapper的跟踪信息。因为跟踪ID是根据伪随机的规则创建的,这是最好的办法去访问跟某个服务或主机相关的跟踪数据。

参考资料:
Dapper分布式跟踪系统-翻译