注:本篇文章主要是作为自己看书后的总结,内容有可能会存在一些个人理解上的偏差,如果有网友找出问题欢迎提出,感谢!!!如果我理解上的错误误导了您,在此表示抱歉!!!
对于分布式链路追踪系统的设计我们在开始前可以提出这么几个问题:
0、什么是分布式链路追踪系统
1、分布式链路追踪系统在业界有哪些实现方法论
2、分布式链路追踪系统功能目标
3、分布式链路追踪系统技术目标
4、分布式链路追踪系统中数据模型应该如何设计
5、分布式链路追踪系统的损耗点会有哪些?如何处理。
6、跨度如何传播?对于跨线程、跨进程如何处理?
分布式追踪系统就是在分布式环境下,将一次业务请求的调用链路完整的记录下来,并能完整的展现出来。对于这样的要求,业界有哪些实现方式呢?就目前来说可以分为:基于规则、黑盒推测、元数据传播等三种方案。
对于这三种实现方案有何差别和优劣呢?基于规则和黑盒推测可以不需要向业务系统添加额外的代码,所以它们是零侵入的实现方案,或者说他们具有良好的应用透明性。由于没有向业务系统添加有关调用链路的追踪信息,所以它们只能通过预定义规则或者使用算法甚至机器学习的方法来判断服务间的调用关系。但是由于没有精确的追踪信息作为保证,这种方法出的调用链路很难保障链路的准确性和完整性。不仅如此,如果有新的链路加入很难保障之前的规则或者黑盒算法能适用于新链路,不具有伸缩性。简而言之,基于规则和黑盒推测并不是一个很好的方案,所以市面上的链路追踪系统基本看不到基于这两种方案来做实现。元数据传播虽然需要向业务系统植入代码,破坏了链路追踪系统的透明性,但是它却可以保障链路的准确性和伸缩性。所以如何在不破坏应用透明情况的下根据元数据传播为方法论设计出分布式链路追踪系统一直是建设分布式链路追踪系统要解决的首要问题。
由此可见这三那种分布式链路追踪系统设计方法论各有优劣,而Dapper论文的一大贡献就是确定了元数据传播在分布式追踪系统中的最终地位。Dapper论文认为分布式追踪系统需要满足这两个需求:一是无所不在的部署,二是持续的监控。
所谓的无所不在的部署指的是分布式链路追踪系统需要业务系统以及涉及到的子系统和第三方系统,如果存在追踪链路上的缺失,那么人们就有理由怀疑追踪系统的分析结果是不准确的。因为分布式链路追踪系统主要分析整个系统调用的瓶颈和异常点,如果整个系统中缺失了某几个子服务或者第三方链路,那么就可以怀疑性能与异常就出在这么几个缺失的服务中。
所谓的持续的监控就是要求链路追踪系统对业务系统需要保障7*24小时的追踪监控,因为业务发生异常是无法预知的,不持续的监控可能会错过异常信息。
从无所不在的部署和持续的监控这两个需求出发,就可以得到分布式追踪系统在设计上的三个目标:低损耗、应用级透明、可伸缩。
低损耗目标显而易见,对于无所不在的部署和持续的监控都意味着追踪系统的高损耗,有可能导致整个系统在性能上出现明显下降。任何一个系统在添加追踪能力后如果性能上出现明显下降,都会直接影响业务系统接入追踪系统的意愿。而追踪系统要是出现了链路的缺失又会导致链路信息的不完整,进而导致业务接入的意愿。所以低损耗是分布式追踪系统最基本也是最重要的一个设计目标。
应用级透明指的是业务系统在接入追踪系统时对业务系统是无感知的,也就是说业务系统在接入追踪系统时不需要做太多的修改甚至无需修改。应用级可以降低业务接入追踪系统的成本从而加强业务系统接入链路追踪系统的意愿。因为如果你是一个开发者,如果需要为了接入链路追踪系统做大量的修改,那么你肯定是不愿意的,甚至有的是在系统已经在线运行很久的老系统,为了接入一个还未验证的链路追踪系统要去承担风险。
可伸缩性要求为了应对业务系统的可扩展性,对于业务系统无论如何扩展,链路追踪系统都需要支持,不能因为业务系统的扩展导致链路追踪的失败、缺失。尤其是在某个服务调用了新的服务的时候,分布式追踪系统需要能都保障依然能完善的记录下新的链路并进行分析。Dapper论文以实现证明元数据的传播可以封装在一个很小的通用组件库中,从而保证准确性和可伸缩性的同时也维护了一定程度的应用透明性。 Dapper的方案虽然还不能做到完全不修改业务系统就能接入链路追踪系统,但是它对业务的侵入性已经降到极低程度,这个通用的组件库就是埋点库。另外说下skywalking通过-javaagent的方式来让业务系统接入。
元数据传播在Dapper论文中也称为基于标注的监控方法,它通过在业务系统中添加与追踪相关信息来维护服务间的调用关系。同时为了保障链路追踪的完整性,这些追踪信息会在业务处理过程中随着调用链路的延伸而传播到调用服务的链路中。显而易见的方法是给一次调用的起始处添加一个唯一的追踪标识来标记这次完整的业务调用(也就是traceId)。追踪标识只是Dapper追踪调用链路的必要信息,在追踪过程中还可以向追踪添加其他需求的信息,比如时间、名称等。这些信息在Dapper中被称为标注(Annotation),Dapper为了便于分析定义了一组核心标注。它们与追踪标识符一起会跟随链路一路扩散下去,直到业务请求处理完毕。通过将这些标注与追踪标识符收集和统一存储起来,人们不仅可以得到一个完整的调用链路,还可以根据标注分析出网络延时、业务处理时间等有用信息。现代分布式追踪系统基本上都是采用元数据传播的方案实现,从整体来说都由客户端和服务端两部分组成。客户端就是刚刚讨论的埋点库,它需要也被监控的业务系统集成在一起;而服务端通常需要独立部署,用于接收所有的埋点发送过来的追踪信息并对他们进行统一的存储和分析。
追踪埋点库会针对不同的语言提供对应的语言版本构件库。这样就可以根据业务系统所使用的语言选择对应的语言构件库。虽然语言不同,但是都是依据 OpenTracing 规范进行构建的。 如果业务系统的语言没有对应的官方语言构件库,那么可以根据OpenTracing规范自己开发埋点库。
虽然埋点库提供多种编程语言的实现,但它们使用的追踪数据模型都是一样的,基本上都源于Dapper。Dapper将一次业务请求的追踪描述成树形结构并称之为追踪树(Trace Tree),而组成追踪树的节点是追踪树的基本单元,被称之为跨度(Span)。Trace和Span这两个概念后来被业界广泛接受。
对于一个具体的系统来说,一个追踪对应于一个请求的完整业务处理过程,它代表了从客户端发出业务请求到处理结果返回给客户端的完整流程。而跨度则对应于这次业务请求中一个具体的处理步骤,它可能是一个独立服务的处理过程,也可能是在一个服务中某个线程的处理过程。追踪和跨度都可以通过标识符来标识自己,而每个跨度除了有自己的标识符以外,还需要记录所属追踪的标识符以定义从属关系。在追踪树中除了根节点以外,其余节点都会有一个父节点。跨度需要记录父节点的标识符,以表明跨度与跨度间的调用关系。所以总体来说,一个跨度需要记录的信息包括这个跨度的标识符、父节点标识符以及一些标注等。
由于埋点库与业务系统集成在一起,所以埋点库应该尽可能的减少消耗系统资源,以避免对业务系统性能产生影响。埋点库在性能上的开销主要体现在两个方面,一个是生成追踪信息时产生的性能上的损耗,另一个是在收集追踪信息时产生的性能损耗。
在生成追踪信息时,最主要的损耗体现在为追踪和跨度分配标识、添加标注等。由于追踪标识只要在根节点生成一次,其余节点只要负责复制就好了,所以在生成追踪信息上,根节点的损耗是最大的。但是根据Dapper的实践,无论是根节点还是叶子节点,它们在耗时上都只有100~200ns之间。这样的性能损耗对于业务系统来说还是可以接受的。Dapper在实现上将追踪信息与日志一起写入到文件中,所以Dapper还存在写硬盘的损耗,但是由于写日志操作是异步完成的,所以这种损耗一般不会察觉到。现在大部分的开源框架都将日志与追踪信息分开处理,只有在设置了日志需要添加追踪信息时才会将追踪信息写入到日志中。
埋点库主要的性能损耗并不是生成追踪信息而是对追踪信息的收集,这主要体现在对CPU资源和对网络带宽的占用上。由于追踪信息数据对实时性要求并不是非常高,所以为了降低埋点库对业务系统的影响,大部分情况下收集完追踪信息数据并不会马上进行发送,而是使用独立的线程或组件缓存一定数量的数据再统一发送。在Dapper的实践中,日志收集的职责被分配给一个低优先级的守护进程,以防止在一台高负载的服务器中与业务系统争抢CPU资源。
尽管单独看一次收集对于CPU和网络带宽的影响并不大,但对于业务请求量巨大的互联网应用来说,这些影响在海量请求下会对系统的响应时间和吞吐量产生较为明显的影响。因此,为了降低这种影响,Dapper在收集追踪信息时引入了采样率的概念。所谓采样率就是收集起来的追踪数据与埋点库产生的追踪数据的比例。在此针对skywlaking采样率来进行说明,如果在设置了采样率的情况下,skywalking会在根节点创建前判断是否此次链路数据是否属于该采样的链路,如果是的话产生可忽略的根节点(子系统根据父系统传递的数据判断是否需要产生可忽略的根节点),子节点依次产生对应类型跨度。另外skywalking还存在服务端采样率,服务端采样率有利于保护存储应用,防止数据量写入太大。
除此之外,Dapper还提出了可变采样率的概念,即在业务系统负载较低时采用较高的采样率,在负载较高时采用较低的采样率。这样可以保证业务系统在不受影响的情况下最大化收集链路数据。
采样率不仅可以控制埋点库对于业务系统响应时间和吞吐量的影响,还可以控制追踪数据的总体规模。对于业务量QPS高的系统来说,入股每次请求都要记录,那么每日产生的数据量将是巨大的。在Dapper实现中,即使引用了采样率,但是每日产生的数据量依然达到了1TB以上,所以在很多开源系统中,对于数据的存储和选型上都选择了NoSQL数据库,并且对于存储的实现上都是用单独的模块来完成。
采样率其实只是采样策略中的一种实现方案,采样策略本身可分为基于头部和基于尾部两种。Dapper所使用的采样率其实属于基于头部的采样策略。基于头部的采样策略在调用一开始就决定了追踪数据是否上报,而基于尾部的采样策略则是在调用完成后根据追踪数据决定是否上报。从实现角度来看,基于头部的采样策略有两种方式,一种是在一开始直接不生成追踪数据,这样也就不需要收集;另一种是也会生成追踪数据,但是标注上设置为不需要收集的链路,在收集环节根据标注进行过滤。由于追踪数据不仅对于分析性能瓶颈有意义,对于获取服务间的依赖关系也是重要的依据,所以即使在不需要上报时它也是有价值的。
由于基于头部的采样策略在调用开始就确定了该链路是否会上报,这就导致了追踪数据是否有价值并不影响上报,从而导致基于头部的采样策略会出现收集到大量无用数据,而缺失了有价值数据。所以在一些商用分布式链路追踪系统都是采用的基于尾部的采样策略。基于尾部的采样策略会先将链路数据保存在内存中,当整个链路收集完毕后根据调用链路中是否存在错误或者性能问题决定该链路数据是否保存、是否需要忽略。显而易见,基于尾部的采样策略更具优势。
由于同一个调用链路中的跨度是通过追踪标识符和跨度标识符构建关联关系的,所以追踪标识符和跨度标识符需要在同一个调用链路中进行传播。由于追踪标识符需要保证整个调用链路必须相同,所以追踪标识符需要在整个链路中传播。因为跨度标识符更多的是保障父子间跨度关系,所以需要在父子间进行跨度传播。这种为了构建跨度关联关系而进行的数据传播称为跨度传播。跨度传播在追踪系统中都是最为重要的话题之一,因此它是构建完整调用链路最为关键的一环。
说起跨度传播就要提到两个重要的场景,一个是服务间的跨进程传播,一个是服务内的跨线程传播。服务间的跨进程传播通常在跨进程前将需要传播的数据先提取(Extract)出来放入载体(Carrier)中,在子进程中先将父进程的载体先注入(Inject)到子进程中再继续执行业务逻辑。服务内的跨线程传播通常是在父线程调用子线程前将需要跨线程传递的数据作为子线程的入参携带进去。对于不同的链路追踪系统采用不同的实现方式,例如Zipkin定义了B3传播协议,skywalking定义了SW8传播协议。