惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Security Archives - TechRepublic
Security Archives - TechRepublic
N
News and Events Feed by Topic
Last Week in AI
Last Week in AI
博客园 - 司徒正美
The GitHub Blog
The GitHub Blog
O
OpenAI News
The Last Watchdog
The Last Watchdog
T
The Blog of Author Tim Ferriss
M
MIT News - Artificial intelligence
P
Proofpoint News Feed
Forbes - Security
Forbes - Security
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
有赞技术团队
有赞技术团队
Jina AI
Jina AI
GbyAI
GbyAI
V
Vulnerabilities – Threatpost
L
LangChain Blog
Vercel News
Vercel News
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
AI
AI
博客园 - 聂微东
W
WeLiveSecurity
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Scott Helme
Scott Helme
罗磊的独立博客
Martin Fowler
Martin Fowler
S
Security Affairs
T
Tor Project blog
Recent Announcements
Recent Announcements
F
Fortinet All Blogs
美团技术团队
C
Cisco Blogs
PCI Perspectives
PCI Perspectives
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
Security @ Cisco Blogs
T
Threat Research - Cisco Blogs
A
About on SuperTechFans
Cisco Talos Blog
Cisco Talos Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
I
Intezer
B
Blog
WordPress大学
WordPress大学
I
InfoQ
G
Google Developers Blog
www.infosecurity-magazine.com
www.infosecurity-magazine.com
V
V2EX
P
Privacy & Cybersecurity Law Blog
雷峰网
雷峰网

Xudong's Blog

AI杀死了个人博客 新手选购微单的策略 一些让Python代码更快的技巧 HTTP报文结构 Python日志库logging总结 写定向爬虫时遇到的问题 Java中的协变与逆变 给Hexo主题添加LaTeX公式支持 Python3中的Iterator与Iterable Rss订阅源分享 Git Tips Redis的数据库与持久化 Redis中的数据结构 控制反转与依赖注入 Vim命令笔记 Java中的equals和hashCode方法 Java中对象域的初始化 Java中的基本类型和自动装拆箱 Hello, world. 使用Hexo搭建静态博客
让MDC在各种线程间穿梭自如
Xudong Sun · 2022-12-17 · via Xudong's Blog

在Java应用中,Logback或Log4j的MDC可以帮我们实现在记录日志时携带当前上下文之外的信息。一个典型的应用场景是,当一个HTTP请求进入Spring应用后,在拦截器中向MDC写入为当前请求生成trace id / transaction id,后续业务类中打印的log都可以携带id,方便日志的定位。

MDC的实现方式

由于我们经常使用Slf4j之类的log facade,在使用时只需要关心类似org.slf4j.MDC的类提供的接口就可以。但是在具体日志库实现时,通常会实现一个MDCAdapter接口以便门面MDC类调用。不论是Logback还是Log4j,实现MDC的方式都是通过ThreadLocal来存储我们写入MDC的数据。

以Logback为例,它的LogbackMDCAdapter类中的第一行代码就是创建一个ThreadLocal对象。

