找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3963

积分

0

好友

555

主题
发表于 3 天前 | 查看: 18| 回复: 0

在微服务架构中,一次请求往往涉及多个模块、中间件和多台机器之间的协作。这一系列调用中,有些是串行的,有些是并行的,那么我们该如何确定这个请求背后具体调用了哪些应用、哪些模块、哪些节点,以及它们的调用顺序呢?又该如何定位每个环节的性能瓶颈?本文将为你揭示答案。

本文将围绕以下几个方面展开:

  • 分布式追踪系统的原理及作用
  • SkyWalking 的原理及架构设计
  • 企业级分布式调用链实践案例

分布式追踪系统的原理及作用

如何评估一个接口的性能表现?我们通常至少会关注以下三个指标:

  • 接口的响应时间(RT)是多少?
  • 是否存在异常响应?
  • 主要的性能瓶颈在哪里?

单体架构

在公司发展初期,可能会采用如下图所示的单体架构。对于这种架构,我们该如何计算上述指标呢?

单体架构下的请求流向示意图

最容易想到的方法无疑是使用 AOP(面向切面编程)。

使用AOP进行性能监控的流程图
使用 AOP 在调用具体业务逻辑前后分别记录时间,即可计算出整体调用耗时;通过 AOP 捕获异常也能定位异常来源。

微服务架构

在单体架构中,所有服务和组件都部署在同一台机器上,因此实现这些监控指标相对容易。然而,随着业务快速发展,架构必然朝着微服务架构演进。

复杂的微服务架构示意图
图示:一个典型的复杂微服务架构

假设用户反馈某个页面响应缓慢,而我们知道该页面的请求调用链是 A -> C -> B -> D。此时,如何定位是哪个模块引发了问题?每个服务(A, B, C, D)都可能部署在多台机器上,我们又该如何知道某个请求具体调用了哪台机器?

无法定位具体调用机器的微服务架构示意图

可以明显看到,由于无法准确定位每个请求经过的确切路径,在微服务架构下面临以下几个痛点:

  1. 排查问题难度大,周期长
  2. 特定场景难以复现
  3. 系统性能瓶颈分析困难

分布式调用链系统正是为了解决以上问题而诞生,其主要作用包括:

  • 自动采集数据
  • 分析数据生成完整调用链:有了完整的请求调用链,问题复现的概率大大增加。
  • 数据可视化:每个组件的性能可视化,帮助我们快速定位系统瓶颈,及时发现问题。

通过分布式追踪系统,可以清晰定位如下请求的具体调用链路,从而实现轻松的请求链路追踪和各个模块的性能瓶颈分析。

跨服务调用链追踪示意图

分布式调用链标准 - OpenTracing

了解了分布式调用链的作用,接下来看看其实现原理。首先,为了解决不同分布式追踪系统 API 不兼容的问题,OpenTracing 规范应运而生。它是一个轻量级的标准化层,位于应用程序/类库与追踪或日志分析程序之间。

OpenTracing标准化层架构图
OpenTracing 通过提供平台无关、厂商无关的 API,使开发人员能够方便地添加或切换追踪系统的实现。

这类似于 Java 中的 JDBC。JDBC 提供一套标准接口,让各个数据库厂商去实现,程序员面对接口编程,无需关心底层实现。这里的接口就是标准,制定标准至关重要,它能实现组件的可插拔。

JDBC作为标准化接口的类比图

接下来,我们了解 OpenTracing 的核心数据模型,主要有以下三个概念:

  • Trace:一个完整的请求链路。
  • Span:一次调用过程(需包含开始时间和结束时间)。
  • SpanContext:Trace 的全局上下文信息,例如其中包含的 traceId

理解这三个概念至关重要。为了帮助大家更好地理解,可以参考下图:

Trace、Span、SpanContext概念关系示意图

