日志追踪对于功能问题的排查和数据流转的路径分析时非常重要的,有了全链路日志追踪体系机制可以非常有效且快速的定位问题,但在多线程环境中,若没有相关成熟的框架的支持,想要实现日志追踪,就需要手动将主线程中的日志参数传递给子线程,接下来就在线程池场景下借助MDC实现了traceId参数的透传。
方案1:解决办法是采用自定义的日志格式,把用户的信息采用某种方式编码在日志记录中。这种方式的问题在于要求在每个使用日志记录器的类中,都可以访问到用户相关的信息。这样才可能在记录日志时使用。这样的条件通常是比较难以满足的,MDC的作用是解决这个问题。
方案2: MDC(Mapped Diagnostic Context,映射调试上下文) 是slf4j提供的一种轻量级的日志跟踪工具,Log4j、Logback或者Log4j2等日志中最常见区分同一个请求的方式是通过线程名/线程ID,但是而如果请求量大,线程名/线程ID会在相邻的时间内出现多次重复的打印,因此引出了trace-id,即在接收到的时候生成唯一的请求id,在整个执行链路中带上此唯一id。
public class MDC {static final String NULL_MDCA_URL = "http://www.slf4j.org/codes.html#null_MDCA";static final String NO_STATIC_MDC_BINDER_URL = "http://www.slf4j.org/codes.html#no_static_mdc_binder";static MDCAdapter mdcAdapter;private MDC() {}private static MDCAdapter bwCompatibleGetMDCAdapterFromBinder() throws NoClassDefFoundError {try {return StaticMDCBinder.getSingleton().getMDCA();} catch (NoSuchMethodError var1) {return StaticMDCBinder.SINGLETON.getMDCA();}}public 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);}}public static MDCCloseable putCloseable(String key, String val) throws IllegalArgumentException {put(key, val);return new MDCCloseable(key);}public static String get(String key) 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 {return mdcAdapter.get(key);}}public static void remove(String key) 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.remove(key);}}public static void clear() {if (mdcAdapter == null) {throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");} else {mdcAdapter.clear();}}public static Map getCopyOfContextMap() {if (mdcAdapter == null) {throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");} else {return mdcAdapter.getCopyOfContextMap();}}public static void setContextMap(Map contextMap) {if (mdcAdapter == null) {throw new IllegalStateException("MDCAdapter cannot be null. See also http://www.slf4j.org/codes.html#null_MDCA");} else {mdcAdapter.setContextMap(contextMap);}}public static MDCAdapter getMDCAdapter() {return mdcAdapter;}static {try {mdcAdapter = bwCompatibleGetMDCAdapterFromBinder();} catch (NoClassDefFoundError var2) {mdcAdapter = new NOPMDCAdapter();String msg = var2.getMessage();if (msg == null || !msg.contains("StaticMDCBinder")) {throw var2;}Util.report("Failed to load class \"org.slf4j.impl.StaticMDCBinder\".");Util.report("Defaulting to no-operation MDCAdapter implementation.");Util.report("See http://www.slf4j.org/codes.html#no_static_mdc_binder for further details.");} catch (Exception var3) {Util.report("MDC binding unsuccessful.", var3);}}public static class MDCCloseable implements Closeable {private final String key;private MDCCloseable(String key) {this.key = key;}public void close() {MDC.remove(this.key);}}
}
Sl4fj的MDC模式主要的门面类是MDC.java,但是最核心的类是MDCAdapter,可由下面的代码观测出。
对应而定实现加载所有相关的MDCAdapter的类就在这里,而代码里面的功能展示信息也是我们经常会遇见的,如果出现错误的时候,经常会打印再日志的最开始部分。
大家可以观察到对应的MDCAdapter的实现类有一下这几种,基本上是会交由第三方去实现的。
而对于Sl4j本身不提供传递traceId的能力,真正提供能力的是MDCAdapter接口的实现。
Logback使用的是LogbackMDCAdapter。比如Log4j的是Log4jMDCAdapter,Logback的是LogbackMDCAdapter。其内部自己的MDCAdapter属于空实现的NOPMDCAdapter类和BasicMDCAdapter,第三方的OPMDCAdapter,不太了解。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sw9tGguI-1668663492640)(https://oscimg.oschina.net/oscnet/up-50011a2da52c96dfaf890a62f1e6b268341.png)]
public class BasicMDCAdapter implements MDCAdapter {private InheritableThreadLocal
当在logback.xml中配置了%X{key} 或 SiftingAppender的的,在需要输出日志的时候,从MDC中获取对应的key值,然后append到日志字符串中或生成文件路径,然后输出。
public class LogbackMDCAdapter implements MDCAdapter {final ThreadLocal> copyOnThreadLocal = new ThreadLocal();private static final int WRITE_OPERATION = 1;private static final int MAP_COPY_OPERATION = 2;final ThreadLocal lastOperation = new ThreadLocal();public LogbackMDCAdapter() {}private Integer getAndSetLastOperation(int op) {Integer lastOp = (Integer)this.lastOperation.get();this.lastOperation.set(op);return lastOp;}private boolean wasLastOpReadOrNull(Integer lastOp) {return lastOp == null || lastOp == 2;}private Map duplicateAndInsertNewMap(Map oldMap) {Map newMap = Collections.synchronizedMap(new HashMap());if (oldMap != null) {synchronized(oldMap) {newMap.putAll(oldMap);}}this.copyOnThreadLocal.set(newMap);return newMap;}public void put(String key, String val) throws IllegalArgumentException {if (key == null) {throw new IllegalArgumentException("key cannot be null");} else {Map oldMap = (Map)this.copyOnThreadLocal.get();Integer lastOp = this.getAndSetLastOperation(1);if (!this.wasLastOpReadOrNull(lastOp) && oldMap != null) {oldMap.put(key, val);} else {Map newMap = this.duplicateAndInsertNewMap(oldMap);newMap.put(key, val);}}}public void remove(String key) {if (key != null) {Map oldMap = (Map)this.copyOnThreadLocal.get();if (oldMap != null) {Integer lastOp = this.getAndSetLastOperation(1);if (this.wasLastOpReadOrNull(lastOp)) {Map newMap = this.duplicateAndInsertNewMap(oldMap);newMap.remove(key);} else {oldMap.remove(key);}}}}public void clear() {this.lastOperation.set(1);this.copyOnThreadLocal.remove();}public String get(String key) {Map map = (Map)this.copyOnThreadLocal.get();return map != null && key != null ? (String)map.get(key) : null;}public Map getPropertyMap() {this.lastOperation.set(2);return (Map)this.copyOnThreadLocal.get();}public Set getKeys() {Map map = this.getPropertyMap();return map != null ? map.keySet() : null;}public Map getCopyOfContextMap() {Map hashMap = (Map)this.copyOnThreadLocal.get();return hashMap == null ? null : new HashMap(hashMap);}public void setContextMap(Map contextMap) {this.lastOperation.set(1);Map newMap = Collections.synchronizedMap(new HashMap());newMap.putAll(contextMap);this.copyOnThreadLocal.set(newMap);}
主要问题是针对于Collections.synchronizedMap(new HashMap()),建立HashMap的安全模式进行构建容器对象,所以是线程安全的问题控制。
final ThreadLocal> copyOnThreadLocal = new ThreadLocal();
某些应用程序采用多线程的方式来处理多个用户的请求。在一个用户的使用过程中,可能有多个不同的线程来进行处理。典型的例子是Web应用服务器。当用户访问某个页面时,应用服务器可能会创建一个新的线程来处理该请求,也可能从线程池中复用已有的线程。
MDC.put(“trace-id”, traceId);
MDC.remove(“trace-id”);
在日志配置文件中节点中以%X{trace-id}取出,比如:%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [uniqid-%X{trace-id}] %logger{50}-%line - %m%n以上方式,只能在做put操作的当前线程中获取到值。那是因为MDC的实现原理主要就是ThreadLocal,ThreadLocal只对当前线程有效。例如我们举一个简单的Logback案例、log4j和log4j2也是一样的模式。
%d{yyyy-MM-dd HH:mm:ss.SSS} [%trace-id] %-5level %logger{36} - %msg%n
此外logback 中也可以使用占位符%X{ }来占位,替换到对应的 MDC 中 key 的值
%X{trace-id}- %m%n
当我们处于多个线程之间进行传递traceId的时候,可能就会存在ThreadLocal的问题了。那么如果要破除ThreadLocal只对当前线程有线的方法:
针对于跨线程传递traceId的问题,主要通过InheritableThreadLocal的方式进行拷贝传递即可,需要注意的是,如果没有采用线程池的场景的化,基本上不会出现什么问题,但是如果村子啊线程池的场景下那么就只能我们自己手动实现和处理了,如果采用TransmittableThreadLocal的话大家可以自行百度了,在这里我们主要实现的是自己去通过几种方式实现功能传递。
那么就可以采用TaskDecorator的线程任务装饰器方式为线程池的线程提供traceId的传递操作,例如以下代码。
此处我采用的是log back,如果是log4j或者log4j2还是有一些区别的,比如说MDC.getCopyOfContextMap()。
public class MDCTaskDecorator implements TaskDecorator {@Overridepublic Runnable decorate(Runnable runnable) {// 此时获取的是父线程的上下文数据Map contextMap = MDC.getCopyOfContextMap();return () -> {try {if (contextMap != null) {// 内部为子线程的领域范围,所以将父线程的上下文保存到子线程上下文中,而且每次submit/execute调用都会更新为最新的上 // 下文对象MDC.setContextMap(contextMap);}runnable.run();} finally {// 清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因MDC.clear();}};}
}
@Bean("taskExecutor")public Executor taskExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();//配置核心线程数executor.setCorePoolSize(5);//配置最大线程数executor.setMaxPoolSize(10);//配置队列大小executor.setQueueCapacity(100);//配置线程池中的线程的名称前缀executor.setThreadNamePrefix("mdc-trace-");// 异步MDCexecutor.setTaskDecorator(new MDCTaskDecorator());//执行初始化executor.initialize();return executor;}
这样就是先了traceId传递到线程池中了。
与上面的不同我们如果用的不是spring的线程池那么无法实现TaskDecorator接口,那么就无法实现他的功能了,此时我们就会定义我们自身的线程装配器。
public class MDCTaskDecorator {public static Callable buildCallable(final Callable callable, final Map context) {return () -> {if (CollectionUtils.isEmpty(context)) {MDC.clear();} else {//MDC.put("trace_id", IdUtil.objectId());MDC.setContextMap(context);}try {return callable.call();} finally {// 清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因MDC.clear();}};}public static Runnable buildRunnable(final Runnable runnable, final Map context) {return () -> {if (CollectionUtils.isEmpty(context)) {MDC.clear();} else {//MDC.put("trace_id", IdUtil.objectId());MDC.setContextMap(context);}try {runnable.run();} finally {// 清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因MDC.clear();}};}
}
清除子线程的,避免内存溢出,就和ThreadLocal.remove()一个原因
主线程中,如果使用了线程池,会导致线程池中丢失MDC信息;解决办法:需要我们自己重写线程池,在调用线程跳动run之前,获取到主线程的MDC信息,重新put到子线程中的。
public class ThreadPoolMDCExecutor extends ThreadPoolTaskExecutor {@Overridepublic void execute(Runnable task) {super.execute(MDCTaskDecorator.buildRunnable(task, MDC.getCopyOfContextMap()));}@Overridepublic Future> submit(Runnable task) {return super.submit(MDCTaskDecorator.buildRunnable(task, MDC.getCopyOfContextMap()));}@Overridepublic Future submit(Callable task) {return super.submit(MDCTaskDecorator.buildCallable(task, MDC.getCopyOfContextMap()));}
}
public class ThreadPoolMDCScheduler extends ThreadPoolTaskScheduler {@Overridepublic ScheduledFuture> scheduleWithFixedDelay(Runnable task, Date startTime, long delay) {return super.scheduleWithFixedDelay(MDCTaskDecorator.buildRunnable(task), startTime, delay);}@Overridepublic ScheduledFuture> schedule(Runnable task, Date startTime) {return super.schedule(MDCTaskDecorator.buildRunnable(task), startTime);}
}
同理,即使你使用ExecutorCompletionService实现多线程调用,也是相同的方案和思路机制。
使用CompletableFuture实现多线程调用,其中收集CompletableFuture运行结果,也可以手动使用相似的思路进行填充上下文信息数据,但是别忘记了清理clear就好。
private CompletableFuture test() {Map copyOfContextMap = MDC.getCopyOfContextMap();return CompletableFuture.supplyAsync(() -> {MDC.setContextMap(copyOfContextMap);//执行业务操作MDC.clear();return new Result();}, threadPoolExecutor).exceptionally(new Function() {@Overridepublic Result apply(Throwable throwable) {log.error("线程[{}]发生了异常[{}], 继续执行其他线程", Thread.currentThread().getName(), throwable.getMessage());MDC.clear();return null;}});}
后续的它的姊妹篇【Logback+Spring-Aop】实现全面生态化的全链路日志追踪系统服务插件「SpringAOP整合篇」,会进行分析通过SpringAOP模式将Logback的MDC模式与SpringAOP结合形成一个较为体系化的组件,从而减少了很多开发过程中的代码和问题。