it-swarm.com.de

Wie verwende ich MDC mit Thread-Pools?

In unserer Software verwenden wir MDC umfassend, um beispielsweise Sitzungs-IDs und Benutzernamen für Webanfragen zu verfolgen. Dies funktioniert gut, wenn im ursprünglichen Thread ausgeführt wird. Es gibt jedoch viele Dinge, die im Hintergrund verarbeitet werden müssen. Dazu verwenden wir die Klassen Java.concurrent.ThreadPoolExecutor und Java.util.Timer zusammen mit einigen selbstgerollten asynchronen Ausführungsservices. Alle diese Dienste verwalten ihren eigenen Thread-Pool.

Dies ist das, was Logbacks Handbuch zur Verwendung von MDC in einer solchen Umgebung zu sagen hat:

Eine Kopie des zugeordneten Diagnosekontexts kann nicht immer von Arbeitsthreads vom initiierenden Thread geerbt werden. Dies ist der Fall, wenn Java.util.concurrent.Executors für die Threadverwaltung verwendet wird. Zum Beispiel erstellt die newCachedThreadPool-Methode einen ThreadPoolExecutor, und wie andere Thread-Pooling-Codes verfügt sie über eine komplexe Thread-Erstellungslogik.

In solchen Fällen wird empfohlen, MDC.getCopyOfContextMap () im ursprünglichen (Master) -Thread aufzurufen, bevor eine Aufgabe an den Executor gesendet wird. Wenn der Task als erste Aktion ausgeführt wird, sollte er MDC.setContextMapValues ​​() aufrufen, um die gespeicherte Kopie der ursprünglichen MDC-Werte dem neuen verwalteten Executor-Thread zuzuordnen.

Dies wäre in Ordnung, aber es ist sehr leicht, das Hinzufügen dieser Anrufe zu vergessen, und es gibt keine einfache Möglichkeit, das Problem zu erkennen, bevor es zu spät ist. Das einzige Zeichen bei Log4j ist, dass MDC-Informationen in den Protokollen fehlen. Mit Logback erhalten Sie veraltete MDC-Informationen (da der Thread im Tread-Pool seinen MDC von der ersten Task übernimmt, die darauf ausgeführt wurde). Beides ist ein ernstes Problem in einem Produktionssystem.

Ich sehe unsere Situation in keiner Weise als etwas Besonderes, dennoch konnte ich im Web nicht viel über dieses Problem finden. Anscheinend stoßen viele Menschen nicht auf dieses Problem, daher muss es einen Weg geben, um dies zu vermeiden. Was machen wir hier falsch?

113

Ja, das ist ein häufiges Problem, auf das ich auch gestoßen bin. Es gibt einige Problemumgehungen (wie manuell einstellen, wie beschrieben), aber im Idealfall möchten Sie dies

  • Legt den MDC konsistent fest;
  • Vermeidet stillschweigende Fehler, bei denen der MDC nicht korrekt ist, aber Sie wissen es nicht; und
  • Minimiert Änderungen an der Verwendung von Thread-Pools (z. B. Unterklassifizierung von Callable mit MyCallable überall oder ähnlicher Hässlichkeit).

Hier ist eine Lösung, die ich verwende, um diese drei Anforderungen zu erfüllen. Code sollte selbsterklärend sein.

(Als Randbemerkung kann dieser Executor erstellt und an Guavas MoreExecutors.listeningDecorator() übergeben werden, wenn Sie Guavas ListanableFuture verwenden.)

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

Wir sind auf ein ähnliches Problem gestoßen. Möglicherweise möchten Sie ThreadPoolExecutor erweitern und die Methoden before/afterExecute überschreiben, um die erforderlichen MDC-Aufrufe auszuführen, bevor Sie neue Threads starten oder stoppen.

25
Mark

IMHO ist die beste Lösung:

  • verwenden Sie ThreadPoolTaskExecutor
  • eigene TaskDecorator implementieren
  • benutze es: executor.setTaskDecorator(new LoggingTaskDecorator());

Der Dekorateur kann so aussehen:

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

}
9
Tomáš Myšík

Ähnlich wie bei den zuvor veröffentlichten Lösungen können die Methoden newTaskFor für Runnable und Callable überschrieben werden, um das Argument (siehe akzeptierte Lösung) beim Erstellen der RunnableFuture zu umschließen. 

Hinweis: Daher muss die executorService-Methode der Variable submit anstelle der Methode execute aufgerufen werden.

Bei der Variable ScheduledThreadPoolExecutor würden stattdessen die Methoden decorateTask überschrieben.

2
MyKey_

Dies konnte ich mit folgendem Ansatz lösen

Im Hauptthread (Application.Java, Einstiegspunkt meiner Anwendung)

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

In der run-Methode der Klasse, die von Executer aufgerufen wird

MDC.setContextMap(Application.mdcContextMap);
0
smishra

So mache ich es mit festen Thread-Pools und Executoren:

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

Im Threading-Teil:

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