Як використовувати MDC з пулами ниток?


146

У нашому програмному забезпеченні ми широко використовуємо MDC для відстеження таких речей, як ідентифікатори сесії та імена користувачів для веб-запитів. Це добре працює під час запуску в оригінальній нитці. Однак є багато речей, які потрібно обробити у фоновому режимі. Для цього ми використовуємо класи java.concurrent.ThreadPoolExecutorта java.util.Timerкласи разом із деякими службами виконання асинхронізації. Усі ці служби управляють власним пулом потоків.

Ось що говорить посібник Logback щодо використання MDC в таких умовах:

Копія відображеного діагностичного контексту не завжди може бути успадкована робочими потоками від ініціюючої нитки. Це той випадок, коли java.util.concurrent.Execitors використовується для управління потоками. Наприклад, метод newCchedThreadPool створює ThreadPoolExecutor і, як і інший код об'єднання ниток, має складну логіку створення ниток.

У таких випадках рекомендується MDC.getCopyOfContextMap () викликати в оригінальній (головній) темі перед тим, як подати завдання виконавцю. Коли завдання виконується, як його перша дія, воно повинно викликати MDC.setContextMapValues ​​(), щоб пов’язати збережену копію оригінальних значень MDC з новим керованим потоком виконавця.

Це було б добре, але забути додавання цих дзвінків дуже просто, і не існує простого способу розпізнати проблему, поки не пізно. Єдиний знак з Log4j полягає в тому, що ви отримуєте відсутність інформації про MDC в журналах, а при Logback ви отримуєте застарілу інформацію про MDC (оскільки нитка в пулі протектора успадковує його MDC від першого завдання, яке було виконано на ньому). Обидва є серйозними проблемами у виробничій системі.

Я не бачу в нашій ситуації особливої ​​особливості, але я не зміг знайти багато про цю проблему в Інтернеті. Мабуть, це не те, проти чого багато людей наштовхуються, тому повинен бути спосіб уникнути цього. Що ми робимо тут неправильно?


1
Якщо ваша програма розгорнута в середовищі JEE, ви можете використовувати перехоплювачі Java для встановлення контексту MDC перед викликом EJB.
Максим Кирилов

2
Починаючи з версії 1.1.5 для зворотного зв'язку, дочірні потоки значення MDC більше не успадковуються.
Чекі


2
@Ceki Документацію потрібно оновити: "Дочірня нитка автоматично успадковує копію відображеного діагностичного контексту свого батьківського". logback.qos.ch/manual/mdc.html
steffen

Я створив запит на тягу до slf4j, який вирішує проблему використання MDC в потоках (посилання github.com/qos-ch/slf4j/pull/150 ). Можливо, якщо люди прокоментують і запитають про це, вони включать зміни до SLF4J :)
Чоловік

Відповіді:


79

Так, це звичайна проблема, з якою я також стикався. Існує кілька обхідних завдань (наприклад, встановити їх вручну, як описано), але в ідеалі ви хочете вирішити це

  • Встановлює MDC послідовно;
  • Уникайте негласних помилок, де MDC неправильний, але ви цього не знаєте; і
  • Мінімізує зміни в тому , як використовувати пули потоків (наприклад , підкласи Callableз MyCallableвсюди, або подібним потворністю).

Ось таке рішення, яке я використовую, що відповідає цим трьом потребам. Код повинен бути роз'яснювальним.

(Як бічна примітка, цього виконавця можна створити та подати Гуаві MoreExecutors.listeningDecorator(), якщо ви використовуєте гуави 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

2
Правильно; його не слід встановлювати. Це просто здається гарною гігієною, наприклад, якщо метод wrap () піддавався та використовувався кимось іншим на дорозі.
jlevy

Чи можете ви надати посилання на те, як цей MdcThreadPoolExecutor був приєднаний або посилався на Log4J2? Чи є десь, де нам потрібно конкретно посилатися на цей клас, чи це "автоматично" зроблено? Я не використовую Guava. Я міг би, але хотів би знати, чи є ще якийсь спосіб до його використання.
jcb

Якщо я правильно розумію ваше запитання, відповідь - так, це "магічні" локальні змінні потоку в SLF4J - див. Реалізацію MDC.setContextMap () тощо. Також, до речі, для цього використовується SLF4J, а не Log4J, що є кращим. як це працює з Log4j, Logback та іншими налаштуваннями журналу.
веселий

1
Просто для повноти: якщо ви використовуєте Spring's ThreadPoolTaskExecutorзамість простої Java ThreadPoolExecutor, ви можете використовувати MdcTaskDecoratorописане на moelholm.com/2017/07/24/…
Піно

27

Ми зіткнулися з подібною проблемою. Можливо, ви захочете розширити ThreadPoolExecutor та замінити його до / після виконувати методи, щоб здійснити необхідні дзвінки MDC перед запуском / зупинкою нових потоків.


10
Методи beforeExecute(Thread, Runnable)і afterExecute(Runnable, Throwable)можуть бути корисні і в інших випадках , але я не впевнений , як це буде працювати для установки Головдержслужби. Вони обидва виконані під нерестиною нитки. Це означає, що ви повинні мати можливість отримати оновлену карту з головної нитки раніше beforeExecute.
Кенстон Чой

Краще встановити MDC у фільтрі, це означає, що коли запит обробляється за бізнес-логікою, контекст не буде оновлюватися. Я не думаю, що ми повинні оновлювати MDC скрізь у додатку
декрет

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

}

Вибачте, не дуже впевнений, що ти маєш на увазі. ОНОВЛЕННЯ: Я думаю, що зараз бачу, покращить свою відповідь.
Томаш Мишик

6

Ось так я це роблю з пулами з фіксованими нитками та виконавцями:

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

У нарізній частині:

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

2

Подібно до раніше розміщених рішень, newTaskForметоди для Runnableта Callableможуть бути перезаписані, щоб обернути аргумент (див. Прийняте рішення) при створенні RunnableFuture.

Примітка: Отже, метод executorService's submitповинен викликатися замість executeметоду.

Для ScheduledThreadPoolExecutor, ці decorateTaskметоди були б переписані замість цього.


2

Якщо ви зіткнулися з цією проблемою у середовищі, пов’язаному з весняними рамками, де ви виконуєте завдання за допомогою @Asyncанотації, ви можете прикрасити завдання, використовуючи підхід TaskDecorator . Зразок того, як це зробити, подано тут: https://moelholm.com/blog/2017/07/24/spring-43-using-a-taskdecorator-to-copy-mdc-data-to-async-threads

Я зіткнувся з цим питанням, і стаття вище допомогла мені вирішити цю проблему, тому я цим ділюсь тут.


0

Інший варіант, подібний до існуючих відповідей тут, полягає в тому, щоб реалізувати ExecutorServiceі дозволити передавати до нього делегата. Тоді, використовуючи generics, він все ще може викрити фактичного делегата на випадок, якщо хочеться отримати статистику (доки інші методи модифікації не використовуються).

Довідковий код:

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

У методі run класу, який викликається Executer

MDC.setContextMap(Application.mdcContextMap);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.