Come usare MDC con pool di thread?


146

Nel nostro software utilizziamo ampiamente MDC per tracciare cose come ID di sessione e nomi utente per richieste web. Funziona bene durante l'esecuzione nel thread originale. Tuttavia, ci sono molte cose che devono essere elaborate in background. Per questo usiamo le classi java.concurrent.ThreadPoolExecutore java.util.Timerinsieme ad alcuni servizi di esecuzione asincrona self-roll. Tutti questi servizi gestiscono il proprio pool di thread.

Questo è ciò che il manuale di Logback ha da dire sull'uso di MDC in tale ambiente:

Una copia del contesto diagnostico mappato non può sempre essere ereditata dai thread di lavoro dal thread di avvio. Questo è il caso in cui java.util.concurrent.Executors viene utilizzato per la gestione dei thread. Ad esempio, il metodo newCachedThreadPool crea un ThreadPoolExecutor e, come altri codici di pool di thread, ha una complessa logica di creazione dei thread.

In tali casi, si consiglia di richiamare MDC.getCopyOfContextMap () sul thread originale (principale) prima di inviare un'attività all'esecutore. Quando l'attività viene eseguita, come prima azione, deve richiamare MDC.setContextMapValues ​​() per associare la copia memorizzata dei valori MDC originali al nuovo thread gestito di Executor.

Questo andrebbe bene, ma è molto facile dimenticare di aggiungere quelle chiamate e non esiste un modo semplice per riconoscere il problema fino a quando non è troppo tardi. L'unico segno con Log4j è che si ottengono informazioni MDC mancanti nei registri e con Logback si ottengono informazioni MDC non aggiornate (poiché il thread nel pool di tread eredita il proprio MDC dalla prima attività eseguita su di esso). Entrambi sono seri problemi in un sistema di produzione.

Non vedo la nostra situazione in alcun modo speciale, ma non ho trovato molto su questo problema sul web. Apparentemente, questo non è qualcosa contro cui molte persone si scontrano, quindi ci deve essere un modo per evitarlo. Cosa stiamo facendo di sbagliato qui?


1
Se l'applicazione viene distribuita in ambiente JEE, è possibile utilizzare gli intercettori java per impostare il contesto MDC prima che venga richiamato EJB.
Maxim Kirilov,

2
A partire dalla versione 1.1.5 del logback, i valori MDC non sono più ereditati dai thread figlio.
Ceki,


2
@Ceki La documentazione deve essere aggiornata: "Un thread figlio eredita automaticamente una copia del contesto diagnostico mappato del suo genitore." logback.qos.ch/manual/mdc.html
steffen

Ho creato una richiesta pull per slf4j che risolve il problema dell'utilizzo di MDC tra thread (link github.com/qos-ch/slf4j/pull/150 ). Può essere, se le persone commentano e lo chiedono, incorporeranno il cambiamento in SLF4J :)
Maschio

Risposte:


79

Sì, anche questo è un problema comune. Ci sono alcune soluzioni alternative (come l'impostazione manuale, come descritto), ma idealmente vuoi una soluzione che

  • Imposta l'MDC in modo coerente;
  • Evita i bug taciti in cui l'MDC è errato ma non lo conosci; e
  • Riduce al minimo le modifiche al modo in cui si utilizzano i pool di thread (ad es. Sottoclasse Callablecon MyCallableovunque o bruttezza simile).

Ecco una soluzione che uso che soddisfa queste tre esigenze. Il codice dovrebbe essere autoesplicativo.

(Come nota a margine, questo esecutore può essere creato e inviato a Guava MoreExecutors.listeningDecorator(), se usi 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);
                    }
                }
            }
        };
    }
}

Nel caso in cui il contesto precedente non sia vuoto, non è sempre spazzatura? Perché lo porti in giro?
Djjeck,

2
Destra; non dovrebbe essere impostato. Sembra solo una buona igiene, ad esempio se il metodo wrap () fosse esposto e utilizzato da qualcun altro lungo la strada.
jlevy,

