Bagaimana cara menggunakan MDC dengan thread pools?

146

Dalam perangkat lunak kami, kami secara luas menggunakan MDC untuk melacak hal-hal seperti ID sesi dan nama pengguna untuk permintaan web. Ini berfungsi dengan baik saat berjalan di utas asli. Namun, ada banyak hal yang perlu diproses di latar belakang. Untuk itu kami menggunakan java.concurrent.ThreadPoolExecutordan java.util.Timerkelas bersama dengan beberapa layanan eksekusi async self-rolled. Semua layanan ini mengelola kumpulan utas mereka sendiri.

Ini adalah apa yang dikatakan oleh manual Logback tentang penggunaan MDC dalam lingkungan seperti itu:

Salinan konteks diagnostik yang dipetakan tidak selalu dapat diwarisi oleh utas pekerja dari utas inisiasi. Ini adalah kasus ketika java.util.concurrent.Executors digunakan untuk manajemen utas. Sebagai contoh, metode newCachedThreadPool menciptakan ThreadPoolExecutor dan seperti kode pooling thread lainnya, ia memiliki logika pembuatan thread yang rumit.

Dalam kasus seperti itu, disarankan agar MDC.getCopyOfContextMap () dipanggil pada utas (master) asli sebelum mengirimkan tugas ke pelaksana. Ketika tugas berjalan, sebagai tindakan pertama, ia harus memanggil MDC.setContextMapValues ​​() untuk mengaitkan salinan nilai MDC yang tersimpan dengan utas baru yang dikelola Pelaksana.

Ini akan baik-baik saja, tetapi sangat mudah untuk lupa menambahkan panggilan-panggilan itu, dan tidak ada cara mudah untuk mengenali masalahnya sampai semuanya terlambat. Satu-satunya tanda dengan Log4j adalah bahwa Anda mendapatkan info MDC yang hilang di log, dan dengan Logback Anda mendapatkan info MDC basi (karena utas di kumpulan tapak mewarisi MDC-nya dari tugas pertama yang dijalankan di dalamnya). Keduanya merupakan masalah serius dalam sistem produksi.

Saya tidak melihat situasi kami istimewa dengan cara apa pun, namun saya tidak dapat menemukan banyak tentang masalah ini di web. Rupanya, ini bukan sesuatu yang banyak orang lawan, jadi harus ada cara untuk menghindarinya. Apa yang kita lakukan salah di sini?

Lóránt Pintér
sumber
1
Jika aplikasi Anda digunakan dalam lingkungan JEE Anda dapat menggunakan pencegat java untuk mengatur konteks MDC sebelum EJB memanggil.
Maxim Kirilov
2
Pada versi logback 1.1.5, nilai MDC tidak lagi diwarisi oleh utas anak.
Ceki
jira.qos.ch/browse/LOGBACK-422 terselesaikan
lyjackal
2
@Ceki Dokumentasi perlu diperbarui: "Utas anak secara otomatis mewarisi salinan konteks diagnostik yang dipetakan induknya." logback.qos.ch/manual/mdc.html
steffen
Saya membuat permintaan tarik ke slf4j yang memecahkan masalah menggunakan MDC di utas (tautan github.com/qos-ch/slf4j/pull/150 ). Mungkin, jika orang berkomentar dan memintanya, mereka akan memasukkan perubahan dalam SLF4J :)
Pria

Jawaban:

79

Ya, ini adalah masalah umum yang saya alami. Ada beberapa solusi (seperti pengaturannya secara manual, seperti dijelaskan), tetapi idealnya Anda menginginkan solusi itu

  • Menetapkan MDC secara konsisten;
  • Menghindari bug diam-diam di mana MDC salah tetapi Anda tidak mengetahuinya; dan
  • Minimalkan perubahan pada cara Anda menggunakan kumpulan utas (mis. Subklas Callabledengan di MyCallablemana - mana, atau keburukan serupa).

Inilah solusi yang saya gunakan yang memenuhi ketiga kebutuhan ini. Kode harus jelas.

