现代互联网通常被实现为复杂的大规模分布式微服务系统。这些应用可能是由不同团队使用不同编程语言开发的软件模块集合构建的,并且可能跨越数千台计算机及多个物理设备。在这样的环境中,有一个帮助理解系统行为和关于性能问题推理的工具显得非常宝贵。这里介绍由Google生产的分布式系统链路追踪系统Dapper的设计,并描述其设计目标如何满足大规模系统低开销、对应用透明性和广泛的覆盖部署
设计目标介绍
分布式服务追踪是整个分布式系统中跟踪一个用户请求的过程,包括数据采集、数据传输、数据存储、数据分析和数据可视化,捕获此类跟踪让我们构建用户交互背后的整个调用链的视图,这是调试和监控微服务的关键工具。GoogleDapper就是这样需求下的一套应用于大型分布式系统环境下的链路追踪系统。借助Dapper理念,系统开发人员只需将Trace组件嵌入到基础通用库中,就可以正常运行,而应用层开发者则不需要关心具体Trace组件实现、集成方式,达到以应用层透明的方式嵌入各个模块的目的
Dapper与其他追踪系统存在概念上的相似之处,特别是PinPoint、Magpi和X-Trace,但是某些设计选择是其在Google的环境中取得成功的关键,例如使用抽样和将指令限制到相当少量的的公共库。Dapper最初作为一个独立的追踪工具,后来发展成为一个监控平台,为Google的开发者提供有关复杂分布式系统行为的更多信息。这样的系统特别令人感兴趣,因为大规模小型服务集合是一个特别具有成本效益的互联网服务工作负载平台。理解此上下文系统行为需要观察许多不同程序和机器上的相关活动
网络搜索示例将能够说明这样一个系统需要解决的一些挑战。一个前端服务可以将Web查询分发给数百个查询服务,每个查询服务在其自己的索引中搜索。该查询还可以被发送到许多其他子系统,这些子系统可以处理广告、检查拼写或寻找专门的结果,包括图像、视频、新闻等,所有这些服务的结果有选择地组合在结果页面中,我们称这个模型为“通用搜索”。总共可能需要数千台机器和许多不同的服务来处理一个通用搜索查询。此外,网络搜索用户对延迟很敏感,这可能是由于任何子系统的性能不佳造成的。工程师仅仅考虑延迟可能知道存在问题,但可能无法猜测哪个服务有问题,也无法猜测其行为不良的原因。首先,工程师可能无法准确知道正在使用哪些服务,每周都可能加入新服务和修改部分服务,以增加用户可见的功能,并改进性能和安全性等其他方面;其次,工程师不可能是所有内部微服务的专家,每一个微服务可能有不同团队构建和维护;第三,服务和机器可以由许多不同的客户端同时共享,因此性能问题可能是由于另一个应用的行为引起
上述场景给Dapper提出了两个基本要求:覆盖面的广度和持续监控。覆盖面的广度,因为即使系统的一小部分未被监控,追踪系统是不是值得信任都会被人质疑。此外,监控应该是7X24的,因为通常情况下,异常或其他值得注意的系统行为难以或不可能再现。根据这两个明确的要求推出以下三个具体的设计目标
低开销分布式系统对性能和资源要求都很严格,Trace组件对服务的影响必须足够小。一些高度优化的服务中,即使很小的监控开销也很容易察觉到,并且可能迫使部署团队关闭追踪系统应用级透明Trace组件嵌入到基础通用库中,以提高稳定性,应用开发者不需要关心它们。可扩展性至少在未来几年内它需要处理Google服务和集群规模一个额外的设计目标是--追踪数据产生后可以快速导出数据并快速进行分析,在最短时间内定位系统异常,理想情况是数据存入追踪仓库后一分钟内就能统计出来。尽管追踪分析系统使用一小时前的旧数据进行分析依然相当有价值,但如果追踪系统能够提供足够快的信息反馈,就可以对生产环境下的异常状况做出快速反应
要真正做到应用级别的透明可能是最具有挑战的设计目标,通过将Dapper的核心追踪代码做的很轻巧,然后把它植入到那些无所不在的公共组件中,比如线程调用,控制流以及RPC库。使用自适应采样有助于使系统变得具有可扩展并降低性能损耗。结果展示的相关系统也需要包含一些用来收集跟踪数据的代码,用来图形化的工具,以及用来分析大规模跟踪数据的库和API,虽然单独使用Dapper有时就足够让开发人员查明异常来源,但是Dapper的数据往往侧重性能方面的调查,其初衷也并非取代所有其他监控工具
这些要求产生来三个具体的设计目标
低开销追踪系统对运行服务的影响能够忽略不计,在一些高度优化的服务中,即使很小的监控开销也很容易引起注意,并且可能迫使部署团队关闭追踪系统对应用透明可扩展性至少在未来几年内它需要处理Google服务和集群规模一个额外的设计目标是追踪数据产生后可以快速进行分析:理想情况下在一分钟之内。尽管追踪分析系统使用数小时内的追踪数据进行分析依然十分有价值,新信息的可用性使得能够对产生异常作出更快地反应。
真正对应用透明可能是最具有挑战的设计目标,通过将Dapper的核心追踪工具限制为一个无处不在的线程,控制流和RPC库代码的小型语料库来实现的。使用自适应采样有助于减少开销使系统具有可扩展性
分布式链路追踪
当我们进行微服务架构开发时,通常会根据业务或功能划分成若干个不同的微服务模块,各模块之间通过REST/RPC等协议进行调用。一个用户在前端发起某种操作,可能需要很多微服务的协同才能完成,如果在业务调用链路上任何一个微服务出现问题或者网络超时,将导致功能失效,或出现某些模块在调用链中耗时突然增大等问题。随着业务越来越复杂,微服务架构横向、纵向扩展之后,其规模越来越大,对于微服务之间的调用链路的分析将会越来越复杂。此时你会发现,如果在最初架构设计时,能够将分布式链路追踪这种需求考虑进来,后期微服务集群扩容时候,前期所做的工作将会达到事半功倍的效果
一个简单常见的服务调用示例:图中,A-E分别表示5个微服务,用户发起一个请求RequestX到达前端A,它将分别向B、C服务发送RPC,服务B可以立即响应,服务C需要等服务D、E工作返回后才能响应服务A,A收到B和C的响应后以ReplyX作为消息返回用户RequestX请求
图1服务调用示例以上一个完整的调用回路中,一次请求需要经过多个系统处理完成,并且追踪系统像是内嵌在RPC调用链上的树形结构,然而,我们的核心数据模型不仅限于特定的RPC框架;我们还会追踪Gmail中的SMTP会话,来自外部世界的HTTP请求以及SQL服务的出站查询。形式上,分布式链路追踪使用Trace树形结构来记录请求之间的关系(父子关系、先后顺序等)
TraceTrees和Span
Trace的三个主要构成元素
Span基本工作单元,例如,在一个新建的Span中发送一个RPC等同于发送一个回应请求给RPC,Span通过一个64位ID唯一标识,Trace以另一个64位ID表示。Span还有其他数据信息,比如摘要、时间戳事件、关键值注释(Tags)、Span的ID、以及进度ID(通常是IP地址)。Span在不断地启动和停止,同时记录了时间信息,当你创建一个Span,你必须在未来的某个时刻停止它。将两个服务例如上面图1中的服务A和服务B的请求/响应过程叫做一次SpanTracetrees在分布式追踪系统中使用Trace表示对一次请求完整调用链的追踪。可以看出每一次跟踪Trace都是一个树型结构,Span可以体现出服务之间的具体依赖关系Annotation用来及时记录一个事件的存在,一些核心Annotation用来定义一个请求的开始和结束分布式链路追踪就是记录每个服务上发送和接收的每条消息的消息标识符和时间戳事件的集合,要将所有记录条目与给定的发起者(RequestX)关联上,以形成一个完整调用链。目前已提出两类聚合该信息的解决方案:黑盒和基于注解的监控方案。黑盒方案假设除了上述消息记录之外没有其他信息,并使用统计回归技术来推断该关联。基于注释的解决方案(Dapper)依赖应用程序或中间件使用全局标识符精确地为每一条记录打标签,该标识符将这些消息记录链接到原始请求。虽然黑盒方案比基于注解的方案更便携,但由于它们依赖于统计推断,它们需要更多数据才能获得足够的准确性。显然,基于注解的方法的主要缺点是需要工具程序。在我们的环境中,由于所有应用程序都使用相同的线程模型、控制流和RPC系统,因此我们发现可以将检测限制在一小组公共库中,并实现一个能对应用程序开发人员有效且透明的监控系统
对于每一个Trace树,定义一个全局概率唯一64位的整数TraceID。追踪系统中用时间戳Span记录一个服务调用的开始和结束时间--时间区间,每一个Span有一个ParentSpanID和自身的SpanID,没有ParentSpanID的Span称为RootSpan,当前Span的ParentSpanID级为调用链路上游的SpanID,所有的Span都关联到一个特定的Trace并共享该Trace的TraceID
图2DapperTraceTree下图提供了一个典型Dapper追踪Span中记录事件的更详细视图,这个特定的Span描述了上图中两个Helper.CallRPC的周期。Dapper的RPC库工具记录了Span开始和结束时间以及任何RPC时间信息。如果应用程序所有制选择使用自己的注解(如图foo注解)来扩充追踪,则这些注解也会与其余的Span数据一起记录
Span可以包含来自不同主机的信息,事实上,每一个RPCSpan可能包含客户端和服务端两个过程的Annotations内容。由于客户端和服务端时间戳来源于不同主机,我们必须考虑Server之间的时钟偏差。在我们的链路追踪分析工具中,利用客户端发送请求总是在服务端接收它之前,服务端响应消息总是在接收消息之后这样一个事实,这样每个服务端的RPC调用请求就有一个时间戳的上限和下限
图3Span细节从图3可以很清楚的看出,这是一个Span名为Helper.Call的调用,SpanID是5,SpanParentID是3,TraceID是。我们重点看看Span对应的四种状态
ClientSend(CS)客户端发起一个请求的发送时间,这个Annotation描述这个Span的开始ServerReceived(SR)服务端接收时间,服务端获得请求并准备开始处理它。如果将SR减去CS便可获得网络延迟ServerSend(SS)服务端发送时间,Annotation表明请求处理的完成(当请求返回客户端)。如果SS减去SR便可获得服务端处理请求的时间ClientReceived(CR)客户端接收时间,表明Span的结束,客户端成功接收到服务端的响应,如果CR减去CS便可获得客户端从服务端获取回复所需时间通过收集这四个时间戳,就可以在一次请求完成之后计算出整个Trace的执行耗时和网络耗时,以及Trace中每个Span过程的执行耗时和网络耗时
服务调用耗时=CR-CS服务处理耗时=SS-SR网络耗时=服务调用耗时-服务处理耗时Dapper在应用开发人员几乎零干预的情况下能够遵循分布式控制路径,几乎完全依赖于一些常用库工具
当线程处理追踪的控制路径时,Dapper将追踪上下文附加到线程本地存储。追踪上下文是一个小且易于复制的span属性容器,例如trace和spanid当计算延迟或异步时,大多数Google开发人员使用公共控制流库来构造回调并在线程池或其他执行程序中调度它们。Dapper确保所有此类回调都存储其创建者的追踪上下文,并且在调用回调时,此追踪上下文与响应的线程相关联。通过这种方式,用于追踪重建的DapperId能够透明地遵循异步控制路径几乎所有Google的进程间通信围绕一个带有C++和Java绑定的RPC框架构建的。我们已经使用该框架来定义围绕所有RPC的span,span和traceid从客户端传输到服务器以用于跟踪RPC三阶段收集Trace数据
Dapper追踪日志和收集管道是一个三阶段过程
Span数据写入本地日志文件由Dapper收集器将这些数据通过Dapper守护进程从所有主机中拉出来最终将其写入到Dapper的Bigtable仓库中。一次Trace被设计成Bigtable的一行,,每一列相当于一个SpanBigtable支持稀疏表格布局正适合这种情况,因为一个Trace可能会产生任意数量的Span。追踪数据收集的中位延迟(即数据从应用程序二进制文件采集并传播到中央存储库所需要的时间)不到15秒,随着时间的推移98%的延迟本身就是双峰;大约75%的时间,98%收集延迟不超过2分钟,但是其他大约25%的时间可以增长到几个小时
图4Dapper收集管道带外追踪收集
出于两个不相关的原因,Dapper系统使用请求树本身执行带外追踪记录和收集
首先,带内收集方案(追踪数据在RPC响应头中发回)可能会影响应用程序网络动态。在Google的许多大型系统中,寻找具有数千Span的Trace并不罕见,然而,RPC应答数据规模相对较小,通常不超过10KB。而头部数据往往非常庞大,在这种情况下,如果将二者放在一起传输带内Dapper追踪数据会“矮化”应答数据,影响后续分析其次,带内收集方案需要保证所有RPC都完全嵌套。我们发现有许多中间件系统在他们自己的后端返回最终结果之前将结果返回给调用者。带内收集无法考虑这种非嵌套的分布式执行模式Dapper部署状态
也许Dapper代码库中最关键部分的工具是RPC、线程和控制流库,包括Span创建、采样及记录到本地磁盘。除了轻量级外,代码还需要稳定和健壮,因为它链接了大量应用程序,使得维护和bug修复变得困难。核心工具不超过0行C++代码,在Java中不到行。健值注解的实现增加了行代码
可以从两个维度评估Dapper的渗透率:可以生成Dapper追踪的生产过程比例(即那些被Dapper工具运行时库链接的应用)和运行Dapper追踪收集守护进程机器的比例。Dapper守护进程是我们基本机器镜像的一部分,几乎存在于Google的每个服务器上
有些情况下,Dapper无法正确遵循控制路径。这些通常源于使用非标准控制流原语,或者当Dapper错误地将因果关系归因于不相关事件时。Dapper提供了一个简单的库来帮助开发人员手动控制追踪传播,目前有40个C++应用程序和33个Java应用程序需要一些手动追踪传播,相当于数千个传播中的一小部分
管理追踪开销
由于生成链路追踪和收集开销以及存储和分析追踪数据所需的资源量,追踪系统的成本会受到监控
追踪链路生成的开销是Dapper性能中最关键的部分,因为在紧急情况下收集和分析更容易被关闭。Dapper运行时库中最重要的追踪生成开销是创建和销毁Spans和Annotations并把它们记录到本地磁盘以供后续收集。RootSpan创建和解构平均需要纳秒,而NonRootSpan相同的操作需要纳秒,不同之处在于为RootSpan分配全局唯一TraceID的额外成本
追踪链路收集开销读取保存在本地磁盘的链路追踪数据还可能干扰被监控中的前台工作负载;将Dapper守护进程限制在内核调度程序中的最低优先级,以防在负载很重的主机中出现CPU争用;Dapper也是轻量的网络资源消费者,我们的存储库中每个Span平均对应个字节,作为被监控的应用程序中网络活动的一小部分,Dapper追踪数据收集占Google生产环境中不到0.01%的流量
对生产负载的影响每个使用大量机器的高吞吐量在线服务请求是最需要被有效追踪的,它们趋向于生产大量的追踪数据,它们也是对性能干扰敏感的
自适应采样归因于任何给定过程的Dapper开销与每单位时间处理样本追踪数成比例。Dapper的第一个生产版本对Google的所有流程使用了统一的采样频率,平均每个候选者采样一条Trace。这个简单的方案对高吞吐量的在线服务是有效的,因为绝大多数感兴趣的事件仍然可能经常出现以至于被捕获。但是较低流量的工作负载可能在如此低的采样率下丢失重要事件,同时容忍更高的采样率和可接受的性能开销。这种系统的解决方案是覆盖默认采样率,这需要我们在Dapper中试图避免人工干预。自适应采样方案通过每单位时间所需的采样追踪速率参数化。这样低流量的工作负载会自动提高其采样率,而流量非常高的负载会降低采样率从而控制开销。实际采样率跟Trace本身一起记录;这有助于在Dapper数据周围建立的分析工具中准确计算追踪频率
通用Dapper工具
Dapper从一个原型开始,迭代构建收集基础架构、程序接口及基于web交互的用户接口帮助Dapper用户独立解决他们的问题。以下介绍通用分析工具
DapperDepotAPI(DAPI)提供直接从Dapper区域存储仓库中访问分布式链路追踪记录,DAPI和Dapper追踪仓库被设计为串行,DAPI意味着为Dapper存储库中的原始数据提供一个清晰直观的界面。建议以下三种方式访问链路追踪数据通过TraceID访问DAPI可以根据给定的全局唯一TraceID加载任何Trace批量访问DAPI可以利用MapReduce并行提供对数十亿DapperTrace的访问,用户重写一个虚拟函数,该函数接受DapperTrace作为其唯一参数,并框架将在用用户指定的时间窗口内为每个收集的Trace调用一次该函数索引访问Dapper仓库支持单个索引匹配我们的公共访问模式。该索引从常用的追踪特征映射到不同的DapperTrace。由于TraceID是为随机分配的,因此这是快速访问与特定服务或主机关联Trace的最佳方法TheDapperuserinterface大多数Dapper使用都发生在基于web的交互式用户界面中,下图展示了典型的用户工作流
图5Dapper用户接口通用工作流小结
GoogleDapper分布式链路追踪,是诸多开源解决方案(Zipkin)的原型,它能够实时监控分析应用性能,将其应用于复杂分布式微服务架构中也能快速帮助定位故障,是日常运维排障不可多得的好帮手,也是学习分布式链路追踪的经典
推荐阅读
[1]dapperalarge-scaledistributedsystemstracinginfrastructure
(文章图片引用自Dapper论文)