1
2
3
public class LogbackMDCAdapter implements MDCAdapter {
final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal();
... ...

新版本的logback使用了两个ThreadLocal成员变量 readWriteThreadLocalMapreadOnlyThreadLocalMap实现读写分离,在读取上下文时不会受到写操作的影响,去除了代码中复制map时使用的synchronized关键字,原来提高了并发访问效率。但是在LogbackMDCAdapterSimple的实现中,仍然使用了单个ThreadLocal成员变量来存储上下文Map。

奇怪的是,Slf4j提供的一个MDCAdapter基础实现类org.slf4j.helpers.BasicMDCAdapter中使用了一个InheritableThreadLocal解决子线程中MDC传递的问题。这个类的注释中也提到了,代码其实是参考LogbackMDCAdapter的,现在后者更新进化后更复杂了。那么为什么Logback现在反而使用了对子线程不友好的基础ThreadLocal类呢?

1
2
3
4
5
6
7
8
9
10
11
12
public class BasicMDCAdapter implements MDCAdapter {

private InheritableThreadLocal<Map<String, String>> inheritableThreadLocal = new InheritableThreadLocal<Map<String, String>>() {
@Override
protected Map<String, String> childValue(Map<String, String> parentValue) {
if (parentValue == null) {
return null;
}
return new HashMap<String, String>(parentValue);
}
};
... ...

结合LogbackMDCAdapter的代码和注释,google以后得知,放弃使用InheritableThreadLocal是因为logback的MDC曾因为这个类出现过内存泄露问题。并且,当子线程存在时,后续不管是父线程还是子线程调用MDC.put()方法,都会让子线程和父线程中的MDC上下文map出现差异,这可能导致调用者预期之外的结果。与其让这种预期之外的情况出现,不如让调用者自己管理线程之间的MDC传递,隔离不同线程下的MDC。

多线程下的MDC传递

跨线程传递

org.slf4j.MDC类提供了Map<String, String> getCopyOfContextMap()setContextMap(Map<String, String> contextMap)两个方法来方便我们获取和设置整个上下文Map。所以我们可以轻易写出类似下面的代码:

1
2
3
4
5
6
7
final Map<String, String> mdcMap = MDC.getCopyOfContextMap();
new Thread(() -> {
MDC.setContextMap(mdcMap);
try { ... ... } finally {
MDC.clear();
}
}).start();

通过lambda外的final Map变量来设置原来线程上下文的MDC非常方便。需要注意的是,一定要在线程结束前清空MDC,否则会导致内存泄露。如果是使用CompletableFuture,不要忘记在.exceptionally()的lambda里加上MDC.clear()

此时,如果我们实现一个辅助类来帮助装饰我们的Runnable lambda,会让代码更加简洁,可维护性更强。类中的包装方法结构都是类似的,只不过包装的对象不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class MDCDecorator {
public static <T> Callable<T> buildCallable(final Callable<T> callable) {
final Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
Map<String, String> previousMdcContext = MDC.getCopyOfContextMap();
if (context == null || context.isEmpty()) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
try {
return callable.call();
} finally {
if (previousMdcContext != null) {

MDC.setContextMap(previousMdcContext);
} else {

MDC.clear();
}
}
};
}

public static Runnable buildRunnable(final Runnable runnable) {
final Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
Map<String, String> previousMdcContext = MDC.getCopyOfContextMap();
if (context == null || context.isEmpty()) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
try {
runnable.run();
} finally {
if (previousMdcContext != null) {

MDC.setContextMap(previousMdcContext);
} else {

MDC.clear();
}
}
};
}

public static <T> Supplier<T> buildSupplier(Supplier<T> supplier) {
final Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
Map<String, String> previousMdcContext = MDC.getCopyOfContextMap();
if (context == null || context.isEmpty()) {
MDC.clear();
} else {
MDC.setContextMap(context);
}
try {

return supplier.get();
} finally {

if (previousMdcContext != null) {
MDC.setContextMap(previousMdcContext);
} else {

MDC.clear();
}
}
};
}
}

以后,不管是在手动创建线程时还是使用CompletableFuture,都可以很简单的带上MDC:

1
2
3
4
5
6
7
new Thread(MDCDecorator.buildRunnable(() -> {
... ...
})).start();

CompletableFuture.supplyAsync(MDCDecorator.buildSupplier(() -> {
return new Object();
}));

跨线程池传递

虽然上面的MDCDecorator也可以用在向线程池提交任务时。但是,对于我们经常在Spring应用程序中使用的ThreadPoolTaskExecutor,有更优雅的方法来设置一个装饰器。首先,要实现一个org.springframework.core.task.TaskDecorator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class MDCTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {

Map<String, String> contextMap = MDC.getCopyOfContextMap();
return () -> {
try {
if (contextMap != null) {

MDC.setContextMap(contextMap);
}

runnable.run();
} finally {

MDC.clear();
}
};
}
}

然后在初始化Executor的Bean时设置装饰器:

1
2
3
4
5
6
7
8
9
10
@Bean("taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
... ...

executor.setTaskDecorator(new MDCTaskDecorator());

executor.initialize();
return executor;
}

注意其它线程池

除了我们手动向线程池提交任务,Web服务中可能还会存在别的线程池。例如,在使用javax.websocket.server.ServerEndpoint(或jakarta.websocket.server.ServerEndpoint)注解标识的类会提供websocket接口,这个类中使用@OnMessage@OnOpen等注解的方法会被异步地在javax.websocket实现类内部维护的线程池中被回调。

在这种情况下,我们要借助javax.websocket.Session / jakarta.websocket.Session 对象来存储MDC信息。并在onMessage、onOpen等方法内部手动把Session中存储的ContextMap拿出来设置为MDC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@OnOpen
public void onOpen(Session session, EndpointConfig endpointConfig) {

MDC.put("traceId", TraceIDUtil.generate());

Map<String, String> contextMap = MDC.getCopyOfContextMap();
session.getUserProperties().put("MDC_CONTEXT_MAP", contextMap);
... ...
}

@OnMessage
public void onMessage(Session session, String message, @PathParam("model-type") String modelType) throws IOException {

Map<String, String> contextMap = (Map<String, String>) session.getUserProperties().get("MDC_CONTEXT_MAP");
MDC.setContextMap(contextMap);
try {
... ...
} finally {
MDC.clear();
}
}

有些Http请求库可能也会使用内部的线程池来优化并发性能,都需要根据具体类库来定制decorator。另外,跨服务的MDC传递也还没有讨论,这些内容后面有空再写。


Reference

Java Logging with Mapped Diagnostic Context (MDC) Baeldung
实现全面生态化的全链路日志追踪系统服务插件「Logback-MDC篇」
LOGBACK-620 Performance improvement for LogbackMDCAdapter - QOS.ch JIRA