如何在线程池中使用MDC?

Lór*_*tér 128 java logging logback slf4j mdc

在我们的软件中,我们广泛使用MDC来跟踪会话ID和Web请求的用户名等内容.这在原始线程中运行时工作正常.但是,有很多事情需要在后台处理.为此,我们使用java.concurrent.ThreadPoolExecutorjava.util.Timer类以及一些自动异步执行服务.所有这些服务都管理自己的线程池.

这就是Logback的手册在这样的环境中使用MDC所要说的:

映射的诊断上下文的副本不能始终由来自启动线程的工作线程继承.当java.util.concurrent.Executors用于线程管理时就是这种情况.例如,newCachedThreadPool方法创建一个ThreadPoolExecutor,就像其他线程池代码一样,它具有复杂的线程创建逻辑.

在这种情况下,建议在将任务提交给执行程序之前,在原始(主)线程上调用MDC.getCopyOfContextMap().当任务运行时,作为其第一个操作,它应调用MDC.setContextMapValues()以将原始MDC值的存储副本与新的Executor托管线程相关联.

这样会很好,但是很容易忘记添加这些调用,并且没有简单的方法来识别问题,直到为时已晚.Log4j的唯一标志是您在日志中丢失了MDC信息,而使用Logback,您会收到过时的MDC信息(因为胎面池中的线程从其上运行的第一个任务继承其MDC).两者都是生产系统中的严重问题.

我不认为我们的情况有任何特殊之处,但我在网上找不到这个问题.显然,这不是很多人碰到的东西,所以必须有办法避免它.我们在这做错了什么?

jle*_*evy 72

是的,这是我遇到的一个常见问题.有一些解决方法(如手动设置,如上所述),但理想情况下,您需要一个解决方案

  • 一致地设置MDC;
  • 避免MDC不正确但您不知道的默认错误; 和
  • 最大限度地减少对线程池使用方式的更改(例如Callable,MyCallable在任何地方进行子类化或类似的丑陋).

这是我使用的解决方案,满足这三个需求.代码应该是不言自明的.

(作为旁注,MoreExecutors.listeningDecorator()如果您使用番石榴,可以创建此执行程序并将其提供给Guava ListanableFuture.)

import org.slf4j.MDC;

import java.util.Map;
import java.util.concurrent.*;

/**
 * A SLF4J MDC-compatible {@link ThreadPoolExecutor}.
 * <p/>
 * In general, MDC is used to store diagnostic information (e.g. a user's session id) in per-thread variables, to facilitate
 * logging. However, although MDC data is passed to thread children, this doesn't work when threads are reused in a
 * thread pool. This is a drop-in replacement for {@link ThreadPoolExecutor} sets MDC data before each task appropriately.
 * <p/>
 * Created by jlevy.
 * Date: 6/14/13
 */
public class MdcThreadPoolExecutor extends ThreadPoolExecutor {

    final private boolean useFixedContext;
    final private Map<String, Object> fixedContext;

