如何在线程池中使用MDC?


146

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

这是Logback手册关于在这样的环境中使用MDC的内容:

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

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

这样做很好,但是忘记添加这些呼叫非常容易,并且直到太晚之前,都没有简便的方法来识别问题。使用Log4j的唯一迹象是您在日志中丢失了MDC信息,而使用Logback则获得了陈旧的MDC信息(因为胎面池中的线程从其上运行的第一个任务继承了其MDC)。两者都是生产系统中的严重问题。

我认为我们的处境没有任何特别之处,但是我在网络上找不到太多有关此问题的信息。显然,这不是很多人反对的事情,因此必须有一种避免的方法。我们在这里做错了什么?


1
如果您的应用程序是在JEE环境下部署的,则可以在调用EJB之前使用Java拦截器来设置MDC上下文。
Maxim Kirilov

2
从1.1.5版开始,子线程不再继承MDC值。
塞基


2
@Ceki文档需要更新:“子线程自动继承其父对象的映射诊断上下文的副本。” logback.qos.ch/manual/mdc.html
steffen

我向slf4j创建了一个拉取请求,以解决跨线程使用MDC的问题(链接github.com/qos-ch/slf4j/pull/150)。可能是,如果人们发表评论并提出要求,他们会将更改内容纳入SLF4J :)

Answers:


79

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

  • 一致地设置MDC;
  • 避免MDC不正确但您不知道的默认错误;和
  • 最小化对线程池使用方式的更改(例如CallableMyCallable随处可见的子类化或类似的丑陋情况)。

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

(作为附带说明,MoreExecutors.listeningDecorator()如果您使用Guava的,可以创建此执行程序并将其馈送到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);
                    }
                }
            }
        };
    }
}

如果先前的上下文不为空,那么它是否总是垃圾?为什么要随身携带?
djjeck 2014年

2
对; 不应该设置它。看起来好像卫生状况良好,例如,如果wrap()方法被其他人暴露并使用过。
jlevy

您能否提供有关Log4J2如何附加或引用此MdcThreadPoolExecutor的参考?是否需要在某个地方专门引用此类,还是“自动”完成了?我没有用番石榴。我可以,但是我想知道在使用它之前是否还有其他方法。
jcb

如果我正确理解您的问题,答案是肯定的,这是SLF4J中的“魔术”线程局部变量-请参见MDC.setContextMap()等的实现。此外,顺便说一下,这使用的是SLF4J,而不是Log4J,这是可取的因为它与Log4j,Logback和其他日志记录设置一起使用。
jlevy

1
出于完整性考虑:如果您使用的是Spring ThreadPoolTaskExecutor而不是纯Java ThreadPoolExecutor,则可以使用moelholm.com/2017/07/24/…中MdcTaskDecorator描述
Pino,

27

我们遇到了类似的问题。您可能需要扩展ThreadPoolExecutor并重写before / afterExecute方法,以在开始/停止新线程之前进行所需的MDC调用。


10
该方法beforeExecute(Thread, Runnable)afterExecute(Runnable, Throwable)可能在其他情况下,有益的,但我不知道这将如何设置的MDCs工作。它们都在生成的线程下执行。这意味着您必须能够从之前从主线程获取更新的地图beforeExecute
肯尼·崔

最好在过滤器中设置MDC,这意味着当请求由业务逻辑处理时,上下文将不会更新。我认为我们不应该在整个应用程序的任何地方更新MDC
dereck

15

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

  • 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();
            }
        };
    }

}

抱歉,不确定您的意思。更新:我认为我现在看到的将改善我的答案。
托马什Myšík

6

这是我使用固定线程池和执行程序执行的操作:

ExecutorService executor = Executors.newFixedThreadPool(4);
Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();

在线程部分:

executor.submit(() -> {
    MDC.setContextMap(mdcContextMap);
    // my stuff
});

2

与先前发布的解决方案类似,在创建时可以覆盖和的newTaskFor方法,以便包装自变量(请参见接受的解决方案)。RunnableCallableRunnableFuture

注:因此,executorServicesubmit方法必须被调用,而不是execute方法。

对于ScheduledThreadPoolExecutordecorateTask方法将被覆盖。



0

与现有答案类似的另一个变体是实现ExecutorService并允许将委托传递给它。然后使用泛型,它仍然可以公开实际的委托,以防有人想要获取某些统计信息(只要不使用其他修改方法)。

参考代码:

public class MDCExecutorService<D extends ExecutorService> implements ExecutorService {

    private final D delegate;

    public MDCExecutorService(D delegate) {
        this.delegate = delegate;
    }

    @Override
    public void shutdown() {
        delegate.shutdown();
    }

    @Override
    public List<Runnable> shutdownNow() {
        return delegate.shutdownNow();
    }

    @Override
    public boolean isShutdown() {
        return delegate.isShutdown();
    }

    @Override
    public boolean isTerminated() {
        return delegate.isTerminated();
    }

    @Override
    public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException {
        return delegate.awaitTermination(timeout, unit);
    }

    @Override
    public <T> Future<T> submit(Callable<T> task) {
        return delegate.submit(wrap(task));
    }

    @Override
    public <T> Future<T> submit(Runnable task, T result) {
        return delegate.submit(wrap(task), result);
    }

    @Override
    public Future<?> submit(Runnable task) {
        return delegate.submit(wrap(task));
    }

    @Override
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException {
        return delegate.invokeAll(wrapCollection(tasks));
    }

    @Override
    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException {
        return delegate.invokeAll(wrapCollection(tasks), timeout, unit);
    }

    @Override
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException {
        return delegate.invokeAny(wrapCollection(tasks));
    }

    @Override
    public <T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
        return delegate.invokeAny(wrapCollection(tasks), timeout, unit);
    }

    @Override
    public void execute(Runnable command) {
        delegate.execute(wrap(command));
    }

    public D getDelegate() {
        return delegate;
    }

    /* Copied from https://github.com/project-ncl/pnc/blob/master/common/src/main/java/org/jboss/pnc/common
    /concurrent/MDCWrappers.java */

    private static Runnable wrap(final Runnable runnable) {
        final Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            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);
                }
            }
        };
    }

    private static <T> Callable<T> wrap(final Callable<T> callable) {
        final Map<String, String> context = MDC.getCopyOfContextMap();
        return () -> {
            Map previous = MDC.getCopyOfContextMap();
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            try {
                return callable.call();
            } finally {
                if (previous == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(previous);
                }
            }
        };
    }

    private static <T> Consumer<T> wrap(final Consumer<T> consumer) {
        final Map<String, String> context = MDC.getCopyOfContextMap();
        return (t) -> {
            Map previous = MDC.getCopyOfContextMap();
            if (context == null) {
                MDC.clear();
            } else {
                MDC.setContextMap(context);
            }
            try {
                consumer.accept(t);
            } finally {
                if (previous == null) {
                    MDC.clear();
                } else {
                    MDC.setContextMap(previous);
                }
            }
        };
    }

    private static <T> Collection<Callable<T>> wrapCollection(Collection<? extends Callable<T>> tasks) {
        Collection<Callable<T>> wrapped = new ArrayList<>();
        for (Callable<T> task : tasks) {
            wrapped.add(wrap(task));
        }
        return wrapped;
    }
}

-3

我可以使用以下方法解决此问题

在主线程中(Application.java,我的应用程序的入口点)

static public Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();

在Executer调用的类的run方法中

MDC.setContextMap(Application.mdcContextMap);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.