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


146

私たちのソフトウェアでは、MDCを幅広く使用して、セッションIDやWebリクエストのユーザー名などを追跡しています。これは、元のスレッドで実行している間は正常に機能します。ただし、バックグラウンドで処理する必要があるものはたくさんあります。そのために、いくつかの自己ロール非同期実行サービスとともに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を継承するため)。どちらも本番システムでは深刻な問題です。

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


1
アプリケーションがJEE環境でデプロイされている場合は、Javaインターセプターを使用して、EJBを呼び出す前にMDCコンテキストを設定できます。
Maxim Kirilov 2013年

2
logbackバージョン1.1.5以降、MDC値は子スレッドに継承されなくなりました。
Ceki


2
@Cekiドキュメントを更新する必要があります:「子スレッドは、その親のマップされた診断コンテキストのコピーを自動的に継承します。」 logback.qos.ch/manual/mdc.html
steffen

スレッド間でMDCを使用する問題を解決するslf4jへのプルリクエストを作成しました(リンクgithub.com/qos-ch/slf4j/pull/150)。おそらく、人々がコメントしてそれを要求すると、SLF4Jに変更が組み込まれます:)
男性

回答:


79

はい、これも私が遭遇した一般的な問題です。いくつかの回避策があります(説明したように手動で設定するなど)が、理想的には、

  • MDCを一貫して設定します。
  • MDCが正しくないが知らない暗黙のバグを回避します。そして
  • スレッドプールの使用方法の変更を最小限に抑えます(たとえばCallableMyCallableあらゆる場所でのサブクラス化、または同様の醜さ)。

これら3つのニーズを満たす、私が使用するソリューションは次のとおりです。コードは自明です。

(補足として、このエグゼキュータを作成して、Guava 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);
                    }
                }
            }
        };
    }
}

前のコンテキストが空でない場合、それは常にゴミではありませんか?どうして持ち歩くの?
djjeck 2014

2
正しい; 設定しないでください。たとえば、wrap()メソッドが公開され、他の誰かによって使用されている場合、それは良い衛生状態のように思えます。
jlevy

このMdcThreadPoolExecutorがどのようにLog4J2によってアタッチまたは参照されたかに関する参照を提供できますか?このクラスを具体的に参照する必要がある場所はありますか、それとも「自動的に」行われますか?私はグアバを使用していません。できますが、使用する前に他の方法があるかどうか知りたいです。
jcb

私があなたの質問を正しく理解していれば、答えは「はい」です。これは、SLF4Jの「マジック」スレッドローカル変数です。MDC.setContextMap()などの実装を参照してください。また、これは、Log4JではなくSLF4Jを使用します。 Log4j、Logback、およびその他のロギング設定で機能するためです。
jlevy

1
完全を期すために、ThreadPoolTaskExecutorプレーンJavaではなくSpring ThreadPoolExecutorを使用している場合MdcTaskDecoratorは、moelholm.com / 2017/07/24 /…に
Pino

27

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


10
メソッドbeforeExecute(Thread, Runnable)とはafterExecute(Runnable, Throwable)、他の場合に役立つかもしれないが、私は確かにこれはMDCの設定のために動作しますかありませんよ。どちらも、生成されたスレッドの下で実行されます。これは、前にメインスレッドから更新されたマップを取得できる必要があることを意味しますbeforeExecute
Kenston Choi 2016

フィルターに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メソッドはRunnable、のCallable作成時に引数をラップするために上書きできます(受け入れられたソリューションを参照)RunnableFuture

注:この結果、executorServicesubmit方法が代わりに呼ばれなければならないexecute方法。

以下のためにScheduledThreadPoolExecutordecorateTask方法が代わりに上書きされます。


2

@Asyncアノテーションを使用してタスクを実行するSpringフレームワーク関連の環境でこの問題に直面した場合は、TaskDecoratorアプローチを使用してタスクを装飾できます。これを行う方法のサンプルがここに提供されています:https : //moelholm.com/blog/2017/07/24/spring-43-using-a-taskdecorator-to-copy-mdc-data-to-async-threads

私はこの問題に直面し、上記の記事は私がそれに取り組むのを助けたので、私はここでそれを共有しています。


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);
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.