如图所示,一次完整的下单请求就是一个 Trace。显然,对于这个请求,必须有一个全局标识来唯一标识它,这就是 traceId。每一次具体的调用称为一个 Span。每次调用都需要携带全局的 traceId,这样才能将每次调用与全局请求关联起来。traceId 正是通过 SpanContext 来传输的。我们可以把传输协议比作“车”,SpanContext 比作“货”,Span 比作“路”,这样或许更容易理解。

理解了基本概念,我们来看看分布式追踪系统如何采集一个统一视图中的微服务调用链。

Collector收集调用链数据示意图

可以看到,底层有一个 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 数据的自动采集。这种方式做到了对业务代码的 无侵入性。插件化意味着可插拔,扩展性好(下文将介绍如何自定义插件)。

SkyWalking Agent插件化采集原理图

如何跨进程传递 context

我们知道数据通常分为 header 和 body。例如 HTTP 有 header 和 body,RocketMQ 也有 MessageHeader 和 MessageBody。body 通常存放业务数据,因此不适合在 body 中传递 context,而应该在 header 中传递,如下图所示:

跨进程传递Context的header-body模型
Dubbo 中的 attachment 就相当于 header,所以我们把 context 放在 attachment 中,从而解决了 context 的传递问题。

Dubbo插件中Context传递流程
提示:此处的 context 传递流程均由 Dubbo 插件处理,业务层无感知。下文将分析插件的实现方式。

如何保证 traceId 的全局唯一性

要保证全局唯一性,可以采用分布式 ID 或本地生成 ID。使用分布式方案需要一个发号器,每次请求都需进行一次网络调用,会带来额外开销。因此,SkyWalking 最终选择了本地生成 ID 的方式,并采用了性能卓越的 Snowflake 算法。

Snowflake算法生成的ID结构图
图示:Snowflake 算法生成的 ID 结构

然而,Snowflake 算法有一个众所周知的问题:时间回拨。这可能导致生成的 ID 重复。那么 SkyWalking 是如何解决这个问题的呢?

SkyWalking解决时间回拨的代码逻辑
每生成一个 ID,都会记录生成时间 (lastTimestamp)。如果发现当前时间比上一次生成 ID 的时间 (lastTimestamp) 还小,则说明发生了时间回拨,此时会生成一个随机数作为 traceId。

这里可能会有同学提出疑问:生成的随机数是否也可能与已存在的全局 ID 重复?是否需要再加一层校验?

这就涉及到系统设计的权衡了。首先,如果对生成的随机数进行唯一性校验,无疑会增加一层调用,带来一定的性能损耗。但实际上,时间回拨发生的概率极低(一旦发生,机器时间紊乱,业务会受严重影响,因此机器时间调整必然慎之又慎),再加上随机数本身重复的概率也很小。综合考虑,此处确实没有必要再加一层全局唯一性校验。对于技术方案的选型,一定要避免过度设计,过犹不及。

请求量巨大,全部采集是否会影响性能?

如果对每个请求都进行采集,数据量无疑会非常庞大。但反过来想,真的有必要采集每一个请求吗?其实并非如此。我们可以设置采样频率,只采集部分数据。SkyWalking 默认设置为 3 秒内采样 3 次,其余请求不采样,如下图所示:

SkyWalking默认采样频率示意图

这样的采样频率通常足以分析组件性能。但这种“3秒采样3次”的方式会有什么潜在问题呢?

理想情况下,每个服务调用都在同一时间点发生(如下图),那么每次在同一时间点采样确实没问题。

理想情况下的服务调用时序图

但在生产环境中,由于网络延迟等因素,服务调用几乎不可能完全同步,实际调用情况很可能如下图所示:

实际情况下的服务调用时序图

这会导致某些调用在服务 A 上被采样了,却在服务 B、C 上未被采样,从而无法分析完整的调用链性能。那么 SkyWalking 是如何解决的呢?

它的解决方案是:如果上游调用携带了 Context(表明上游已采样),则下游强制采集数据。 这样可以保证调用链的完整性。

