Diş havuzları ile MDC nasıl kullanılır?


146

Yazılımımızda, web istekleri için oturum kimlikleri ve kullanıcı adları gibi şeyleri izlemek için MDC'yi yaygın olarak kullanıyoruz. Bu, orijinal iş parçacığında çalışırken iyi çalışır. Ancak, arka planda işlenmesi gereken birçok şey var. Bunun için java.concurrent.ThreadPoolExecutorve java.util.Timersınıflarını, bazı kendi kendine yuvarlanan zaman uyumsuz yürütme hizmetleriyle birlikte kullanıyoruz. Tüm bu hizmetler kendi iş parçacığı havuzunu yönetir.

Bu nedir Logback kılavuzu böyle bir ortamda MDC kullanma hakkında şöyle demektedir:

Eşlenen tanı bağlamının bir kopyası her zaman başlangıç ​​iş parçacığından çalışan iş parçacıkları tarafından devralınamaz. İplik yönetimi için java.util.concurrent.Executors kullanıldığında durum budur. Örneğin, newCachedThreadPool yöntemi bir ThreadPoolExecutor oluşturur ve diğer iş parçacığı havuzlama kodu gibi, karmaşık iş parçacığı oluşturma mantığı vardır.

Bu gibi durumlarda, MDC.getCopyOfContextMap () yönteminin yürütücüye bir görev göndermeden önce orijinal (ana) iş parçacığında çağrılması önerilir. Görev ilk eylemi olarak çalıştığında, özgün MDC değerlerinin depolanan kopyasını yeni Executor yönetilen iş parçacığıyla ilişkilendirmek için MDC.setContextMapValues ​​() öğesini çağırmalıdır.

Bu iyi olurdu, ancak bu aramaları eklemeyi unutmak çok kolaydır ve sorunu çok geç olana kadar tanımanın kolay bir yolu yoktur. Log4j ile tek işaret, günlüklerde eksik MDC bilgilerini almanız ve Logback ile eski MDC bilgilerini almanızdır (sırt havuzundaki iplik MDC'yi üzerinde çalıştırılan ilk görevden devralır). Her ikisi de bir üretim sisteminde ciddi sorunlardır.

Durumumuzu hiçbir şekilde özel görmüyorum, ancak web üzerinde bu sorun hakkında fazla bir şey bulamadım. Görünüşe göre, bu birçok insanın çarptığı bir şey değil, bu yüzden bundan kaçınmanın bir yolu olmalı. Burada neyi yanlış yapıyoruz?


1
Uygulamanız JEE ortamı altında dağıtılırsa, EJB çağrılmadan önce MDC bağlamını ayarlamak için java önleyicilerini kullanabilirsiniz.
Maxim Kirilov

2
Logback sürüm 1.1.5'ten itibaren, MDC değerleri artık alt iş parçacıkları tarafından miras alınmaz.
Ceki


2
@Ceki Belgelerin güncellenmesi gerekiyor: "Bir alt iş parçacığı, üst öğesinin eşlenmiş tanı bağlamının bir kopyasını otomatik olarak devralır." logback.qos.ch/manual/mdc.html
steffen

İş parçacıkları arasında MDC kullanma sorununu çözen slf4j için bir çekme isteği oluşturdum (bağlantı github.com/qos-ch/slf4j/pull/150 ). İnsanlar yorum yapar ve sorarlarsa, SLF4J'deki değişikliği dahil edeceklerdir :)
Erkek

Yanıtlar:


79

Evet, bu da karşılaştığım yaygın bir sorundur. Birkaç geçici çözüm vardır (açıklandığı gibi manuel olarak ayarlamak gibi), ancak ideal olarak,

  • MDC'yi tutarlı bir şekilde ayarlar;
  • MDC'nin yanlış olduğu zımni hataları önler, ancak bilmezsiniz; ve
  • İş parçacığı havuzlarını kullanma şeklinizdeki değişiklikleri en aza indirir (örneğin Callable, MyCallableher yerde alt sınıflandırma veya benzer çirkinlik).

İşte bu üç ihtiyacı karşılayan bir çözüm. Kod kendi kendini açıklayıcı olmalıdır.

(Bir yan not olarak, bu infaz oluşturulabilir ve Guava en beslenen MoreExecutors.listeningDecorator()Eğer Guava en kullanırsanız, 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);
                    }
                }
            }
        };
    }
}

Önceki bağlam boş değilse, her zaman çöp değil mi? Neden etrafta taşıyorsun?
djjeck

