背景
传统的大型单体系统随着业务体量的增大已经很难满足市场对技术的需求,通过对将整块业务系统拆分为多个互联依赖的子系统并针对子系统进行独立优化,能够有效提升整个系统的吞吐量。在进行系统拆分之后,完整的业务事务逻辑所对应的功能会部署在多个子系统上,此时用户的一次点击请求会触发若干子系统之间的相互功能调用,如何分析一次用户请求所触发的多次跨系统的调用过程、如何定位存在响应问题的调用链路等等问题是链路追踪技术所要解决的问题。
举一个网络搜索的示例,来说明这样一个链路监控系统需要解决的一些挑战。当用户在搜索引擎中输入一个关键词后,一个前端服务可能会将这次查询分发给数百个查询服务,每个查询服务在其自己的索引中进行搜索。该查询还可以被发送到许多其他子系统,这些子系统可以处理敏感词汇、检查拼写、用户画像分析或寻找特定领域的结果,包括图像、视频、新闻等。所有这些服务的结果有选择地组合在一起,最终展示在搜索结果页面中,我们将这个模型称为一次完整的搜索过程。
在这样一次搜索过程中,总共可能需要数千台机器和许多不同的服务来处理一个通用搜索查询。此外,在网络搜索场景中,用户的体验和延迟紧密相关,一次搜索延时可能是由于任何子系统的性能不佳造成的。开发人员仅考虑延迟可能知道整个系统存在问题,但却无法猜测哪个服务有问题,也无法猜测其行为不良的原因。首先,开发人员可能无法准确知道正在使用哪些服务,随时都可能加入新服务和修改部分服务,以增加用户可见的功能,并改进性能和安全性等其他方面;其次,开发人员不可能是庞大系统中每个内部微服务的专家,每一个微服务可能有不同团队构建和维护;另外,服务和机器可以由许多不同的客户端同时共享,因此性能问题可能是由于另一个应用的行为引起。
Dapper简介
在分布式链路追踪方面,Google早在2010年针对其内部的分布式链路跟踪系统Dapper,发表了相关论文对分布式链路跟踪技术进行了介绍(强烈推荐阅读)。其中提出了两个基本要求。第一,拥有广泛的覆盖面。针对庞大的分布式系统,其中每个服务都需要被监控系统覆盖,即使是整个系统的一小部分没有被监控到,该链路追踪系统也可能是不可靠的。第二,提供持续的监控服务。对于链路监控系统,需要7*24小时持续保障业务系统的健康运行,保证任何时刻都可以及时发现系统出现的问题,并且通常情况下很多问题是难以复现的。根据这两个基本要求,分布式链路监控系统的有如下几个设计目标:
- 应用级透明
链路监控组件应该以基础通用组件的方式提供给用户,以提高稳定性,应用开发者不需要关心它们。对于Java语言来说,方法可以说是调用的最小单位,想要实现对调用链的监控埋点势必对方法进行增强。Java中对方法增强的方式有很多,比如直接硬编码、动态代理、字节码增强等等。应用级透明其实是一个比较相对的概念,透明度越高意味着难度越大,对于不同的场景可以采用不同的方式。
- 低开销
低开销是链路监控系统最重要的关注点,分布式系统对于资源和性能的要求本身就很苛刻,因此监控组件必须对原服务的影响足够小,将对业务主链路的影响降到最低。链路监控组件对于资源的消耗主除了体现在增强方法的消耗上,其次还有网络传输和数据存储的消耗,因为对于链路监控系统来说,想要监控一次请求势必会产生出请求本身外的额外数据,并且在请求过程中,这些额外的数据不仅会暂时保存在内存中,在分布式场景中还会伴随着该请求从上游服务传输至下游服务,这就要求产生的额外数据尽可能地少,并且在伴随请求进行网络传输的时候只保留少量必要的数据。
- 扩展性和开放性
无论是何种软件系统,可扩展性和开放性都是衡量其质量优劣的重要标准。对于链路监控系统这样的基础服务系统来说,上游业务系统对于链路监控系统来说是透明的,在一个规模较大的企业中,一个基础服务系统往往会承载成千上万个上游业务系统。每个业务系统由不同的团队和开发人员负责,虽然使用的框架和中间件在同一个企业中有大致的规范和要求,但是在各方面还是存在差异的。因此作为一个基础设施,链路监控系统需要具有非常好的可扩展性,除了对企业中常用中间件和框架的支撑外,还要能够方便开发人员针对特殊的业务场景进行定制化的开发。
数据模型
OpenTracing规范
Dapper将请求按照三个维度划分为Trace、Segment、Span三种模型,该模型已经形成了OpenTracing规范。OpenTracing是为了描述分布式系统中事务的语义,而与特定下游跟踪或监控系统的具体实现细节无关,因此描述这些事务不应受到任何特定后端数据展示或者处理的影响。大的概念就不多介绍了,重点看一下Trace、Segment、Span这三种模型到底是什么。
- Trace
表示一整条调用链,包括跨进程、跨线程的所有Segment的集合。
- Segment
表示一个进程(JVM)或线程内的所有操作的集合,即包含若干个Span。
- Span
表示一个具体的操作。Span在不同的实现里可能有不同的划分方式,这里介绍一个比较容易理解的定义方式:
- Entry Span:入栈Span。Segment的入口,一个Segment有且仅有一个Entry Span,比如HTTP或者RPC的入口,或者MQ消费端的入口等。
- Local Span:通常用于记录一个本地方法的调用。
- Exit Span:出栈Span。Segment的出口,一个Segment可以有若干个Exit Span,比如HTTP或者RPC的出口,MQ生产端,或者DB、Cache的调用等。
按照上面的模型定义,一次用户请求的调用链路图如下所示:
唯一id
每个请求有唯一的id还是很必要的,那么在海量的请求下如何保证id的唯一性并且能够包含请求的信息?Eagleeye的traceId设计如下:
根据这个id,我们可以知道这个请求在2022-10-18 10:10:40发出,被11.15.148.83机器上进程号为14031的Nginx(对应标识位e)接收到。其中的四位原子递增数从0-9999,目的是为了防止单机并发造成traceId碰撞。
关系描述
将请求划分为Trace、Segment、Span三个层次的模型后,如何描述他们之间的关系?
从【OpenTracing规范】一节的调用链路图中可以看出,Trace、Segment可以作为整个调用链路中的逻辑结构,而Span才是真正串联起整个链路的单元,系统可以通过若干个Span串联起整个调用链路。
在Java中,方法是以入栈、出栈的形式进行调用,那么系统在记录Span的时候就可以通过模拟出栈、入栈的动作来记录Span的调用顺序,不难发现最终一个链路中的所有Span呈现树形关系,那么如何描述这棵Span树?Eagleeye中的设计很巧妙,EagleEye设计了RpcId来区别同一个调用链下多个网络调用的顺序和嵌套层次。 如下图所示:
RpcId用0.X1.X2.X3.....Xi来表示,根节点的RpcId固定从0开始,id的位数("."的数量)表示了Span在这棵树中的层级,Id最后一位表示了Span在这一层级中的顺序。那么给定同一个Trace中的所有RpcId,便可以很容易还原出一个完成的调用链:
- 0
- 0.1
- 0.1.1
- 0.1.2
- 0.1.2.1
- 0.2
- 0.2.1
- 0.3
- 0.3.1
- 0.3.1.1
- 0.3.2
跨进程传输
再进一步,在整个调用链的收集过程中,不可能将整个Trace信息随着请求携带到下个应用中,为了将跨进程传输的trace信息减少到最小,每个应用(Segment)中的数据一定是分段收集的,这样在Eagleeye的实现下跨Segment的过程只需要携带traceId和rpcid两个简短的信息即可。在服务端收集数据时,数据自然也是分段到达服务端的,但由于种种原因分段数据可能存在乱序和丢失的情况:
如上图所示,收集到一个Trace的数据后,通过rpcid即可还原出一棵调用树,当出现某个Segment数据缺失时,可以用第一个子节点替代。
数据埋点
如何进行方法增强(埋点)是分布式链路追系统的关键因素,在Dapper提出的要求中可以看出,方法增强同时要满足应用级透明和低开销这两个要求。之前我们提到应用级透明其实是一个比较相对的概念,透明度越高意味着难度越大,对于不同的场景可以采用不同的方式。本文我们介绍阿里的Eagleye和开源的SkyWalking来比较两种埋点方式的优劣。
编码
阿里Eagleeye的埋点方式是直接编码的方式,通过中间件预留的扩展点实现。但是按照我们通常的理解来说,编码对于Dapper提出的扩展性和开放性似乎并不友好,那为什Eagleye么要采用这样的方式?个人认为有以下几点:
- 阿里有中间件的使用规范,不是想用什么就用什么,因此对于埋点的覆盖范围是有限的;
- 阿里有给力的中间件团队专门负责中间件的维护,中间件的埋点对于上层应用来说也是应用级透明的,对于埋点的覆盖是全面的;
- 阿里应用有接入Eagleye监控系统的要求,因此对于可插拔的诉求并没有非常强烈。
从上面几点来说,编码方式的埋点完全可以满足Eagleye的需要,并且直接编码的方式在维护、性能消耗方面也是非常有优势的。
字节码增强
相比于Eagleye,SkyWalking这样开源的分布式链路监控系统,在开源环境下就没有这么好做了。开源环境下面临的问题其实和阿里集团内部的环境正好相反:
- 开源环境下每个开发者使用的中间件可能都不一样,想用什么就用什么,因此对于埋点的覆盖范围几乎是无限的;
- 开源环境下,各种中间件都由不同组织或个人进行维护,甚至开发者还可以进行二次开发,不可能说服他们在代码中加入链路监控的埋点;
- 开源环境下,并不一定要接入链路监控体系,大多数个人开发者由于资源有限或其他原因没有接入链路监控系统的需求。
从上面几点来说,编码方式的埋点肯定是无法满足SkyWalking的需求的。针对这样的情况,Skywalking采用如下的开发模式:
Skywalking提供了核心的字节码增强能力和相关的扩展接口,对于系统中使用到的中间件可以使用官方或社区提供的插件打包后植入应用进行埋点,如果没有的话甚至可以自己开发插件实现埋点。Skywalking采用字节码增强的方式进行埋点,下面简单介绍字节码增强的相关知识和Skywalking的相关实现。
对Java应用实现字节码增强的方式有Attach和Javaagent两种,本文做一个简单的介绍。
- Attach
Attach是一种相对动态的方式,在阿尔萨斯(Arthas)这样的诊断系统中广泛使用,利用J