当前位置: 首页 > 科技观察

一口气说出“分布式跟踪系统”的原理!

时间:2023-03-20 23:39:25 科技观察

在微服务架构中,一个请求往往会涉及到多个模块,多个中间件,多台机器相互协作完成。图片来自Pexels。在这一系列的调用请求中,有的是串行的,有的是并行的。那么如何确定这个请求背后调用了哪些应用、哪些模块、哪些节点以及调用顺序呢?如何定位各个模块的性能问题?本文将为您揭晓答案。本文将从以下几个方面进行阐述:分布式追踪系统的原理和作用SkyWalking的原理和架构设计我司在分布式调用链上的实践分布式追踪系统的原理和作用如何衡量一个界面,一般我们至少会关注以下三个指标:怎么知道界面的RT?有没有异常反应?单体架构应该用什么方法来计算以上三个指标呢?最容易想到的就是使用AOP:使用AOP打印调用具体业务逻辑前后的时间来计算整体的调用时间,使用AOP来捕捉异常,也可以知道是哪里的调用引起了例外。在微服务架构中,由于在单体架构中所有的服务和组件都在一台机器上,所以这些监控指标相对容易实现。但是随着业务的快速发展,单体架构必然会向微服务架构发展,如下图所示:如果在稍微复杂的微服务架构中有用户反映某个页面速度慢,我们知道请求调用这个页面的链条是A→C→B→D,这个时候怎么定位到哪个模块就可能出问题了。每个服务ServiceA、B、C、D都有几台机器。你怎么知道某个请求调用了服务的哪台机器?可以明显看出,由于无法准确定位每个请求的准确路径,微服务架构下存在以下痛点:问题排查难度大,周期长。某些场景很难重现。系统性能瓶颈分析困难。分布式调用链就是为解决上述问题而诞生的。其主要功能如下:自动采集数据。分析数据生成完整的调用链:有了请求的完整调用链,问题重现的概率就很高。数据可视化:各个组件的性能可视化,可以帮助我们很好的定位系统的瓶颈,及时发现问题。通过分布式跟踪系统,可以很好地定位后续请求的各个具体请求链接,从而轻松实现请求链接跟踪,轻松实现各模块的性能瓶颈定位和分析。分布式调用链标准:OpenTracing知道分布式调用链的作用,下面就来看看如何实现分布式调用链的实现和原理。首先,为了解决不同分布式追踪系统API不兼容的问题,诞生了OpenTracing规范。OpenTracing是一个轻量级规范化层,位于应用程序/库和跟踪或日志分析程序之间。通过这种方式,OpenTracing使开发人员能够通过提供独立于平台和供应商的API轻松添加跟踪系统的实现。说到这里,大家有没有想过用Java实现类似的实现呢?记住JDBC,通过提供一套标准的接口供各个厂商实现,程序员可以面对接口编程而不用关心具体的实现。这里的接口其实就是一个标准,所以制定一套标准非常重要,可以实现组件的可插拔性。接下来我们看一下OpenTracing的数据模型。主要分为三种:Trace:一个完整??的请求链接。Span:一个调用过程(需要有开始时间和结束时间)。SpanContext:Trace的全局上下文信息,比如里面的TraceId。理解这三个概念非常重要。为了让大家更好的理解这三个概念,我特地画了一张图:如图所示,一个完整的下单请求就是一个完整的trace。显然,对于这个请求,必须有一个全局标识符来标识这个请求。每一次调用都称为一个Span,每一次调用都必须携带一个全局的TraceId。只有这样,全局的TraceId才能与每次调用关联起来。这个TraceId是通过SpanContext传递过来的。既然要传输,就必须按照协议来调用。如图所示,如果把transportprotocol比作汽车,SpanContext比作货物,Span比作公路,应该更容易理解。了解了这三个概念之后,我再来看看分布式追踪系统是如何将微服务调用链收集在统一图中的。我们可以看到底层有一个Collector一直在默默的收集数据,那么每次调用Collector会收集什么信息呢?globaltrace_id:这个很明显,这样每次子调用都可以关联到原始请求。span_id:图中的0,1,1.1,2,这样可以识别是哪个call。parent_span_id:比如b调用d的span_id为1.1,那么它的parent_span_id就是a调用b的span_id,为1,这样相邻的两个调用就可以关联起来。有了这些信息,Collector收集到的每次调用的信息如下:根据这些图表信息,显然可以画出调用链的可视化视图如下:这样就实现了一个完整的分布式跟踪系统。上面的实现看似很简单,但是我们需要仔细思考以下问题:如何自动采集Span数据:在不侵入业务代码的情况下自动采集。如何跨进程传递上下文。TraceId如何保证全局唯一。收集这么多请求会不会影响性能。下面我就来看看SkyWalking是如何解决以上四个问题的。SkyWalking原理及架构设计如何自动采集Span数据SkyWalking采用插件+javaagent的形式实现Span数据的自动采集。这样可以做到对代码无侵入,插件意味着可插拔,扩展性好(后面会介绍如何定义自己的插件)。如何跨进程传递Context我们知道数据一般分为Header和Body,就像HTTP有Header和Body,RocketMQ也有MessageHeader和MessageBody。Body一般包含业务数据,所以不宜在Body中传递Context,而是在Header中传递,如图:Dubbo中的Attachment相当于Header,所以我们将Context放在附件中,从而解决了ContextPass的问题。提示:这里传递上下文的过程都是由DubboPlugin处理的,业务是不知道的。下面将分析这个Plugin是如何实现的。如何保证TraceId的全局唯一性为了保证全局唯一性,我们可以使用分布式或者本地生成的ID。如果我们使用分布式,我们需要一个发行者。每次请求都要先请求发行者,会有网络调用开销。因此,SkyWalking最终采用了本地生成ID的方式。它使用著名的高性能Snowflow算法。Snowflake算法生成的id,但是Snowflake算法有一个众所周知的问题:时间回调,可能会导致生成的id重复。那么SkyWalking是如何解决时间回调问题的呢?每次生成一个id,都会记录这个id生成的时间(lastTimestamp)。如果发现当前时间小于上次生成id的时间(lastTimestamp),说明发生了时间回调,此时会生成一个随机数作为TraceId。这里可能有些同学想认真点。他们可能认为生成的随机数也将与生成的全局id相同。最好再加一层验证。这里想谈谈系统设计中方案的选择。首先,如果对生成的随机数进行唯一性校验,无疑会多出一层调用,造成一定的性能损失。但实际上时间回调的概率很小(由于发生后机器时间乱序,业务会受到很大影响,所以机器时间的调整一定要慎重),而且生成的随机数重合的概率也是非常小,综合考虑实在没必要??在这里加一层全局唯一性校验。对于技术方案的选择,一定要避免过度设计,太多就是太多。全部收集会影响性能吗?这么多请求,如果每一个请求都收集起来,数据量无疑会非常大,但是反过来想想,真的有必要把每一个请求都收集起来吗?其实大可不必。我们可以设置采样频率,只采样部分数据。SkyWalking默认3秒采样3次,其余请求不采样。如图:这个采样频率其实已经足够我们分析元器件的性能了。按下3秒以3个样本的频率对数据进行采样有什么问题?理想情况下,每次服务调用都是在同一个时间点(如下图),所以每次都在同一个时间点采样真的没问题。但是在生产中,基本上不可能每个服务调用都在同一个时间点被调用,因为期间存在网络调用延迟,实际调用情况很可能如下图所示:在这个case,有些调用会在服务A上被采样,而不会在服务B和C上被采样,所以无法分析调用链的性能,那么SkyWalking是如何解决的。是这样解决的:如果上游带了Context(说明上游已经采样),下游会强制采集数据。这使链接保持完整。SkyWalking的基本结构SkyWalking的基本结构如下。可以说几乎所有的分布式调用都是由以下几个部分组成的:首先当然是节点数据的定时采样。数据采样后定期上报存储在ES、MySQL等持久层,有了数据,自然可以根据数据做可视化分析。SkyWalking性能如何接下来大家肯定更关心SkyWalking的性能,那么我们来看看官方的评测数据:图中蓝色代表未使用SkyWalking的性能,橙色代表使用SkyWalking的性能使用SkyWalking。以上是在TPS为5000的情况下测得的数据。可以看出,无论是CPU、内存还是响应时间,使用SkyWalking带来的性能损失几乎可以忽略不计。接下来看一下SkyWalking与业界另一知名分布式追踪工具Zipkin、Pinpoint的对比(采样率为1秒,线程数为500,总请求数时进行对比)是5000)。可见Zipkin(117ms)和PinPoint(201ms)在按键响应时间上远不如SkyWalking(22ms)!从性能损失来看,SkyWalking胜出!再看另一个指标:对代码的侵入性如何,ZipKin需要埋在应用程序中,对代码的侵入性很强,而SkyWalking通过javaagent+插件修改字节码实现不侵入代码。除了良好的性能和代码侵入性,SkyWaking还有以下优势:支持多语言,组件丰富:目前支持Java、.NetCore、PHP、NodeJS、Golang、LUA语言,组件还支持Dubbo、MySQL和其他常用组件,大部分都能满足我们的需求。可扩展性:对于不满意的插件,我们可以按照SkyWalking的规则手动编写一个,新实现的插件对代码没有侵入性。我们公司在分布式调用链上SkyWalking的实践在我们公司的应用架构中。从上面我们可以看出SkyWalking有很多优点,那么它的组件我们都用上了吗?事实上,并非如此。我们来看看它在我们公司的应用。应用架构:从图中可以看出,我们只使用了SkyWalking的Agent进行采样,放弃了“数据上报与分析”、“数据存储”、“数据可视化”等三大组件。那为什么不直接采用SkyWalking的完整解决方案呢?因为在接入SkyWalking之前,我们的Marvin监控生态已经比较完善。如果完全换成SkyWalking,一来没必要,Marvin大部分场景都能满足我们的需求,二来系统更换成本高,三来如果重新对接用户,学习成本非常高高的。这也给了我们一个启示:任何产品抓住机会都是非常重要的,后续产品的更换成本会非常高。抢占先机,就是抢占用户心智。国外还是做不了Whatsapp一样的,因为机会没了。另一方面,对于架构,没有最好的,只有最合适的。架构设计的本质是结合当前业务场景进行折衷的平衡。我司SkyWalking改造实践我司主要做了以下改造和实践:预发布环境需要强制采样调试,实现更细粒度的采样。traceId嵌入日志,自研实现SkyWalking插件。①预发布环境需要强制采样调试。从上面的分析可以看出,Collector是在后台定时采样的。这不好吗?为什么我们需要实施强制采样?或者排查定位问题,有时线上出现问题,我们希望在预发布上复现,希望看到这个请求完整的调用链,所以需要在预发布上进行强制采样。所以我们修改了Skywalking的Dubbo插件来实现强制采样。我们在请求cookie上带一个像force_flag=true这样的键值对来表示我们要强制采样。网关收到这个cookie后,会在Dubbo的Attachment中带上键值对force_flag=true。那么Skywalking的Dubbo插件就可以根据这个来判断是否是强制采样。如果有这个值,就是强制采样。如果没有这个值,则进行正常定时采样。②实现更细粒度的采样?称为更细粒度的采样。我们先来看一下Skywalking默认的采样方式,即统一采样。我们知道这个方法默认3秒前3个样本,其他请求全部丢弃,所以有问题。假设3秒内本机有多个Dubbo、MySQL、Redis调用,但如果前三个调用都是Dubbo调用,其他调用如MySQL、Redis等不会被采样。所以我们修改Skywalking实现分组采样,如下:也就是说我们在3秒内对Redis、Dubbo、MySQL等采样了3次,就避免了这个问题。③如何在日志中嵌入TraceId?在输出日志中嵌入TraceId,方便我们排查问题,所以打印出TraceId是非常有必要的。如何在日志中嵌入TraceId?我们使用的是log4j,这里需要了解一下log4j的插件机制,log4j允许我们自定义插件来输出日志的格式。首先我们需要定义日志的格式,在自定义日志格式中嵌入%traceId作为占位符,如下:然后我们实现一个log4j插件,如下:首先,log4j插件需要定义一个类,继承LogEventPatternConverter类,使用标准的Plugin声明自己为Plugin。需要替换的占位符通过@ConverterKeys注解指定,然后在format方法中替换。这样日志中就会出现我们想要的TraceId,如下:④我们公司开发了哪些Skywalking插件?SkyWalking实现了很多插件,但是没有Memcached和Druid的插件,所以我们按照他们的规范开发了这两个插件:插件怎么实现,可以看到主要是由分三部分:插件定义类:指定插件的定义类,最终插件会根据这里的定义类进行打包生成。Instrumentation:指定方面、切入点,以及要增强哪个类的哪个方法。Interceptor:指定是否在步骤2的方法、post或exception的前面写增强逻辑。可能大家看了还是不太明白,下面就用DubboPlugin来简单说明一下。我们知道,在Dubbo服务中,每个请求都会收到来自Netty的消息,提交给业务线程池处理,真正调用到业务方法时结束。中间经过了十几个Filter的处理:MonitorFilter可以拦截所有客户端的请求或者服务端的处理请求,所以我们可以对MonitorFilter进行增强。在它调用Invoke方法之前,将全局的TraceId注入到它的Invocation的Attachment中,保证在请求到达真正的业务逻辑之前,全局的TraceId已经存在。那么显然我们需要在插件中指定我们要增强的类(MonitorFilter),并增强它的方法(Invoke)。我们应该对此方法进行哪些改进?这就是拦截器(Inteceptor)要做的事情。看一下Dubbo插件中的Instrumentation(DubboInstrumentation):看一下代码中描述的拦截器(Inteceptor)是干什么的。关键步骤如下:首先,beforeMethod表示在执行MonitorFilter的invoke方法之前会调用这里的方法。与之对应的是afterMethod,代表执行invoke方法后的增强逻辑。其次,我们从第2点和第3点可以看出,无论是Consumer还是Provider,其全局ID都做了相应的处理。这样可以保证到达真正的业务层时全局的Traceid是可用的。定义好Instrumentation和Interceptor之后,最后一步就是在skywalking.def中指定定义的类://skywalking-plugin.def文件dubbo=org.apache。像skywalking.apm.plugin.asf.dubbo.DubboInstrumentation这样封装的插件会增强MonitorFilter的Invoke方法。在Invoke方法执行之前,它会将全局的TraceId等操作注入到Attachment中,这些都是静默的。非侵入性。小结本文由浅入深地介绍了分布式跟踪系统的原理。相信大家对它的作用和工作机制有了更深入的了解。特别要注意的是某项技术的引入,一定要结合现有的技术架构做出最合理的选择,就像SkyWalking有四个模块,而我们公司只用它的Agent采样功能。没有最好的技术,只有最合适的技术。通过这篇文章,相信大家应该对SkyWalking的实现机制有了更清晰的认识。本文只介绍SkyWalking的插件实现,但毕竟是工业级软件。要了解它的深奥,还需要多阅读源码。作者:码海编辑:陶家龙来源:转载自公众号码海(ID:seofcode)