[说明:本文是阅读Google论文“Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”以后的一个简要总结,完整译文可参考此处。 另论文“Uncertainty in Aggregate Estimates from Sampled Distributed Traces”中有关于采样的更详细分析。此外,Twitter开源的Zipkin就是参考Google Dapper而开发。]html
Dapper最初是为了追踪在线服务系统的请求处理过程。好比在搜索系统中,用户的一个请求在系统中会通过多个子系统的处理,并且这些处理是发生在不一样机器甚至是不一样集群上的,当请求处理发生异常时,须要快速发现问题,并准肯定位到是哪一个环节出了问题,这是很是重要的,Dapper就是为了解决这样的问题。前端
对系统行为进行跟踪必须是持续进行的,由于异常的发生是没法预料的,并且多是难以重现的。同时跟踪须要是无所不在,遍及各处的,不然可能会遗漏某些重要的点。基于此Dapper有以下三个最重要的设计目标:低的额外开销,对应用的透明性,可扩展。同时产生的跟踪数据须要能够被快速分析,这样能够帮助用户实时获取在线服务状态。git
实现方法github
低的额外开销:不是对全部的请求进行跟踪而是要采样,收集跟踪数据时进行二次采样web
对应用的透明性:修改多线程,控制流,RPC等基础库代码,插入负责跟踪的代码。在Google,应用使用的是相同的多线程,控制流,RPC等基础库代码,因此仅经过修改它们就能够实现跟踪功能。当线程进行一个被跟踪的处理时,Dapper会将一个trace context关联到线程本地化存储中。trace context中包含了span相关属性,好比trace和span id。对于须要进行异步处理的状况,Google开发者一般都会采用一个通用的控制流库来实现回调,并将它们调度到一个线程池或是执行器中调用。Dapper保证全部回调都会保存它们建立者的trace context,同时在该回调被调用时该trace context也会被关联到对应线程。这样,Dapper就能够实现这种异步处理过程的跟踪。对于被跟踪的RPC调用,span和trace id也会跟着被从客户端传到服务端。从功能上看这部分代码主要包括span的建立,采样,本地磁盘日志写入,可是由于它会被不少应用所依赖,维护和bug fix难度高,须要很是高的稳定性和健壮性。同时还要轻量,实际上这部分代码的C++实现也总共不到1000行。缓存
Dapper支持用户直接获取Tracer对象,并输出本身的自定义信息,用户能够输出本身任意想输出的内容,为防止用户过分输出,提供用户可配置参数来控制其上限。服务器
跟踪时须要对请求进行标记,会产生一个惟一ID(在Dapper中是一个64位整数)用来标识该请求。对于Dapper来讲,一个trace(跟踪过程)其实是一颗树,树中的节点被称为一个span,根节点被称为root span。以下图所述:网络
须要注意的是一个span可能包含来自多个主机的信息;实际上每一个RPC span包含了来自客户端和服务端的信息。可是客户端和服务端的时钟是有误差的,文中并未指明如何解决这个问题,只是说能够利用以下事实"RPC的发起客户端是在服务端接收到前进行地,针对该RPC的响应则是由服务端在客户端收到前发出的",肯定时间戳的一个上下界。多线程
Dapper的整个数据收集过程以下图所示:首先将span数据写入本地日志文件,而后将数据收集并写入Bigtable,每一个trace记录将会被做为表中的一行,Bigtable的稀疏表结构很是适合存储trace记录,由于每条记录可能有任意个的span。整个收集过程是out-of-band的,与请求处理是彻底不相干的两个独立过程,这样就不会影响到请求的处理。若是改为in-band的,即将trace数据与RPC响应报文一块发送回来,会影响到应用的网络情况,同时RPC调用也有可能不是完美嵌套的,某些RPC调用可能会在它自己依赖的那些返回前就提早返回了。app
Dapper提供API容许用户直接访问这些跟踪数据。Google内部开发人员能够基于这些API开发通用的或者面向具体应用的分析工具。这一点,对于提升Dapper的做用和影响力起到了出人意料的效果。
跟踪开销
若是跟踪带来的额外开销过高,用户一般会选择关掉它,所以低开销很是重要。采样能够下降开销,可是简单的采样可能致使采样结果无表明性,Dapper经过采用自适应的采样机制来知足性能和表明性两方面的需求。
trace的生成开销对Dapper来讲是最关键的,由于数据的收集和分析能够临时关掉,只要数据一直生成就能够后面再进行收集分析。在trace生成中,最大头的地方在于span和annotation的建立和销毁上。根span的建立和销毁平均须要204ns,普通span只须要176ns,区别在于根span须要产生一个全局惟一的trace id。若是span没有被采样到,那么对它添加annotation的开销基本可忽略,大概须要9ns。可是若是是被采样到的话,那么平均开销是40ns。这些测试是在一个2.2GHZ的x86服务器上进行的。本地磁盘写入是Dapper运行库中最昂贵的操做,可是它们能够异步化,批量化,所以它们基本上只会影响到那些高吞吐率的应用。
将跟踪数据经过Dapper后台进程读出也会产生一些开销。可是根据咱们的观察,Dapper daemon的CPU开销始终在0.3%之下,内存占用也不多,此外也会带来轻量的网络开销,可是每一个span平均只有426字节大小,网络开销只占了整个产品系统流量的0.01%不到。
对于那些每一个请求均可能产生大量跟踪数据的应用来讲,咱们还会经过采样来下降开销。咱们经过保证跟踪开销能够始终保持在很低的水平上,使得用户能够放心大胆的使用它。最初咱们的采样策略很简单,是在每1024个请求中选择一个进行跟踪,这种模式对于那种请求量很高的服务来讲,能够保证跟踪到有价值的信息,可是对于那些负载不高的服务来讲,可能会致使采样频率太低,从而遗漏重要信息。所以咱们决定以时间为采样单位,保证单位时间内能够进行固定次数的采样,这样采样频率和开销都更好控制了。
应用
用户能够经过DAPI(Dapper“Depot API”)直接访问跟踪数据。DAPI提供以下几种访问方式:指定trace id进行访问;大规模批量访问,用户能够经过MapReduce job并行访问,用户只须要实现一个以Dapper trace为参数的虚函数,在该函数内完成本身的处理便可,框架负责为用户指定时间段内的全部trace调用该函数;经过索引进行访问,Dapper会为跟踪数据创建索引,用户可经过索引进行查询,由于trace id是随机生成的,所以用户一般须要经过服务名或机器名进行检索(实际上Dapper是按照(服务名,机器名,时间戳)进行索引的)。
大部分用户都是经过一个交互式web接口来使用Dapper的,典型流程以下图所示:
1.用户输入他感兴趣的服务和时间窗口,选择相应跟踪模式(这里是span名称),以及他最关心的某个度量参数(这里是服务延迟)
2.页面上会展现一个指定服务的全部分布式执行过程的性能摘要,用户可能会对这些执行过程根据须要进行排序,而后选择一个详细看
3.一旦用户选定了某个执行过程后,将会有一个关于该执行过程的图形化描述展示出来,用户能够点击选择本身关心的那个过程
4.系统根据用户在1中选择的度量参数,以及3中选择的具体过程,显示一个直方图。在这里显示的是有关getdocs的延迟分布的直方图,用户能够点击右侧的example,选择具体的一个执行过程进行查看
5.显示关于该执行过程的具体信息,上方是一个时间轴,下方用户能够进行展开或折叠,查看该执行过程各个组成部分的开销,其中绿色表明处理时间,蓝色表明花在网络上的时间。
经验教训
在开发过程当中使用Dapper,能够帮助用户提升性能(分析请求延迟,发现关键路径上没必要要的串行化),进行正确性检查(用户请求是否正确发送给了服务提供者),理解系统(请求处理可能依赖不少其余系统,Dapper帮助用户了解整体延迟,从新设计最小化依赖),测试(新代码release前要经过一个Dapper跟踪测试,验证系统行为和性能)。好比,经过使用Dapper,Ads Review团队将延迟下降了两个数量级。
同时咱们还将跟踪系统与异常监控系统进行集成,若是异常发生在一个采样到的Dapper tracer上下文中,那么相应的trace和span id还会被做为异常报告的元数据,前端的异常监控服务还会提供相应连接指向跟踪系统。这样能够帮助用户了解异常发生时的状况。
解决长尾延迟,帮助用户分析复杂系统环境下的延迟问题。网络性能的瞬时降低不会影响系统的吞吐率,可是对延迟有很大影响。不少开销昂贵的查询模式是因为未预料到的服务间的交互形成的,Dapper的出现使得这种问题的发现变得很是容易。
帮助用户进行服务间依赖的推理。Google维护了很是多的集群,每一个集群上承载了各类各样的任务,而任务间可能存在依赖关系。可是各个任务须要精确知道它所依赖的服务信息,以帮助发现瓶颈或进行服务的移动。服务间的依赖是复杂的并且是动态的,单纯依赖配置文件很难判断。可是经过使用Dapper的trace信息和DAPI MapReduce接口能够自动化的肯定服务间依赖关系。
帮助网络管理员对跨集群的网络活动进行应用层的分析。帮助发现某些昂贵的网络请求开销的产生缘由。
不少存储系统都是共享的。好比GFS,会有不少用户,有的多是直接访问GFS,有的多是好比经过Bigtable产生对GFS的访问,若是没有Dapper,对这种共享式系统将会很难调试,经过Dapper提供的数据,共享服务的owner能够方便的对用户根据各项指标(好比网络负载,请求耗时)进行排序。
救火。可是不是全部的救火,均可以使用Dapper来完成。好比,那些正在救火的Dapper用户须要访问最新的数据,可是他可能根本没有时间写一个新的DAPI代码或者等待周期性报告的产生。对于那些正在经历高延迟的服务,Dapper的用户接口并不适于用来快速定位延迟瓶颈。可是能够直接与Dapper daemon进行交互,经过它能够很容易地收集最新数据。在发生灾难性的故障时,一般没有必要去看统计结果,单个例子就能够说明问题。可是对于那些共享存储服务来讲,信息聚合会很重要。对于共享服务来讲,Dapper的聚合信息能够用来作过后分析。可是若是这些信息的聚合没法在问题爆发后的10分钟以内完成,那么它的做用将会大大削弱,所以对于共享服务来讲,Dapper在救火时的效果没有想象中的那么好。
经过开放跟踪数据给用户,激发了用户创造力,产生了不少意料以外的应用。那些没有跟踪功能的应用只须要用新的库从新编译下它们的程序,就得到了跟踪功能,迁移很是方便。
可是还存在以下一些须要改进的地方:
合并产生的影响。咱们一般假设各类子系统会一次处理一个请求。可是某些状况下,请求会被缓存,而后一次性地在一组请求上执行某个操做(好比磁盘写入)。在这种状况下,被追踪的请求拿到的实际上并非它自己的处理过程。
对批量处理进行跟踪。虽然Dapper是为在线服务系统而设计,最初是为了理解用户发送请求给Google后产生的一系列系统行为。可是离线的数据处理实际上也有这样的需求。在这种状况下,咱们能够将trace id与某些有意义的工做单元进行关联,好比输入数据里的一个key(或者是一个key range)。
寻找根本缘由。Dapper能够迅速找到系统短板,可是在寻找根本缘由时并不那么高效。好比某个请求变慢可能并非由于它本身的缘由,而是由于在它以前已经有不少请求在排队。用户能够在应用层将某些参数,好比队列大小交给跟踪系统。
记录内核级信息。咱们有不少工具能够进行内核执行过程的跟踪和profiling,可是直接将内核级信息捆绑到一个应用层的trace context中很难优雅的实现。咱们打算采用一种折中的解决方案,经过在应用层获取内核活动参数的快照,而后将它们与一个活动span关联起来。
http://bigbully.github.io/Dapper-translation/