(Sebagai catatan, pelaksana ini dapat dibuat dan diumpankan ke Guava MoreExecutors.listeningDecorator(), jika Anda menggunakan 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);
                    }
                }
            }
        };
    }
}
cemburu
sumber
Jika konteks sebelumnya tidak kosong, bukankah itu selalu sampah? Mengapa Anda membawanya berkeliling?
djjeck
2
Baik; seharusnya tidak diatur. Sepertinya kebersihan yang baik, misalnya jika metode wrap () terbuka dan digunakan oleh orang lain di jalan.
jlevy
Bisakah Anda memberikan referensi tentang bagaimana MdcThreadPoolExecutor ini dilampirkan atau dirujuk oleh Log4J2? Adakah suatu tempat di mana kita perlu secara khusus mereferensikan kelas ini, atau apakah "secara otomatis" dilakukan? Saya tidak menggunakan Jambu Biji. Saya bisa, tetapi saya ingin tahu apakah ada cara lain sebelum menggunakannya.
jcb
Jika saya memahami pertanyaan Anda dengan benar, jawabannya adalah ya, itu adalah variabel "ajaib" thread-lokal di SLF4J - lihat implementasi MDC.setContextMap () dll. Selain itu, ini menggunakan SLF4J, bukan Log4J, yang lebih disukai karena berfungsi dengan Log4j, Logback, dan pengaturan logging lainnya.
jlevy
1
Hanya untuk kelengkapan: jika Anda menggunakan Spring ThreadPoolTaskExecutorbukan Java biasa ThreadPoolExecutor, Anda dapat menggunakan yang MdcTaskDecoratordijelaskan di moelholm.com/2017/07/24/…
Pino
27

Kami mengalami masalah serupa. Anda mungkin ingin memperpanjang ThreadPoolExecutor dan mengganti metode sebelum / sesudah Eksekusi untuk membuat panggilan MDC yang Anda butuhkan sebelum memulai / menghentikan utas baru.

Menandai
sumber
10
Metode beforeExecute(Thread, Runnable)dan afterExecute(Runnable, Throwable)mungkin membantu dalam kasus lain tetapi saya tidak yakin bagaimana ini akan bekerja untuk pengaturan MDC. Keduanya dieksekusi di bawah utas melahirkan. Ini berarti Anda harus bisa mendapatkan peta yang diperbarui dari utas utama sebelumnya beforeExecute.
Kenston Choi
Lebih baik mengatur MDC di filter, itu berarti ketika permintaan sedang diproses oleh logika bisnis, konteksnya tidak akan diperbarui. Saya tidak berpikir kita harus memperbarui MDC di mana-mana di seluruh aplikasi
dereck
15

IMHO solusi terbaik adalah:

  • menggunakan ThreadPoolTaskExecutor
  • terapkan sendiri TaskDecorator
  • Gunakan: executor.setTaskDecorator(new LoggingTaskDecorator());

Dekorator dapat terlihat seperti ini:

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

}
Tomáš Myšík
sumber
Maaf, tidak begitu yakin apa yang Anda maksud. PEMBARUAN: Saya rasa saya mengerti sekarang, akan meningkatkan jawaban saya.
Tomáš Myšík
6

Ini adalah bagaimana saya melakukannya dengan kumpulan dan pelaksana thread tetap:

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

Di bagian threading:

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

Mirip dengan solusi yang diposting sebelumnya, newTaskFormetode untuk Runnabledan Callabledapat ditimpa untuk membungkus argumen (lihat solusi yang diterima) saat membuat RunnableFuture.

Catatan: Akibatnya, executorService's submitmetode harus dipanggil bukan executemetode.

Untuk itu ScheduledThreadPoolExecutor, decorateTaskmetode akan ditimpa sebagai gantinya.

Kunci saya_
sumber
2

Jika Anda menghadapi masalah ini dalam lingkungan terkait kerangka kerja di mana Anda menjalankan tugas dengan menggunakan @Asyncanotasi, Anda dapat menghias tugas dengan menggunakan pendekatan TaskDecorator . Contoh cara melakukannya disediakan di sini: https://moelholm.com/blog/2017/07/24/spring-43-using-a-taskdecorator-to-copy-mdc-data-to-async-threads

Saya menghadapi masalah ini dan artikel di atas membantu saya mengatasinya sehingga saya membagikannya di sini.

Soner
sumber
0

Variasi lain yang mirip dengan jawaban yang ada di sini adalah untuk mengimplementasikan ExecutorServicedan memungkinkan delegasi diteruskan ke sana. Kemudian menggunakan obat generik, itu masih dapat mengekspos delegasi yang sebenarnya jika seseorang ingin mendapatkan beberapa statistik (selama tidak ada metode modifikasi lain yang digunakan).

Kode referensi:

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;
    }
}
Kenston Choi
sumber
-3

Saya bisa menyelesaikan ini dengan menggunakan pendekatan berikut

Di utas utama (Application.java, titik masuk aplikasi saya)

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

Dalam menjalankan metode kelas yang dipanggil oleh Executer

MDC.setContextMap(Application.mdcContextMap);
smishra
sumber