码农戏码

新生代农民工的自我修养

0%

链路追踪TraceId

在分布式系统,尤其是微服务系统中,一次外部请求往往需要内部多个模块,多个中间件,多台机器的相互调用才能完成。在这一系列的调用中,可能有些是串行的,而有些是并行的。在这种情况下,我们如何才能确定这整个请求调用了哪些应用?哪些模块?哪些节点?以及它们的先后顺序和各部分的性能如何呢?

这就是分布式链路追踪技术(Distributed Tracing)要解决的问题。

分布式链路追踪是一种监控和诊断分布式系统中请求流程的技术。它通过记录和分析在一个请求过程中,各个微服务之间的调用关系、耗时等信息,帮助开发和运维人员理解系统的行为,快速定位问题所在。

思路

1、每个请求都使用一个唯一标识来追踪全部的链路显示在日志中,并且不修改原有的打印方式(代码无入侵)
2、使用Logback的MDC机制日志模板中加入traceId标识,取值方式为%X{traceId}

MDC(Mapped Diagnostic Context,映射调试上下文)是 log4j 和 logback 提供的一种方便在多线程条件下记录日志的功能。MDC 可以看成是一个与当前线程绑定的Map,可以往其中添加键值对。MDC 中包含的内容可以被同一线程中执行的代码所访问。当前线程的子线程会继承其父线程中的 MDC 的内容。当需要记录日志时,只需要从 MDC 中获取所需的信息即可。MDC 的内容则由程序在适当的时候保存进去。对于一个 Web 应用来说,通常是在请求被处理的最开始保存这些数据。

MDC使用

1
2
3

MDC.put(“traceId”, traceId); // 添加traceId
MDC.remove(“traceId”); // 移除traceId

在日志配置文件中 <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
2
3
4
5
6
7
8
9
10
11
主线程 TTL: {traceId: "traceId-123qwe"}  
    │
    ├─ 提交任务 → TTL 拍快照
    │
    ├─ 异步线程开始执行
    │    └─ TTL 恢复快照 → {traceId: "traceId-123qwe"}
    │    └─ log.info() → traceId=traceId-123qwe
    │    └─ 执行完毕 → TTL 清理
    │
    └─ 主线程继续

全程不需要业务代码参与。你不需要在异步方法里做任何处理。

SLF4J 的 MDC 只是一个门面,真正存数据的是 MDCAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MDC {

static MDCAdapter mdcAdapter;

static {
mdcAdapter = bwCompatibleGetMDCAdapterFromBinder();
}

public static void put(String key, String val) {
mdcAdapter.put(key, val);
}

public static String get(String key) {
return mdcAdapter.get(key);
}
}

所以我们需要自定义 MDCAdapter ,来增强默认 MDCAdapter 中使用的 ThreadLocal

自定义 TtlMDCAdapter类

基于上面 MDC 中的 static MDCAdapter mdcAdapter ,是个 package 访问权限。

所以要替换这个mdcAdapter,需要把 TtlMDCAdapter类 写在 org.slf4j包下

得创建一个 org.slf4j 包,来绕过 Java 访问权限机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

package org.slf4j;

import com.alibaba.ttl.TransmittableThreadLocal;
import org.slf4j.spi.MDCAdapter;

public class TtlMDCAdapter implements MDCAdapter {
/**
* 此处是关键
*/
private final ThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new TransmittableThreadLocal<>();

private static TtlMDCAdapter mtcMDCAdapter;

static {
mtcMDCAdapter = new TtlMDCAdapter();
MDC.mdcAdapter = mtcMDCAdapter; // 一定要在org.slf4j包下
}

public static MDCAdapter getInstance() {
return mtcMDCAdapter;
}

线程池必须统一用 TTL 包装

所有会执行异步任务的线程池都要包一层

1
ExecutorService ttlExecutor = TtlExecutors.getTtlExecutorService(originalExecutor);

TtlExecutors 解决“线程池怎么传”;
TtlMDCAdapter 解决“MDC 本身能不能被 TTL 传”。

替换TTL 版本

在 Spring 容器 refresh 之前,提前调用 TtlMDCAdapter.getInstance(),把 SLF4J MDC 底层的 MDCAdapter 替换成 TTL 版本

1
2
3
4
5
6
7
public class ApplicationInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TtlMDCAdapter.getInstance();
}
}

完整链路

1
2
3
4
5
6
7
8
9
10
11
ApplicationInitializer 启动时注册 TtlMDCAdapter
↓
MDC.put() 落到 TransmittableThreadLocal
↓
线程池被 TtlExecutors 包装
↓
提交任务时捕获 TTL 快照
↓
子线程执行时恢复 MDC
↓
异步日志带上 traceId

改进项

这个方案是个通用方案,基本都是这么解决的

在 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
2
3
4
5
6
7
8
public class TweakMDC {
public static void adapt(@NotNull MDCAdapter mdc) {
// init static block
var ignore = MDC.getMDCAdapter();
// replace current adapter
MDC.mdcAdapter = mdc;
}
}