”
在微服务架构中,一个请求往往涉及多个模块、多个中间件、多台机器的相互配合才能完成。
图片来自
在这一系列的调用请求中,有的是串行的,有的是并行的。那么如何确定这个请求背后调用了哪些应用、哪些模块、哪些节点以及调用的顺序呢?如何定位各个模块的性能问题?本文将为您揭晓答案。
本文将从以下几个方面进行阐述:
分布式追踪系统原理及作用
如何衡量一个界面的性能,一般我们至少会关注以下三个指标:
整体架构
在早期,当一个公司刚刚起步时,它可能会采用以下单体架构。对于单体架构来说,上述三个指标应该如何计算呢?
最容易想到的显然是使用 AOP:
使用AOP打印调用具体业务逻辑前后的时间来计算整体的调用时间。使用AOP来捕获异常并知道调用是在哪里引起异常的。
微服务架构
在单体架构中,由于所有服务和组件都在一台机器上,因此这些监控指标相对容易实现。
但随着业务的快速发展,单体架构必然会向微服务架构发展,如下图:
稍微复杂一点的微服务架构
如果用户反映某个页面很慢,我们就知道这个页面的请求调用链是A→C→B→D。这时候如何定位可能是哪个模块出现了问题。
每个服务A、B、C、D都有几台机器。如何知道某个请求在哪台机器上调用该服务?
可以清楚地看到,由于无法准确定位每个请求的确切路径,微服务架构存在以下痛点:
分布式调用链就是为了解决上述问题而诞生的。其主要功能如下:
通过分布式追踪系统,可以很好的定位后续请求的每个具体请求链路,从而轻松实现请求链路追踪,轻松实现各模块的性能瓶颈定位和分析。
分布式调用链标准:
既然知道了分布式调用链的作用,那么我们就来看看如何实现分布式调用链及其原理。
首先,为了解决不同分布式追踪系统API不兼容的问题,该规范诞生了。
是位于应用程序/类库和跟踪或日志分析程序之间的轻量级标准化层。
这样,开发人员可以通过提供独立于平台和供应商的API来轻松添加跟踪系统的实现。
说到这里,你有没有想过在Java中也有类似的实现呢?还记得 JDBC 吗,通过提供一套标准接口供各个厂商实现,程序员可以面对接口编程,而不必担心具体的实现。
这里的接口其实就是一个标准,所以制定一套标准让组件可插拔是非常重要的。
接下来我们看一下数据模型,主要包括以下三个:
理解这三个概念非常重要。为了让大家更好的理解这三个概念,我特意画了一张图:
如图所示,一个完整的下单请求就是一个。显然,对于这个请求,必须有一个全局标识符来标识这个请求。每次调用称为一个Span,每次调用都必须带上全局标识符。的。
只有这样,才能将全局的情况与每次通话关联起来。这是通过传输完成的。既然要传输,显然就必须按照协议来调用。
如图所示,如果我们将传输协议比作汽车,将其比作货物,将Span比作道路,应该会更容易理解。
理解了这三个概念之后,我们再来看看分布式追踪系统是如何收集统一图中的微服务调用链的。
我们可以看到,最底层有一个人一直在默默地收集数据。那么每次调用都会收集哪些信息呢?
有了这些信息,每次调用收集的信息如下:
根据这些图表信息,显然可以绘制出如下的调用链可视化视图:
这样就实现了一个完整的分布式追踪系统。
上面的实现看起来确实很简单,但是有几个问题需要我们仔细思考:
接下来我就来看看如何解决以上四个问题。
原理及架构设计
如何自动收集Span数据
采用插件+形式,实现Span数据的自动采集。
这可以使代码不具有侵入性。插件意味着可插拔性和良好的扩展性(后面会介绍如何定义自己的插件)。
如何跨进程传递
我们知道数据一般分为和Body,就像有和Body一样,也有Body。
Body一般保存的是业务数据,所以不应该在Body中传入,而应该传入,如图:
相当于in,所以我们把它放进去,这样就解决了的传输问题。
提示:这里的发货流程都是在处理中,商家是不知情的。下面将分析这是如何实现的。
如何确保全球唯一性
为了确保全局唯一性,我们可以使用分布式或本地生成的ID。使用分布式需要一个编号器。每个请求必须首先请求编号器,这会产生网络调用的开销。
所以我们最终采用了本地生成ID的方法,该方法使用了众所周知的算法,并且性能较高。
算法生成的 id
然而该算法有一个众所周知的问题:时间回拨,这可能会导致生成重复的ID。那么如何解决时间回拨问题呢。
每次生成id时,都会记录id生成的时间()。如果发现当前时间比上次生成id()的时间小,则说明发生了时间回拨,结果会生成一个随机数。
这里可能有一些同学想要认真的。他们可能认为生成的随机数也会重复生成的全局 ID。最好再添加一层验证。
这里我想谈谈系统设计中方案的选择。首先,如果对生成的随机数进行唯一性校验,无疑会多一层调用,从而造成一定的性能损失。
但实际上,时间回拨发生的概率很小(发生后,由于机器时间错乱,业务会受到很大影响,所以机器时间的调整一定要谨慎),而且发生时间回拨的概率很小。生成的随机数重叠也很小。 ,这里综合考虑确实没必要加一层全局唯一性验证。
在选择技术方案时,一定要避免过度设计,过度设计是不够的。
所有集合都会影响性能吗?
有这么多的要求。如果把每一个请求调用都收集起来,数据量无疑会非常大。但换个角度想想,真的有必要收集每一个请求吗?
事实上,没有必要。我们可以设置采样频率,只采样部分数据。默认设置为每3秒采样3次,其他请求不采样,如图:
这个采样频率其实已经足够我们分析元件的性能了。以3秒3次的频率采样数据有什么问题?
理想情况下,每次服务调用都发生在同一时间点(如下图所示)。这样的话,每次在同一时间点采样确实是没有问题的。
但在生产中,每个服务调用基本上不可能在同一时间点被调用,因为期间存在网络调用延迟。实际的调用情况很可能如下图:
这会导致一些调用在服务A上被采样,但在服务B和C上没有被采样,并且无法分析调用链的性能。那么如何解决呢。
是这样解决的:如果上游带过来了(说明上游已经采样了),则强制下游采集数据。这确保了链接的完整性。
基础设施
该架构的基础如下。可以说,几乎所有的分布式调用都是由以下几个组件组成:
第一个当然是节点数据的定期采样。采样后,数据定期上报并存储在ES等持久层中。一旦获得数据,就可以根据数据进行可视化分析。
表现如何
接下来大家肯定更关心的是性能,那么我们来看看官方的评测数据:
图中蓝色代表未使用的性能,橙色代表已使用的性能。以上是TPS为5000时测得的数据。
可以看到,无论是CPU、内存,还是响应时间,使用带来的性能损失几乎可以忽略不计。
接下来我们看一下与业界另一知名分布式追踪工具的对比(采样率为每秒1次、线程数为500、总共5000个请求的情况下的对比)。
可以看出,在按键响应时间()中,()远远不如(22ms)!
从性能损失的角度来看,完全是胜利了!
我们来看另一个指标:代码的侵入性有多大。它需要隐藏在应用程序中。代码的侵入性较强,通过修改字节码的+插件方式可以避免对代码的任何侵入。入侵。
除了具有良好的性能和对代码的侵入性之外,它还具有以下优点:
我公司在分布式调用链上的实践
我们公司的应用架构
从上面可以看出,有很多优点。那么我们是否使用了它的所有组件呢?事实上,我们还没有。我们看一下它在我们公司的应用架构:
从图中可以看出,我们只使用了“数据报告与分析”、“数据存储”和“数据可视化”三大组件进行采样。那么为什么不直接采用完整的解决方案呢?
因为我们的监控生态系统在接入之前是比较完整的。
如果我们完全替换它,首先没有必要,而且它可以满足我们大多数场景的需求。其次,系统更换成本高。第三,重新连接用户的学习成本非常高。
这也给我们一个启示:任何产品抓住机遇都很重要。后续产品的更新换代成本将会非常高。抓住机会,就等于抓住用户心智。这就好比微信,虽然UI和功能都做得很好,但是在国外还是做不到同样的事情,因为利用它的机会就没有了。
另一方面,对于建筑来说,没有最好,只有最适合。根据当前业务场景进行权衡取舍是架构设计的本质。
我公司的转型实践
我公司主要做了以下改造和实践:
①预发布环境因调试需要强制采样。
从上面的分析可以看出,采样是在后台定期进行的。这不是很好吗?为什么要实施强制抽样?
为了排查定位问题,线上有时会出现问题。我们希望在预发布中重现它们,并看到该请求的完整调用链,因此需要在预发布时实现强制采样。
所以我们修改了插件来实现强制采样。
我们在请求上放置一个键值对,例如=true,以表明我们想要强制采样。
网关收到后,会添加键值对=true。
然后插件可以据此判断是否是强制采样。如果有这个值,就是强制采样。如果没有该值,则将执行正常的计划采样。
②实现更细粒度的采样?
赶快进行更细粒度的采样。我们先看一下默认的采样方式,即统一采样。
我们知道该方法默认在3秒内采样前3次,并丢弃其他请求。在这种情况下,就有问题了。
假设本机3秒内多次调用 , ,但如果是前3次调用,则 等其他调用将不会被采样。
于是我们对其进行改造,实现分组抽样,如下:
也就是说3秒之内采样了3次, 等等,这样就避免了这个问题。
③如何嵌入到日志中?
嵌入输出日志可以让我们更方便的排查问题,所以把它打出来是非常有必要的。如何将其嵌入到日志中?
我们使用我们将在此处了解的插件机制。它允许我们自定义插件输出日志格式。首先,我们需要定义日志格式,并在自定义的日志格式中嵌入%作为占位符,如下:
然后我们实现一个插件,如下:
第一个插件必须定义一个类,该类必须继承 rter 类并将其自身声明为标准。
@注解指定要替换的占位符,然后在方法中替换。
这样,日志中就会出现我们想要的内容,如下:
④我司开发了哪些插件?
已经实现了很多插件,但是没有提供和的插件,所以我们根据这两个的规范开发了自己的插件:
插件是如何实现的?可以看到它主要由三部分组成:
也许看完之后你还是不明白,那么我们简单解释一下。我们知道,在服务中,每次请求都是从接收消息并提交到业务线程池处理开始,到真正调用业务方法结束。其间有十几道工序。各处理:
它可以拦截客户端发送的或服务器处理的所有请求,因此我们可以对其进行增强。
在调用方法之前将全局注入其中,这样可以在请求到达真正的业务逻辑之前确保全局存在。
所以显然我们需要在插件中指定我们要增强的(),并增强它的()。该方法应进行哪些改进?
这就是()的作用,我们看一下插件中的():
我们来看看代码中描述的拦截器()做了什么事情。关键步骤如下:
首先,意味着这里的方法会在执行的方法之前被调用。相应的,就是说,增强的逻辑会在方法执行完之后再做。
其次,从第2点和第3点可以看出,无论是or,它的全局ID都已经做了相应的处理。
这样就保证了到达真正业务层的时候,这个全局的情况是有保证的。定义and之后,最后一步是在.def中指定定义的类:
// skywalking-plugin.def 文件
dubbo=org.apache.skywalking.apm.plugin.asf.dubbo.DubboInstrumentation
这样封装的插件会对方法进行增强,在方法执行之前注入全局操作。所有这些都是静默的并且不会干扰代码。
总结
本文由浅入深地介绍了分布式追踪系统的原理。相信大家对其作用和工作机制有了更加深刻的认识。
需要特别注意的是,在引入某种技术时,一定要根据现有的技术架构做出最合理的选择。就像有四个模块一样,我们公司只使用了它的采样功能。没有最好的技术,只有最适合的技术。技术。
通过这篇文章,相信大家应该对.net的实现机制有了更加清晰的认识。文章只介绍插件的实现方法。但它毕竟是工业级软件。要了解它的广度和深度,必须阅读更多的源代码。