Potete fornire un riferimento su come questo MdcThreadPoolExecutor è stato collegato o referenziato da Log4J2? Esiste un luogo in cui dobbiamo fare specifico riferimento a questa classe o è "automagicamente" fatto? Non sto usando Guava. Potrei, ma vorrei sapere se c'è qualche altro modo prima di usarlo.
jcb

Se capisco correttamente la tua domanda, la risposta è sì, sono variabili "magiche" thread-local in SLF4J - vedi le implementazioni di MDC.setContextMap () ecc. Inoltre, a proposito, usa SLF4J, non Log4J, che è preferibile come funziona con Log4j, Logback e altre configurazioni di registrazione.
jlevy,

1
Solo per completezza: se stai usando Spring's ThreadPoolTaskExecutorinvece di Java semplice ThreadPoolExecutor, puoi usare il MdcTaskDecoratordescritto su moelholm.com/2017/07/24/…
Pino

27

Abbiamo riscontrato un problema simile. Potresti voler estendere ThreadPoolExecutor e sovrascrivere i metodi before / afterExecute per effettuare le chiamate MDC necessarie prima di avviare / interrompere nuovi thread.


10
I metodi beforeExecute(Thread, Runnable)e afterExecute(Runnable, Throwable)possono essere utili in altri casi, ma non sono sicuro di come funzionerà per l'impostazione degli MDC. Entrambi vengono eseguiti sotto il thread generato. Ciò significa che prima devi essere in grado di ottenere la mappa aggiornata dal thread principale beforeExecute.
Kenston Choi,

Meglio impostare gli MDC nel filtro, ciò significa che quando la richiesta è in fase di elaborazione secondo la logica aziendale, il contesto non verrà aggiornato. Non credo che dovremmo aggiornare MDC ovunque nell'applicazione
dereck

15

IMHO la soluzione migliore è:

  • uso ThreadPoolTaskExecutor
  • implementa il tuo TaskDecorator
  • usalo: executor.setTaskDecorator(new LoggingTaskDecorator());

Il decoratore può apparire così:

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

}

Scusa, non sono davvero sicuro di cosa intendi. AGGIORNAMENTO: Penso di vedere ora, migliorerò la mia risposta.
Tomáš Myšík,

6

Ecco come lo faccio con pool di thread ed esecutori fissi:

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

Nella parte di threading:

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

2

Analogamente alle soluzioni precedentemente pubblicate, i newTaskFormetodi per Runnablee Callablepossono essere sovrascritti per racchiudere l'argomento (vedere la soluzione accettata) durante la creazione di RunnableFuture.

Nota: Di conseguenza, executorServiceè submitnecessario chiamare il executemetodo al posto del metodo.

Al contrario ScheduledThreadPoolExecutor, i decorateTaskmetodi verrebbero sovrascritti.


2

Nel caso in cui si presenti questo problema in un ambiente correlato al framework di primavera in cui si eseguono attività utilizzando l' @Asyncannotazione, è possibile decorare le attività utilizzando l' approccio TaskDecorator . Un esempio di come farlo è fornito qui: https://moelholm.com/blog/2017/07/24/spring-43-using-a-taskdecorator-to-copy-mdc-data-to-async-threads

Ho affrontato questo problema e l'articolo sopra mi ha aiutato ad affrontarlo, ecco perché lo condivido qui.


0

Un'altra variante simile alle risposte esistenti qui è quella di implementare ExecutorServicee consentire a un delegato di passargli. Quindi, usando i generici, può comunque esporre il delegato effettivo nel caso in cui si desideri ottenere alcune statistiche (purché non vengano utilizzati altri metodi di modifica).

Codice di riferimento:

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

Sono stato in grado di risolverlo usando il seguente approccio

Nel thread principale (Application.java, il punto di ingresso della mia applicazione)

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

Nel metodo run della classe che viene chiamata da Executer

MDC.setContextMap(Application.mdcContextMap);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.