学习下MDC的机制
之前在前司的时候研究过一些链路日志相关的,有基于pinpoint,skywalking等中间件的,也有一些基于rpc框架的自研的,不过其中很多相通的逻辑应该是基于MDC来实现的,MDC 全称是 Mapped Diagnostic Context , 可以比较粗略的想成是一个存放诊断日志的工具
可以基于简单的代码来看一下1
2
3
4
5
6
7public static void main(String[] args) {
MDC.put(TRACE_ID, UUID.randomUUID().toString());
logger.info("开始业务逻辑处理");
logger.info("业务逻辑处理结束");
MDC.remove(TRACE_ID);
logger.info("TRACE_ID 还有吗?{}", MDC.get(TRACE_ID) != null);
}
另外再配置下1
2
3
4
5
6
7
8
9
10
11
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<layout class="ch.qos.logback.classic.PatternLayout">
<Pattern>[%t] [%X{TRACE_ID}] - %m%n</Pattern>
</layout>
</appender>
<root level="debug">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
一般springboot这种框架都是自带logback了的,没有的话可以引一下
打印下看1
2
3[main] [d8610b0d-7db8-4926-8de0-0e14e8342018] - 开始业务逻辑处理
[main] [d8610b0d-7db8-4926-8de0-0e14e8342018] - 业务逻辑处理结束
[main] [] - TRACE_ID 还有吗?false
那么MDC里究竟是啥呢
我们可以顺着 org.slf4j.MDC#put 的逻辑看下去,里面的是调用了个接口的1
2
3
4
5
6
7
8
9public static void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key parameter cannot be null");
} else if (mdcAdapter == null) {
throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");
} else {
mdcAdapter.put(key, val);
}
}
这个 org.slf4j.spi.MDCAdapter 接口我们再看下实现类
我们用的是logback,就看下这个实现 ch.qos.logback.classic.util.LogbackMDCAdapter
这里的put呢,就是基于ThreadLocal的实现1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public void put(String key, String val) throws IllegalArgumentException {
if (key == null) {
throw new IllegalArgumentException("key cannot be null");
}
Map<String, String> oldMap = copyOnThreadLocal.get();
Integer lastOp = getAndSetLastOperation(WRITE_OPERATION);
if (wasLastOpReadOrNull(lastOp) || oldMap == null) {
Map<String, String> newMap = duplicateAndInsertNewMap(oldMap);
newMap.put(key, val);
} else {
oldMap.put(key, val);
}
}
至于这些map的操作主要是为了父子线程之间来传递MDC信息用的
主要还是基于1
final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal<Map<String, String>>();
的操作,这样也就能保持线程安全
基于这个逻辑,我们在做链路日志串联的时候就有基础功能了
比如web请求,就在拦截器里可以先给MDC里设置好当前请求的请求traceId
当然这个traceId的唯一性就需要进行一步讨论比如雪花算法这种,不过一般这种情况带上机器id就可以简化点
在微服务之间进行rpc调用的时候可以基于类似于Dubbo的filter机制,在RpcContext中写入traceId,保证服务之间调用能够带上这个traceId
接下去就是更复杂的,比如定时任务,消息队列,
还有涉及到其他中间件,以及最重要的数据库的操作,需要研究类似于数据库连接池或者分库分表中间件的能力
只是总结下来,对于这类基础设施的实现,还是离不开MDC这个日志的基础能力
否则比如我们在整个应用写入日志的时候都需要处理这个traceId
有的说可以用切面,切面其实也是要基于MDC这种存储,另外就是整个应用的切面其实用起来也没那么方便
再说回来,traceId来串联日志的重要性在一般业务排查中还是起到非常大作用的,这个还有业界的dapper这种示范理念
再细化的还会在这个traceId中再叠加树形结构,去细化一个链路中的每次子调用
后面可以再对这些内容进行展开