Google Dapper-大规模分布式系统的基础跟踪设施
[说明:本文是阅读Google论文“Dapper, a Large-Scale Distributed Systems Tracing Infrastructure”之后的一个简要总结,完整译文可参考此处。 另论文“Uncertainty in Aggregate Estimates from Sampled Distributed Traces”中有关于采样的更详细分析。此外,Twitter开源的Zipkin就是参考Google Dapper而开发。]
Dapper最初是为了追踪在线服务系统的请求处理过程。比如在搜索系统中,用户的一个请求在系统中会经过多个子系统的处理,而且这些处理是发生在不同机器甚至是不同集群上的,当请求处理发生异常时,需要快速发现问题,并准确定位到是哪个环节出了问题,这是非常重要的,Dapper就是为了解决这样的问题。
对系统行为进行跟踪必须是持续进行的,因为异常的发生是无法预料的,而且可能是难以重现的。同时跟踪需要是无所不在,遍布各处的,否则可能会遗漏某些重要的点。基于此Dapper有如下三个最重要的设计目标:低的额外开销,对应用的透明性,可扩展。同时产生的跟踪数据需要可以被快速分析,这样可以帮助用户实时获取在线服务状态。
实现方法
低的额外开销:不是对所有的请求进行跟踪而是要采样,收集跟踪数据时进行二次采样
对应用的透明性:修改多线程,控制流,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调用可能会在它本身依赖的那些返回前就提前返回了。
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关联起来。