2
Sağ; ayarlanmamalıdır. Tıpkı iyi bir hijyen gibi görünüyor, örneğin wrap () yöntemi yolda başka biri tarafından ortaya çıkarılmış ve kullanılmışsa.
jlevy

Bu MdcThreadPoolExecutor'un Log4J2 tarafından nasıl eklendiğine veya referans verildiğine ilişkin bir referans verebilir misiniz? Bu sınıfa özel olarak başvurmamız gereken bir yer var mı, yoksa “otomajik olarak” yapılıyor mu? Guava kullanmıyorum. Yapabilirdim, ama kullanmadan önce başka bir yol olup olmadığını bilmek istiyorum.
jcb

Sorunuzu doğru anlarsam, cevap evettir, SLF4J'deki "sihirli" iş parçacığı yerel değişkenleridir - MDC.setContextMap () vb. Uygulamalarına bakın. Bu arada, bu tercih edilen Log4J yerine SLF4J kullanır. Log4j, Logback ve diğer günlük oluşturma kurulumlarıyla çalışır.
jlevy

1
Tamlık için: ThreadPoolTaskExecutorDüz Java yerine Spring kullanıyorsanız , moelholm.com/2017/07/24/…ThreadPoolExecutorMdcTaskDecorator
Pino

27

Benzer bir sorunla karşılaştık. Yeni iş parçacıklarını başlatmadan / durdurmadan önce ihtiyacınız olan MDC çağrılarını yapmak için ThreadPoolExecutor öğesini genişletmek ve önce / sonraExecute yöntemlerini geçersiz kılmak isteyebilirsiniz.


10
Yöntemler beforeExecute(Thread, Runnable)ve afterExecute(Runnable, Throwable)diğer durumlarda yardımcı olabilir, ancak bunun MDC'leri ayarlamak için nasıl çalışacağından emin değilim. Her ikisi de spawned thread altında yürütülür. Bu, daha önce güncellenmiş haritayı ana iş parçasından alabilmeniz gerektiği anlamına gelir beforeExecute.
Kenston Choi

Filtrede MDC'leri ayarlamak daha iyidir, yani istek iş mantığı tarafından işlenirken, içerik güncellenmez.
MDC'yi

15

IMHO için en iyi çözüm:

  • kullanım ThreadPoolTaskExecutor
  • kendininkini uygula TaskDecorator
  • kullanın: executor.setTaskDecorator(new LoggingTaskDecorator());

Dekoratör şöyle görünebilir:

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

}

Üzgünüm, ne demek istediğinden emin değilim. GÜNCELLEME: Sanırım şimdi görüyorum, cevabımı geliştireceğim.
Tomáš Myšík

6

Ben sabit iplik havuzları ve uygulayıcıları ile nasıl yaparım:

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

Diş çekme kısmında:

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

2

Daha önce yayınlanan çözümlere benzer şekilde, argümanı oluştururken (kabul edilen çözüme bakın) için newTaskForyöntemler kullanılabilir Runnableve Callableüzerine yazılabilir RunnableFuture.

Not: Sonuç olarak, yöntem yerine executorService' submityöntemi çağrılmalıdır execute.

İçin ScheduledThreadPoolExecutor, decorateTaskyöntemler yerine üzerine yazılır olacaktır.


2

Bu sorunu, @Asyncaçıklamaları kullanarak görevleri yürüttüğünüz bir yay çerçevesiyle ilgili bir ortamda karşınıza çıkarırsanız , Görevleri TaskDecorator yaklaşımını kullanarak süsleyebilirsiniz . Bunun nasıl yapılacağına dair bir örnek burada sunulmaktadır: https://moelholm.com/blog/2017/07/24/spring-43-using-a-taskdecorator-to-copy-mdc-data-to-async-threads

Bu sorunla karşılaştım ve yukarıdaki makale bununla başa çıkmama yardımcı oldu, bu yüzden burada paylaşıyorum.


0

Buradaki mevcut cevaplara benzer bir başka varyasyon da ExecutorServicebir temsilcinin kendisine iletilmesine izin vermektir. Daha sonra jenerikler kullanılarak, bazı istatistikler elde etmek istemesi durumunda gerçek delege ortaya çıkabilir (başka hiçbir değişiklik yöntemi kullanılmadığı sürece).

Referans Kodu:

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

Bunu aşağıdaki yaklaşımı kullanarak çözebildim

Ana başlıkta (Application.java, uygulamamın giriş noktası)

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

Executer tarafından çağrılan sınıfın run yönteminde

MDC.setContextMap(Application.mdcContextMap);
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.