关于java:分布式追踪系统原理看不懂40张图带你亲手实践

33次阅读

共计 6926 个字符,预计需要花费 18 分钟才能阅读完成。

前言

在微服务架构中,一次申请往往波及到多个模块,多个中间件,多台机器的相互协作能力实现。这一系列调用申请中,有些是串行的,有些是并行的,那么如何确定这个申请背地调用了哪些利用,哪些模块,哪些节点及调用的先后顺序?如何定位每个模块的性能问题?本文将为你揭晓答案。

本文将会从以下几个方面来论述

  • 分布式追踪零碎原理及作用
  • SkyWalking 的原理及架构设计
  • 我司在分布式调用链上的实际

分布式追踪零碎的原理及作用

如何掂量一个接口的性能好坏,个别咱们至多会关注以下三个指标

  • 接口的 RT 你怎么晓得?
  • 是否有异样响应?
  • 次要慢在哪里?

单体架构

在初期,公司刚起步的时候,可能多会采纳如下单体架构,对于单体架构咱们该用什么形式来计算以上三个指标呢?

最容易想到的显然是用 AOP

应用 AOP 在调用具体的业务逻辑前后别离打印一下工夫即可计算出整体的调用工夫,应用 AOP 来 catch 住异样也可晓得是哪里的调用导致的异样。

微服务架构

在单体架构中因为所有的服务,组件都在一台机器上,所以相对来说这些监控指标比拟容易实现,不过随着业务的疾速倒退,单体架构必然会朝微服务架构倒退,如下

如图示:一个略微简单的微服务架构

如果有用户反馈某个页面很慢,咱们晓得这个页面的申请调用链是 A —–> C —–> B —–> D,此时如何定位可能是哪个模块引起的问题。每个服务 Service A,B,C,D 都有好几台机器。怎么晓得某个申请调用了服务的具体哪台机器呢?

能够显著看到,因为无奈精确定位每个申请通过的确切门路,在微服务这种架构下有以下几个痛点

  1. 排查问题难度大,周期长
  2. 特定场景难复现
  3. 零碎性能瓶颈剖析较难

分布式调用链就是为了解决以上几个问题而生,它次要的作用如下

  • 主动采取数据
  • 剖析数据产生 残缺调用链:有了申请的残缺调用链,问题有很大概率可复现
  • 数据可视化:每个组件的性能可视化,能帮忙咱们很好地定位系统的瓶颈,及时找出问题所在

通过分布式追踪零碎能很好地定位如下申请的每条具体申请链路,从而轻易地实现申请链路追踪,每个模块的性能瓶颈定位与剖析。

分布式调用链规范 – OpenTracing

晓得了分布式调用链的作用,那咱们来看下如何实现分布式调用链的实现及原理,首先为了解决不同的分布式追踪零碎 API 不兼容的问题,诞生了 OpenTracing 标准,OpenTracing 是一个轻量级的标准化层,它位于应用程序 / 类库和追踪或日志分析程序之间。

这样 OpenTracing 通过提供平台无关,厂商无关的 API,使得开发人员可能不便地增加追踪零碎的实现。

说到这大家是否想过 Java 中相似的实现?还记得 JDBC 吧,通过提供一套规范的接口让各个厂商去实现,程序员即可面对接口编程,不必关怀具体的实现。这里的接口其实就是规范,所以制订一套规范十分重要,能够实现组件的可插拔。

接下来咱们来看 OpenTracing 的数据模型,次要有以下三个

  • Trace:一个残缺申请链路
  • Span:一次调用过程(须要有开始工夫和完结工夫)
  • SpanContext:Trace 的全局上下文信息, 如外面有 traceId

了解这三个概念十分重要,为了让大家更好地了解这三个概念,我特意画了一张图

如图示,一次下单的 残缺申请 残缺就是一个 Trace, 显然对于这个申请来说,必须要有一个全局标识来标识这一个申请,每一次调用就称为一个 Span,每一次调用都要带上全局的 TraceId, 这样才可把全局 TraceId 与每个调用关联起来,这个 TraceId 就是通过 SpanContext 传输的,既然要传输显然都要遵循协定来调用。如图示,咱们把传输协定比作车,把 SpanContext 比作货,把 Span 比作路应该会更好了解一些。

了解了这三个概念,接下来我看看分布式追踪零碎如何采集对立图中的微服务调用链

咱们能够看到底层有一个 Collector 始终在石破天惊地收集数据,那么每一次调用 Collector 会收集哪些信息呢。

  1. 全局 trace_id:这是显然的,这样能力把每一个子调用与最后的申请关联起来
  2. span_id: 图中的 0,1,1.1,2, 这样就能标识是哪一个调用
  3. parent_span_id:比方 b 调用 d 的 span_id 是 1.1,那么它的 parent_span_id 即为 a 调用 b 的 span_id 即 1,这样能力把两个 紧邻的调用 关联起来。

