在分布式系统,尤其是微服务系统中,一次外部请求往往需要内部多个模块,多个中间件,多台机器的相互调用才能完成。在这一系列的调用中,可能有些是串行的,而有些是并行的。在这种情况下,我们如何才能确定这整个请求调用了哪些应用?哪些模块?哪些节点?以及它们的先后顺序和各部分的性能如何呢?
这就是分布式链路追踪技术(Distributed Tracing)要解决的问题。
分布式链路追踪是一种监控和诊断分布式系统中请求流程的技术。它通过记录和分析在一个请求过程中,各个微服务之间的调用关系、耗时等信息,帮助开发和运维人员理解系统的行为,快速定位问题所在。
思路
1、每个请求都使用一个唯一标识来追踪全部的链路显示在日志中,并且不修改原有的打印方式(代码无入侵)
2、使用Logback的MDC机制日志模板中加入traceId标识,取值方式为%X{traceId}
MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。
MDC使用
1 |
|
在日志配置文件中 <pattern> 节点中以%X{trace-id}取出,比如:<pattern>
1 | <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{50} %msg%n"/> |
问题与挑战
上面 slf4j 原始提供的 MDC,实现原理是通过 ThreadLocal ,所以在单线程场景下,已经完全胜任
但在多线程下,就会出现 traceId 丢失问题。链路追踪就会中断
ThreadLocal 的一个基本特性:子线程拿不到父线程的值。
主线程里MDC.put("traceId", "abc123"),
然后 @Async 开一个新线程去执行,新线程里MDC.get("traceId") 是 null。
解决
1、最差劲的解决办法,在业务代码中定义 traceId,手动传递。
2、要破除 ThreadLocal 只对当前线程有线的方法有两种:
一种是JDK自带的、ThreadLocal的扩展类InheritableThreadLocal,子线程会拷贝父线程中的变量值
一种是引入alibaba 的 transmittable-thread-local包的 TransmittableThreadLocal ( TTL )实现
TTL 原理
TTL 在提交任务时,先拍一个当前线程 TTL 变量的快照,然后把快照绑到任务上。
任务执行时,先把快照恢复到执行线程里,执行完再清理。
1 | 主线程 TTL: {traceId: "traceId-123qwe"} |
全程不需要业务代码参与。你不需要在异步方法里做任何处理。
SLF4J 的 MDC 只是一个门面,真正存数据的是 MDCAdapter
1 | public class MDC { |
所以我们需要自定义 MDCAdapter ,来增强默认 MDCAdapter 中使用的 ThreadLocal
自定义 TtlMDCAdapter类
基于上面 MDC 中的 static MDCAdapter mdcAdapter ,是个 package 访问权限。
所以要替换这个mdcAdapter,需要把 TtlMDCAdapter类 写在 org.slf4j包下
得创建一个 org.slf4j 包,来绕过 Java 访问权限机制
1 |
|
线程池必须统一用 TTL 包装
所有会执行异步任务的线程池都要包一层
1 | ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(originalExecutor); |
TtlExecutors 解决“线程池怎么传”;TtlMDCAdapter 解决“MDC 本身能不能被 TTL 传”。
替换TTL 版本
在 Spring 容器 refresh 之前,提前调用 TtlMDCAdapter.getInstance(),把 SLF4J MDC 底层的 MDCAdapter 替换成 TTL 版本
1 | public class ApplicationInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { |
完整链路
1 | ApplicationInitializer 启动时注册 TtlMDCAdapter |
改进项
这个方案是个通用方案,基本都是这么解决的
在 github 上,看到有人讲 TtlMDCAdapter 太丑陋了
TtlMDCAdapter is ugly, consider refactoring or removing it
https://github.com/trydofor/professional-wings/issues/187
我以为是提供了更好的方案,其实还没有跳脱出 slf4j 的限制
作者说 “TtlMDCAdapter is ugly” 的核心原因是:把一个业务类的实现放在 org.slf4j 包下太 hacky 了。 refactor 后,TtlMDCAdapter 回到了自己的包,TweakMDC 作为 org.slf4j 包下的”注入点”单独存在,职责分离更清晰。
1 | public class TweakMDC { |