SkyWalking 的基础架构

SkyWalking 的基础架构如下图所示,几乎所有的分布式调用链系统都由以下几个核心组件构成:

SkyWalking可观测性分析平台架构图
首先是节点数据的定时采样,采样后将数据上报,存储到 Elasticsearch、MySQL 等持久化层。有了数据,自然可以基于数据进行可视化分析。

SkyWalking 的性能表现如何?

接下来大家肯定关心 SkyWalking 的性能开销。我们来看一下官方的基准测试数据。

SkyWalking性能损耗对比柱状图
图中蓝色柱代表未使用 SkyWalking 时的表现,橙色柱代表使用 SkyWalking 后的表现。这是在 TPS 为 5000 的情况下测得的数据。可以看出,无论是 CPU、内存占用还是响应时间,SkyWalking 带来的性能损耗几乎可以忽略不计。

我们再来看 SkyWalking 与业界另外两款知名分布式追踪工具 Zipkin、Pinpoint 的对比(测试条件:采样率 1 秒 1 个,线程数 500,总请求数 5000)。可以看到,在关键的响应时间指标上,Zipkin (117ms) 和 PinPoint (201ms) 远逊于 SkyWalking (22ms)!

多种APM工具性能对比表格

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

再看另一个重要指标:对代码的侵入性。ZipKin 需要在应用程序中埋点,对代码侵入性强。而 SkyWalking 采用 Javaagent + 插件化修改字节码的方式,可以做到 对代码完全无侵入

除了性能和无侵入性的优势,SkyWalking 还有以下几点长处:

  • 多语言支持与丰富组件:目前支持 Java, .Net Core, PHP, NodeJS, Golang, LUA 等语言,组件上也支持 Dubbo、MySQL 等常见中间件,能满足大部分需求。
  • 良好的扩展性:对于官方未提供的插件,我们可以按照 SkyWalking 的规范手动编写。新实现的插件同样对代码无侵入。

企业级分布式调用链实践

SkyWalking 在实践中的应用架构

由上文可知 SkyWalking 有许多优点,那么我们是否采用了它的全部组件呢?其实不然,下图展示了其在某公司的实际应用架构:

企业自定义的SkyWalking应用架构图

从图中可以看出,该公司只采用了 SkyWalking 的 Agent 进行数据采样,而放弃了其另外的“数据上报及分析”、“数据存储”、“数据可视化”三大组件。为什么不直接采用 SkyWalking 的全套方案呢?原因在于,在接入 SkyWalking 之前,该公司已有的 Marvin 监控生态体系已经相对完善。如果整个替换为 SkyWalking,一来没有必要(Marvin 已能满足大多数需求),二来系统替换成本高,三来用户重新学习成本高。

这也给我们一个启示:任何产品,抢占先机非常重要,后续的替换成本会很高。抢占先机,就是抢占用户的心智。

从另一方面看,对于架构设计而言,没有最好的,只有最合适的。结合当前业务场景进行权衡与折中,才是架构设计的本质。

对 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 进行了改造,实现了“分组采样”,如下图所示:

分组采样示意图
即分别对 Redis、Dubbo、MySQL 等不同类型的调用进行独立的“3秒3次”采样,从而避免了上述问题。

如何在日志中嵌入 traceId?

在输出的日志中嵌入 traceId 对于排查问题至关重要。那么,该如何在日志中嵌入 traceId 呢?我们以 Log4j 为例,这需要了解 Log4j 的插件机制。Log4j 允许我们自定义插件来定义日志输出格式。

首先,需要定义日志格式,在自定义格式中嵌入 %traceId 作为占位符,如下配置所示:

Log4j配置中定义traceId占位符

然后,需要实现一个 Log4j 插件,如下代码所示:

自定义Log4j TraceId转换器代码
首先,Log4j 插件需要定义一个继承 LogEventPatternConverter 的类,并用 @Plugin 注解将其声明为插件。通过 @ConverterKeys 注解指定要替换的占位符(这里是“traceId”),然后在 format 方法中实现替换逻辑。