有了这些信息,Collector 收集的每次调用的信息如下

依据这些图表信息显然能够据此来画出调用链的可视化视图如下

于是一个残缺的分布式追踪零碎就实现了。

以上实现看起来的确简略,但有以下几个问题须要咱们认真思考一下

  1. 怎么 主动 采集 span 数据:主动采集,对业务代码无侵入
  2. 如何跨过程传递 context
  3. traceId 如何保障全局惟一
  4. 申请量这么多采集会不会影响性能

接下我来看看 SkyWalking 是如何解决以上四个问题的

SkyWalking 的原理及架构设计

怎么主动采集 span 数据

SkyWalking 采纳了 插件化 + javaagent 的模式来实现了 span 数据的主动采集,这样能够做到对代码的 无侵入性,插件化意味着可插拔,扩展性好(后文会介绍如何定义本人的插件)

如何跨过程传递 context

咱们晓得数据个别分为 header 和 body, 就像 http 有 header 和 body, RocketMQ 也有 MessageHeader,Message Body, body 个别放着业务数据,所以不宜在 body 中传递 context,应该在 header 中传递 context,如图示

dubbo 中的 attachment 就相当于 header , 所以咱们把 context 放在 attachment 中,这样就解决了 context 的传递问题。

小提示:这里的传递 context 流程均是在 dubbo plugin 解决的,业务无感知,这个 plugin 是怎么实现的呢,下文会剖析

traceId 如何保障全局惟一

要保障全局惟一,咱们能够采纳分布式或者本地生成的 ID,应用分布式话须要有一个发号器,每次申请都要先申请一下发号器,会有一次网络调用的开销,所以 SkyWalking 最终采纳了本地生成 ID 的形式,它采纳了赫赫有名的 snowflow 算法,性能很高。

图示: snowflake 算法生成的 id

不过 snowflake 算法有一个家喻户晓的问题:工夫回拨,这个问题可能会导致生成的 id 反复。那么 SkyWalking 是如何解决工夫回拨问题的呢。

每生成一个 id,都会记录一下生成 id 的工夫(lastTimestamp),如果发现以后工夫比上一次生成 id 的工夫(lastTimestamp)还小,那阐明产生了工夫回拨,此时会生成一个随机数来作为 traceId。这里可能就有同学要较真了,可能会感觉生成的这个随机数也会和已生成的全局 id 反复,是否再加一层校验会好点。

这里要说一下零碎设计上的计划取舍问题了,首先如果针对产生的这个随机数作唯一性校验无疑会多一层调用,会有肯定的性能损耗,但其实工夫回拨产生的概率很小(产生之后因为机器工夫错乱,业务会受到很大影响,所以机器工夫的调整必然要慎之又慎),再加上生成的随机数重合的概率也很小,综合思考这里的确没有必要再加一层全局唯一性校验。对于技术计划的选型,肯定要防止适度设计,过犹不及。

申请量这么多,全副采集会不会影响性能?

如果对每个申请调用都采集,那毫无疑问数据量会十分大,但反过来想一下,是否真的有必要对每个申请都采集呢,其实没有必要,咱们能够设置采样频率,只采样局部数据,SkyWalking 默认设置了 3 秒采样 3 次,其余申请不采样, 如图示

这样的采样频率其实足够咱们剖析组件的性能了,按 3 秒采样 3 次这样的频率来采样数据会有啥问题呢。现实状况下,每个服务调用都在同一个工夫点(如下图示)这样的话每次都在同一时间点采样的确没问题

但在生产上,每次服务调用根本不可能都在同一时间点调用,因为期间有网络调用延时等,理论调用状况很可能是下图这样

这样的话就会导致某些调用在服务 A 上被采样了,在服务 B,C 上不被采样,也就没法剖析调用链的性能,那么 SkyWalking 是如何解决的呢。

它是这样解决的:如果上游有携带 Context 过去(阐明上游采样了),则上游 强制 采集数据。这样能够保障链路残缺。

SkyWalking 的基础架构

SkyWalking 的根底如下架构,能够说简直所有的的分布式调用都是由以下几个组件组成的

首先当然是节点数据的定时采样,采样后将数据定时上报,将其存储到 ES, MySQL 等长久化层,有了数据自然而然可依据数据做可视化剖析。

SkyWalking 的性能如何

接下来大家必定比较关心 SkyWalking 的性能,那咱们来看下官网的测评数据

图中蓝色代表未应用 SkyWalking 的体现,橙色代表应用了 SkyWalking 的体现,以上是在 TPS 为 5000 的状况下测出的数据,能够看出,不论是 CPU,内存,还是响应工夫,应用 SkyWalking 带来的性能损耗简直能够忽略不计。

接下来咱们再来看 SkyWalking 与另一款业界比拟出名的分布式追踪工具 Zipkin, Pinpoint 的比照(在采样率为 1 秒 1 个,线程数 500,申请总数为 5000 的状况下做的比照), 能够看到在要害的响应工夫上,Zipkin(117ms),PinPoint(201ms)远逊色 于 SkyWalking(22ms)!

