点击订阅《云荐大咖》栏目,获取官方推荐优质内容,学技术不迷路!本文以Dapper论文为切入点,延伸至其相关论文内容,结合历史时间轴发展的线索,向读者展示软件从业者对链接跟踪技术的探索与实践。带着疑问回顾历史,提到链路追踪,大多数人会想到Zipkin、Jaeger、Skywalking这些相对成熟的链路追踪开源软件,以及Opentelemetry、OpenTracing、OpenCensus等开源标准。尽管实现方式不同,但使用各种软件、标准和实现方式组合构建的不同链接跟踪系统之间存在许多相似之处。例如,这些链接跟踪系统都需要在调用链接上传播元数据。他们对元数据内容的定义也类似,比如链接的唯一traceid,父链接关联的parentid,标识自己的spanid。都是异步去中心化的跟踪信息上报和收集,离线聚合和聚合跟踪环节。他们都有链接采样等等。链接跟踪系统架构和模型的设计看起来如此相似,我不禁有一些疑问:开发人员在设计链接跟踪时是否有相同的想法?为什么要在调用链接中传递元数据?元数据中的这些信息是否必要?我可以在不侵入和修改代码的情况下访问链接跟踪系统吗?为什么异步分散报告和离线聚合?设置链接采样有什么用?带着种种疑问,我找到了这众多链接跟踪软件的灵感来源——《Google Dapper》论文,并阅读了原文和相关被引论文。这些论文逐渐解开了我心中的疑惑。黑盒模式探索在早期学术界对分布式系统中链路状态检测的探索中,有人认为分布式系统中的每个应用程序或中间件都应该是一个黑盒,链路检测不应该侵入应用程序。系统内部。那时候Spring还没有开发出来,控制反转、切面编程等技术还不是很流行。如果需要侵入应用代码,需要修改应用代码。对于工程师来说,额外的准入门槛太高了。链接检测工具将难以普及。如果不允许侵入应用程序修改代码,则只能从应用程序外部获取并记录链接信息。但是由于黑盒的限制,链接信息比较分散,无法串联起来。如何将这些环节串联起来,就成了需要解决的问题。《Performance Debugging for Distributed Systems of Black Boxes》这篇论文发表于2003年,是对黑盒模式下调用链监控的一次探索。在本文中,提出了两种查找链接信息的算法。第一种算法称为“嵌套算法”。首先,通过生成一个唯一的id,将一次跨服务调用的request(1call)链接和return(11return)链接关联起来,形成一个链接对。然后利用时间顺序将不同的往返链路对关联为层级关联或上下级关联(参见图1)。图1如果应用是单线程的,这个算法是没有问题的。生产应用往往是多线程的,所以使用这种方法不能很好地找到链接之间的对应关系。虽然论文提出了一种scoreboardpenalty的方法,可以去除一些错误关联的链接关系的权重,但是这种方法对于一些基于异步RPC调用的服务存在一些问题。另一种算法称为“卷积算法”,它将往返链路视为独立链路,然后将每个独立链路对视为时间信号,并利用信号处理技术找到信号之间的相关性。该算法的优点是可以用在基于异步RPC调用的服务上。但是,如果实际调用链路中存在回环,则卷积算法可以得到除实际调用链路之外的其他调用链路。比如调用链接A->B->C->B->A,卷积算法不仅获取了自己的调用链接,还获取了A->B->A的调用链接。如果某个节点在一个链接上多次出现,那么这个算法很可能会得到大量的派生调用链接。在黑盒模式下,通过概率统计的方法判断链接之间的关系。概率和统计永远是概率,没有办法准确获取链接之间的关系。另一种思路如何才能准确获取到调用链接之间的关系呢?下面的论文给出了一些思路和做法。Pinpoint:ProblemDeterminationinLarge,DynamicInternetServices注意:这个Pinpoint不是github上的pinpoint-apm。本文的研究对象主要是具有不同组件的单个应用程序。当然,相应的方法也可以推广到分布式集群。在论文中,Pinpoint的架构设计主要分为三个部分。参考图2,Tracing和TraceLog是第一部分,叫做ClientRequestTrace,主要用来收集链接日志。InternalF/D、ExternalF/D和FaultLog是第二部分,是故障检测信息(FailureDetection),主要用来收集故障日志。统计分析是第三部分,称为数据聚类分析,主要用于对采集到的日志数据进行分析,得出故障检测结果。图2在Pinpoint架构中,设计了一种可以有效用于数据挖掘分析方法的数据。如图3所示,每个调用链接都作为样本数据,标有唯一标识requestid,样本的属性记录调用链接经过的程序组件(Component)和失败状态(Failure)。图3为了能够将每次调用的链接日志(TraceLogs)和故障日志(FaultLogs)关联起来,本文以Java应用程序为例,介绍如何在代码中实现这些日志的关联。下面是Pinpoint实践章节的一些重点总结:需要为每个组件生成一个组件id,为每个http请求生成一个唯一的requestid,通过线程局部变量(ThreadLocal)向下传递,供请求中的新线程,需要修改线程创建类,继续传递请求id。对于请求中产生的rpc调用,需要修改请求端的代码,将requestid信息带入header中,每次在接收端解析header注入到线程局部变量中叫。一个组件(component),只是用(requestid,componentid)记录一个TraceLog。对于java应用来说,这几点实践起来简单,可操作性高。传播提供了基本思想。这篇论文发表于2002年,当时Java版本是1.4,已经具备了线程局部变量(ThreadLocal)的能力,在线程中携带信息相对容易。但是因为那个年代切面编程还不是很流行(2003年出现了Spring,javaagent在java1.5才有能力,2004年才发布),所以这种方式并没有得到广泛的应用。如果反过来想,可能正是因为这些编程需求的出现,才推动了java切面编程领域的技术进步。重构调用链路X-Trace:APervasiveNetworkTracingFramework本文主要研究分布式集群中的网络链路。X-Trace论文延续和扩展了Pinpoint论文的思路,提出了一个可以重构完整调用链路的框架和模型。为了实现该目标,本文定义了三个设计原则:1.在调用链路中携带元数据(调用链路中传输的数据也称为带内数据,入站数据)2.上报链路信息不存储在调用链接中,收集链接信息的机制需要与应用程序本身正交(注:不存储在调用链接中的链接数据也称为出界数据)3.注入元数据的实体应该与收集报告的实体分离。原则1和2是迄今为止一直沿用的设计原则。原则1是Poinpont思想的延伸。Linktransfer在原有requestid的基础上扩展了更多元素,其中TaskID、ParentID、OpID是traceid、parentid、spanid的前身。跨度这个词也出现在X-Trace论文的摘要中,也许是Dapper作者对X-Trace论文作者的致敬。我们看一下X-Trace对元数据的定义:1.Flags是一个位数组,用来标记是否使用了TreeInfo、Destination、Options2.TaskID是一个全局唯一的id,用来标识一个唯一的调用链3.TreeInfoParentID-父节点id,调用链中唯一的OpID-当前操作id,调用链中唯一的EdgeType-NEXT表示兄弟关系,DOWN表示父子关系4.Destination用于指定上报地址,用于数据的定义,论文还定义了两个链接传播的操作,即pushDown()和pushNext()。pushDown()表示将元数据复制到下一层,pushNext()表示将元数据从当前节点传播到下一个节点。图4pushDown()和pushNext()伪代码图5pushDown()和pushNext()操作在调用环节的执行位置X-Trace上报环节数据的结构设计原则上遵循第二种设计.如图6所示,X-Trace为应用程序提供了一个轻量级的客户端包,使应用程序可以将链接数据转发给本地的守护进程。本地守护进程打开一个UDP协议端口,接收客户端包发来的数据,放入队列中。队列的另一端根据链路数据的具体配置信息发送到相应的地方,可以是数据库,也可以是数据转发服务、数据采集服务或数据聚合服务。图6X-Trace上报链路数据的架构设计对当今市场上链路跟踪的实现有着相当大的影响。对比Zipkin的collector和Jeager的jaeger-agent,多少能看出X-Trace的影子。X-Trace的三大设计原则,带内和带外数据的定义,元数据传播操作的定义,链路数据上报框架,都是目前链路追踪系统的参考。对比Zipkin的collector和Jeager的jaeger-agent,一定程度上可以看出X-Trace链路数据上报架构的影子。大规模商业实践——DapperDapper,一个大规模分布式系统追踪基础设施Dapper是谷歌用来为开发者提供复杂分布式系统行为信息的内部系统。Dapper论文介绍了Google在设计和实践这种分布式链接跟踪基础设施方面的经验。Dapper论文发表于2010年,论文称Dapper系统在谷歌内部已经有两年的实践经验。Dapper系统的主要目的是为开发人员提供有关复杂分布式系统行为的信息。本文分析了实现这样一个系统需要解决什么样的问题。针对这些问题,提出了两个基本的设计需求:大规模部署和持续监控。针对这两个基本设计需求,提出了三个具体的设计目标:低开销:链路跟踪系统需要保证对在线服务的性能影响可以忽略不计。即使是很小的监控开销也会对一些高度优化的服务产生明显的影响,甚至迫使部署团队关闭跟踪系统。应用程序级透明度:开发人员不应了解链接跟踪功能。如果链接跟踪系统需要依赖应用程序级开发人员的帮助才能工作,那么链接跟踪功能将变得非常薄弱,并且经常由于错误或疏忽而无法正常工作。这违反了大规模部署的设计要求。可扩展性:链接跟踪系统需要能够满足未来几年谷歌服务和集群的规模。虽然Dapper的很多设计理念与Pinpoint、Magpie和X-Trace相似,但Dapper也有一些自己独特的设计。其中之一就是为了达到低开销的设计目标,Dapper对请求链接进行了采样和采集。根据Dapper在Google的实践经验,对于很多常用的场景,即使是采样收集1/1000的请求也能得到足够的信息。另一个独特的特点是它们实现了非常高的应用透明度。这是由于谷歌的应用集群部署的同质性比较高。他们可以将链接跟踪工具的实现代码限制在软件的底层,而无需在应用程序中添加额外的注释信息。例如,如果集群中的应用程序使用相同的http库、消息通知库、线程池工厂和RPC库,则可以将链接跟踪工具限制为这些代码模块。如何定义链接信息?文章首先给出了一个简单的调用链示例,如图7所示。笔者认为,要对一个请求做分布式追踪,需要收集消息的标识码以及消息对应的事件和时间.如果只考虑RPC,调用环节可以理解为RPC的嵌套树。当然,Google的内部数据模型并不仅限于RPC。X-Trace报告链接数据的架构设计对当今市场上链接跟踪的实现有相当大的影响。对比Zipkin的collector和Jeager的jaeger-agent,多少能看出X-Trace的影子。X-Trace的三大设计原则,带内和带外数据的定义,元数据传播操作的定义,链路数据上报框架,都是目前链路追踪系统的参考。对比Zipkin的collector和Jeager的jaeger-agent,一定程度上可以看出X-Trace链路数据上报架构的影子。图7和图8说明了Dapper的跟踪树的结构。树的节点是基本单元,称为跨度。边是父跨度和子跨度之间的连接。一个span就是简单的带有开始和结束时间戳,RPC耗时或者应用相关的注解信息。为了重建Dappertrackingtree,span还需要包含以下信息:spanname:一个易于阅读的名称,如图8中的Frontend.Requestspanid:一个64位的唯一标识符parentid:父spanid图8和图9是RPC跨度的详细信息。值得一提的是,同一个跨度可能包含多个主机的信息。事实上,每个RPC跨度都包含用于客户端和服务器处理的注释。由于客户端的时间戳和服务端的时间戳来自不同的主机,所以需要特别注意这些时间的异常情况。图9是一个span的详细信息。图9如何实现应用程序级透明性?Dapper通过在一些常用包中添加测量点,为应用开发者实现分布式链路跟踪,不受干扰。主要做法如下:当一个线程正在处理链接跟踪路径时,Dapper会将跟踪上下文设置为与线程本地存储相关联。Trackingcontext是一种小而易于复制的span信息。如果计算过程是延迟的或者一步到位的,大部分谷歌开发者会使用通用的控制流库来构造回调函数,使用线程池threadpool或者其他executor来调度。这样,Dapper可以保证所有的回调函数在创建时都会保存跟踪上下文,并且在执行回调函数时将跟踪上下文与正确的线程相关联。Google几乎所有的线程内通信都是建立在RPC框架之上的,包括C++和Java的实现。该框架添加了用于定义所有RPC调用相关跨度的度量。在跟踪的RPC中,跨度和跟踪ID从客户端传递到服务器。在谷歌,这是一个非常必要的衡量点。最后,Dapper论文给出了易读且有助于问题定位的数据模型设计、应用级透明测量实践、低开销设计方案,为链路跟踪在工业应用中的使用扫清了诸多障碍并激发了众多开发者的灵感。自从GoogleDapper论文发表以来,许多开发人员受到该论文的启发并开发了各种链接跟踪。2012年Twitter开源了Zipkin,Naver开源了Pinpoint,2015年WuSheng开源了Skywalking,Uber开源了Jaeger等。从此,链接跟踪进入了百家争鸣的时代。《云荐大咖》是腾讯云家社区的优质内容栏目。云推荐官特邀行业领袖,聚焦前沿技术落地与理论实践,持续为您解读云时代热点技术,探索行业发展新机遇。点击一键订阅,我们会定期为您推送优质内容。