    /**
     * Pool where task threads take MDC from the submitting thread.
     */
    public static MdcThreadPoolExecutor newWithInheritedMdc(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                                                            TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        return new MdcThreadPoolExecutor(null, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    /**
     * Pool where task threads take fixed MDC from the thread that creates the pool.
     */
    @SuppressWarnings("unchecked")
    public static MdcThreadPoolExecutor newWithCurrentMdc(int corePoolSize, int maximumPoolSize, long keepAliveTime,
                                                          TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        return new MdcThreadPoolExecutor(MDC.getCopyOfContextMap(), corePoolSize, maximumPoolSize, keepAliveTime, unit,
                workQueue);
    }

    /**
     * Pool where task threads always have a specified, fixed MDC.
     */
    public static MdcThreadPoolExecutor newWithFixedMdc(Map<String, Object> fixedContext, int corePoolSize,
                                                        int maximumPoolSize, long keepAliveTime, TimeUnit unit,
                                                        BlockingQueue<Runnable> workQueue) {
        return new MdcThreadPoolExecutor(fixedContext, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    private MdcThreadPoolExecutor(Map<String, Object> fixedContext, int corePoolSize, int maximumPoolSize,
                                  long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        this.fixedContext = fixedContext;
        useFixedContext = (fixedContext != null);
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> getContextForTask() {
        return useFixedContext ? fixedContext : MDC.getCopyOfContextMap();
    }

    /**
     * All executions will have MDC injected. {@code ThreadPoolExecutor}'s submission methods ({@code submit()} etc.)
     * all delegate to this.
     */
    @Override
    public void execute(Runnable command) {
        super.execute(wrap(command, getContextForTask()));
    }

    public static Runnable wrap(final Runnable runnable, final Map<String, Object> context) {
        return new Runnable() {
            @Override
            public void run() {
                Map previous = MDC.getCopyOfContextMap();
                if (context == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(context);
                }
                try {
                    runnable.run();
                } finally {
                    if (previous == null) {
                        MDC.clear();
                    } else {
                        MDC.setContextMap(previous);
                    }
                }
            }
        };
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 对; 它不应该被设置.这似乎是良好的卫生,例如,如果wrap()方法暴露并被其他人使用. (2认同)
  • 如果我正确理解你的问题,答案是肯定的,它是 SLF4J 中的“神奇”线程局部变量 - 请参阅 MDC.setContextMap() 等的实现。另外,顺便说一句,这使用 SLF4J,而不是 Log4J,这是更好的选择因为它可以与 Log4j、Logback 和其他日志记录设置一起使用。 (2认同)
  • 从 v1.7.7 开始,SFL4J API 使用泛型,上下文定义为“Map&lt;String, String&gt;”,而不是“Map&lt;String, Object&gt;”,因此代码需要一些细微的更改。 (2认同)
  • 只是为了完整性:如果您使用 Spring 的 `ThreadPoolTask​​Executor` 而不是普通的 Java `ThreadPoolExecutor`,则可以使用 https://moelholm.com/2017/07/24/spring-4-3-using 中描述的 `MdcTaskDecorator` -a-taskdecorator-to-copy-mdc-data-to-async-threads/ (2认同)

小智 26

我们遇到了类似的问题.您可能希望扩展ThreadPoolExecutor并覆盖before/afterExecute方法,以在启动/停止新线程之前进行所需的MDC调用.

  • 方法`beforeExecute(Thread,Runnable)`和`afterExecute(Runnable,Throwable)`在其他情况下可能会有所帮助,但我不确定这对于设置MDC有何用处.它们都是在生成的线程下执行的.这意味着您需要能够在`beforeExecute`之前从主线程获取更新的映射. (7认同)

Tom*_*šík 12

恕我直言,最好的解决方案是:

  • 使用 ThreadPoolTaskExecutor
  • 实现自己的 TaskDecorator
  • 用它: executor.setTaskDecorator(new LoggingTaskDecorator());

装饰者看起来像这样:

private final class LoggingTaskDecorator implements TaskDecorator {

    @Override
    public Runnable decorate(Runnable task) {
        // web thread
        Map<String, String> webThreadContext = MDC.getCopyOfContextMap();
        return () -> {
            // work thread
            try {
                // TODO: is this thread safe?
                MDC.setContextMap(webThreadContext);
                task.run();
            } finally {
                MDC.clear();
            }
        };
    }

}
Run Code Online (Sandbox Code Playgroud)

  • 只是为了让其他人知道:`ThreadPoolTask​​Executor` 和 `TaskDecorator` 都是 Spring 类。 (5认同)
  • 关于您关于线程安全的问题: MDC.getCopyOfContextMap() 可能不是线程安全的,如果与过时的 SLF4J 一起使用,它只会返回一个可变的 ThreadContext.getContext() 。对于较新的 SLF4J 版本,将返回 new HashMap&lt;&gt;(ThreadContext.getContext())。 (2认同)

Ama*_*y D 11

这就是我使用固定线程池和执行程序的方式:

ExecutorService executor = Executors.newFixedThreadPool(4);
Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();
Run Code Online (Sandbox Code Playgroud)

在穿线部分:

executor.submit(() -> {
    MDC.setContextMap(mdcContextMap);
    // my stuff
});
Run Code Online (Sandbox Code Playgroud)

  • 这种方法的一个潜在问题是:上下文值永远不会被取消设置。因此,如果随后在上下文未被覆盖的地方重新使用该线程,您将得到非常混乱的日志消息。 (6认同)

Son*_*ner 9

如果您在使用@Async注解运行任务的 spring 框架相关环境中遇到此问题,您可以使用TaskDecorator方法来装饰任务。

此处提供了如何执行此操作的示例:

我遇到了这个问题,上面的文章帮助我解决了这个问题,这就是我在这里分享的原因。