从性能损耗这个指标上看,SkyWalking 完胜!

再看下另一个指标:对代码的侵入性如何,ZipKin 是须要在应用程序中埋点的,对代码的侵入强,而 SkyWalking 采纳 javaagent + 插件化这种批改字节码的形式能够做到 对代码无任何侵入,除了性能和对代码的侵入性上 SkyWaking 体现不错外,它还有以下劣势几个劣势

  • 对多语言的反对,组件丰盛:目前其反对 Java, .Net Core, PHP, NodeJS, Golang, LUA 语言,组件上也反对 dubbo, mysql 等常见组件,大部分能满足咱们的需要。
  • 扩展性:对于不满足的插件,咱们依照 SkyWalking 的规定手动写一个即可,新实现的插件对代码无入侵。

我司在分布式调用链上的实际

SkyWalking 在我司的利用架构

由上文可知 SkyWalking 有很多长处,那么是不是咱们用了它的全副组件了呢,其实不然,来看下其在我司的利用架构

从图中能够看出咱们只采纳了 SkyWalking 的 agent 来进行采样,放弃了另外的「数据上报及剖析」,「数据存储」,「数据可视化」三大组件,那为啥不间接采纳 SkyWalking 的整套解决方案呢,因为在接入 SkyWalking 之前咱们的 Marvin 监控生态体系曾经绝对比较完善了,如果把其整个替换成 SkyWalking,一来没有必要,Marvin 在大多数场景下都能满足咱们的需要,二来零碎替换老本高,三来如果从新接入用户学习老本很高。

这也给咱们一个启发:任何产品抢占先机很重要,后续产品的替换老本会很高,抢占先机,也就是抢占了用户的心智,这就像微信尽管 UI,性能上制作精良,但在国外照样干不过 Whatsapp 一样,因为先机曾经没了。

从另一方面来看,对架构来说,没有最好的,只有最合适的,联合以后业务场景去均衡折中才是架构设计的实质

我司对 SkyWalking 作了哪些革新和实际

我司次要作了以下革新和实际

  1. 预发环境因为调试须要强制采样
  2. 实现更细粒度的采样?
  3. 日志中嵌入 traceId
  4. 自研实现了 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 秒内进行 3 次 redis, dubbo, mysql 等的采样,也就防止了此问题

日志中如何嵌入 traceId?

输入日志中嵌入 traceId 便于咱们排查问题,所以打出出 traceId 十分有必要,该怎么在日志中嵌入 traceId 呢?咱们用的是 log4j,这里就要理解一下 log4j 的插件机制了,log4j 容许咱们自定义插件来输入日志的格局,首先咱们须要定义日志的格局,在自定义的日志格局中嵌入 %traceId, 作为占位符,如下

而后咱们再实现一个 log4j 的插件,如下

首先 log4j 的插件要定义一个类,这个类要继承 LogEventPatternConverter 这个类,并且用规范 Plugin 将其本身申明为 Plugin,通过 @ConverterKeys 这个注解指定了要替换的占位符,而后在 format 办法里将其替换掉。这样在日志中就会呈现咱们想要的 TraceId , 如下

我司自研了哪些 skywalking 插件

SkyWalking 实现了很多插件,不过未提供 memcached 和 druid 的插件,所以咱们依据其标准自研了这两者的插件

插件如何实现呢,能够看到它次要由三个局部组成

  1. 插件定义类: 指定插件的定义类,最终会依据这里的定义类打包生成 plugin
  2. Instrumentation: 指定切面,切点,要对哪个类的哪个办法进行加强
  3. Interceptor, 指定步骤 2 重要在办法的前置,后置还是异样中写加强逻辑

可能大家看了还是不懂,那咱们以 dubbo plugin 来简略解说一下,咱们晓得在 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 办法执行前对期 attachment 作注入全局 traceId 等操作,这一切都是静默的,对 代码无侵入 的。

总结

本文由浅入深地介绍了分布式追踪零碎的原理,置信大家对其作用及工作机制有了比拟深的了解,特地须要留神的是,引入某项技巧,肯定要联合现有的技术架构作出最正当的抉择,就像 SkyWalking 有四个模块,我司只采纳其 agent 采样性能一样,没有最好的技术,只有最合适的技术,通过此文,置信大家应该对 SkyWalking 的实现机制有了比拟清晰的意识,文中只是介绍了一下 SkyWalking 的插件实现形式,不过其毕竟是工业级软件,要理解其博大精深,还要多读源码哦。

更多面试学习材料曾经整顿到我的 git 仓库中,有须要的敌人自取:https://gitee.com/biwangsheng/personal.git

作 者:码海

原文链接:https://mp.weixin.qq.com/s/U-…

正文完
 0