web-dev-qa-db-ja.com

スレッドプールでMDCを使用する方法

このソフトウェアでは、MDCを広く使用して、WebリクエストのセッションIDやユーザー名などを追跡しています。これは、元のスレッドで実行中に正常に機能します。ただし、バックグラウンドで処理する必要があるものがたくさんあります。そのために、Java.concurrent.ThreadPoolExecutorクラスとJava.util.Timerクラスをいくつかの自動ロール非同期実行サービスとともに使用します。これらのサービスはすべて、独自のスレッドプールを管理します。

これは、そのような環境でMDCを使用することについて Logbackのマニュアル が言わなければならないことです。

マップされた診断コンテキストのコピーは、開始スレッドからワーカースレッドによって常に継承されるとは限りません。これは、スレッド管理にJava.util.concurrent.Executorsが使用される場合です。たとえば、newCachedThreadPoolメソッドはThreadPoolExecutorを作成し、他のスレッドプーリングコードと同様に、複雑なスレッド作成ロジックを備えています。

そのような場合、エグゼキュータにタスクを送信する前に、元の(マスター)スレッドでMDC.getCopyOfContextMap()を呼び出すことをお勧めします。タスクを実行すると、最初のアクションとして、MDC.setContextMapValues()を呼び出して、元のMDC値の保存されたコピーを新しいExecutor管理対象スレッドに関連付ける必要があります。

これは問題ありませんが、それらの呼び出しを追加するのを忘れるのは非常に簡単であり、手遅れになるまで問題を認識する簡単な方法はありません。 Log4jの唯一の兆候は、ログにMDC情報がないことです。Logbackを使用すると、古いMDC情報が得られます(トレッドプールのスレッドは、実行された最初のタスクからMDCを継承するため)。どちらも本番システムの深刻な問題です。

私たちの状況は決して特別なものではありませんが、ウェブ上でこの問題について多くを見つけることができませんでした。どうやら、これは多くの人がぶつかるようなものではないので、それを回避する方法が必要です。ここで何が間違っていますか?

123

はい、これは私が遭遇した共通の問題です。いくつかの回避策があります(説明のように手動で設定するなど)が、理想的には次のようなソリューションが必要です。

  • MDCを一貫して設定します。
  • MDCが間違っているが知らないという暗黙のバグを回避します。そして
  • スレッドプールの使用方法への変更を最小限に抑えます(たとえば、Callableをサブクラス化してMyCallableをどこでも、または同様のsimilarさ)。

これらの3つのニーズを満たす、私が使用するソリューションを次に示します。コードは一目瞭然です。

(補足として、グアバのListanableFutureを使用する場合、このエグゼキュータを作成してグアバのMoreExecutors.listeningDecorator()に供給することができます。)

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);
                    }
                }
            }
        };
    }
}
69
jlevy

同様の問題が発生しました。 ThreadPoolExecutorを拡張し、before/afterExecuteメソッドをオーバーライドして、新しいスレッドを開始/停止する前に必要なMDC呼び出しを行うことができます。

25
Mark

私見の最良の解決策は次のとおりです。

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

}
12
Tomáš Myšík

以前に投稿されたソリューションと同様に、引数をラップするためにnewTaskForおよびRunnableの-​​ Callable メソッドを上書きできます(承認されたソリューションを参照) RunnableFutureを作成するとき。

注:したがって、executorServiceメソッドの代わりにsubmitexecuteメソッドを呼び出す必要があります。

ScheduledThreadPoolExecutorの場合、 decorateTask メソッドが代わりに上書きされます。

2
MyKey_

これは、固定スレッドプールとエグゼキューターを使用して行う方法です。

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

スレッド部分で:

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

私は次のアプローチを使用してこれを解決できました

メインスレッド(Application.Java、私のアプリケーションのエントリポイント)

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

Executerによって呼び出されるクラスのrunメソッド内

MDC.setContextMap(Application.mdcContextMap);
0
smishra