这样,在输出的日志中就会出现我们想要的 TraceId,如下图所示:

日志中输出TraceId示例

自研了哪些 SkyWalking 插件?

SkyWalking 官方提供了大量插件,但并未覆盖所有组件,例如当时缺少 Memcached 和 Druid 的插件。因此,我们根据其规范自研了这两者的插件。

SkyWalking插件组成与目录结构图

那么,插件是如何实现的呢?从上图可以看到,一个插件主要由三部分组成:

  1. 插件定义类:在 skywalking-plugin.def 文件中指定插件的入口类。
  2. Instrumentation:指定切面(Aspect)和切点(Pointcut),即要对哪个类的哪个方法进行增强。
  3. Interceptor:指定在步骤2中方法的前置(before)、后置(after)或异常(exception)时机,具体执行哪些增强逻辑。

可能听起来还是有些抽象,我们以 Dubbo 插件为例简单讲解。我们知道在 Dubbo 服务中,每个请求从 Netty 接收消息、递交给业务线程池处理开始,到真正调用业务方法结束,中间会经过十几个 Filter 的处理。

Dubbo Filter调用栈示例
其中,MonitorFilter 可以拦截所有客户端发出的请求和服务端处理的请求。因此,我们可以对 MonitorFilter 进行增强,在其 invoke 方法被调用前,将全局 traceId 注入到其 Invocationattachment 中。这样可以确保在请求到达真正的业务逻辑之前,就已经存在全局 traceId

所以,我们需要在插件中指定要增强的类(MonitorFilter)及其方法(invoke)。要对这个方法做什么样的增强,则由拦截器(Interceptor)来定义。先来看看 Dubbo 插件中的 Instrumentation 定义(DubboInstrumentation):

Dubbo插件的Instrumentation定义代码

我们再看看拦截器(Interceptor)具体做了什么。以下列出关键步骤的代码:

Dubbo拦截器的beforeMethod关键逻辑
首先,beforeMethod 代表在执行 MonitorFilterinvoke 方法会调用这里的逻辑。与之对应的是 afterMethod,代表在 invoke 方法执行增强。

其次,从代码注释的第2、3点可以看出,无论是 Consumer 端还是 Provider 端,都对其全局 ID (traceId) 进行了相应处理,确保在请求到达业务层时已携带此全局 traceId

定义好 Instrumentation 和 Interceptor 后,最后一步就是在 skywalking-plugin.def 文件中指定定义的类:

// skywalking-plugin.def 文件
dubbo=org.apache.skywalking.apm.plugin.asf.dubbo.DubboInstrumentation

这样打包出来的插件就会对 MonitorFilterinvoke 方法进行增强,在 invoke 方法执行前对其 attachment 进行注入全局 traceId 等操作。这一切都是静默的,对业务代码完全无侵入

总结

本文由浅入深地介绍了分布式追踪系统的基本原理,相信大家对其作用和工作机制有了比较深入的理解。特别需要注意的是,引入任何技术时,一定要结合现有的技术架构做出最合理的选择,就像案例中只采用 SkyWalking 的 Agent 采样功能一样。没有最好的技术,只有最合适的技术

通过本文,相信大家对 SkyWalking 的实现机制有了比较清晰的认识。文中只是简单介绍了 SkyWalking 插件的实现方式,它作为一款工业级软件,其内部实现博大精深,想要深入了解,还需要多阅读源码。

希望本文的技术分享能对大家在理解和实践分布式追踪时有所帮助。更多关于系统架构、微服务等话题的深度讨论,欢迎在技术社区如云栈社区进行交流。




上一篇:Java开发者进阶指南:30篇高并发、Spring与数据库实战精华文章解析
下一篇:分布式调用链追踪系统实现原理:微服务架构下的性能问题定位与监控设计
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 11:20 , Processed in 0.564432 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表