Làm thế nào để sử dụng MDC với nhóm luồng?


146

Trong phần mềm của chúng tôi, chúng tôi sử dụng rộng rãi MDC để theo dõi những thứ như ID phiên và tên người dùng cho các yêu cầu web. Điều này hoạt động tốt trong khi chạy trong chủ đề ban đầu. Tuy nhiên, có rất nhiều thứ cần được xử lý trong nền. Vì vậy, chúng tôi sử dụng java.concurrent.ThreadPoolExecutorjava.util.Timercác lớp cùng với một số dịch vụ thực thi async tự cuộn. Tất cả các dịch vụ này quản lý nhóm chủ đề riêng của họ.

Đây là những gì hướng dẫn của Logback nói về việc sử dụng MDC trong môi trường như vậy:

Một bản sao của bối cảnh chẩn đoán được ánh xạ không phải lúc nào cũng được kế thừa bởi các luồng công nhân từ luồng khởi tạo. Đây là trường hợp khi java.util.concản.Executors được sử dụng để quản lý luồng. Ví dụ, phương thức newCachedThreadPool tạo ra một ThreadPoolExecutor và giống như mã tổng hợp luồng khác, nó có logic tạo luồng phức tạp.

Trong các trường hợp như vậy, MDC.getCopyOfContextMap () được gọi trên luồng gốc (chủ) trước khi gửi một tác vụ cho người thi hành. Khi tác vụ chạy, như là hành động đầu tiên của nó, nó sẽ gọi MDC.setContextMapValues ​​() để liên kết bản sao được lưu trữ của các giá trị MDC ban đầu với luồng được quản lý Executor mới.

Điều này sẽ ổn, nhưng rất dễ quên việc thêm các cuộc gọi đó, và không có cách nào dễ dàng để nhận ra vấn đề cho đến khi quá muộn. Dấu hiệu duy nhất với Log4j là bạn bị thiếu thông tin MDC trong nhật ký và với Logback, bạn nhận được thông tin MDC cũ (vì luồng trong nhóm bước đi kế thừa MDC của nó từ tác vụ đầu tiên được chạy trên nó). Cả hai đều là vấn đề nghiêm trọng trong một hệ thống sản xuất.

Tôi không thấy tình hình của chúng tôi đặc biệt theo bất kỳ cách nào, nhưng tôi không thể tìm thấy nhiều về vấn đề này trên web. Rõ ràng, đây không phải là điều mà nhiều người gặp phải, vì vậy phải có cách để tránh nó. Chúng ta đang làm gì sai ở đây?


1
Nếu ứng dụng của bạn được triển khai trong môi trường JEE, bạn có thể sử dụng các bộ chặn java để đặt bối cảnh MDC trước khi gọi EJB.
Maxim Kirilov

2
Kể từ phiên bản logback 1.1.5, các giá trị MDC không còn được kế thừa bởi các luồng con.
Ceki 17/03/2016


2
@Ceki Tài liệu cần được cập nhật: "Một luồng con tự động kế thừa một bản sao của bối cảnh chẩn đoán được ánh xạ của cha mẹ của nó." logback.qos.ch/manual/mdc.html
steffen

Tôi đã tạo một yêu cầu kéo tới slf4j để giải quyết vấn đề sử dụng MDC trên các luồng (liên kết github.com/qos-ch/slf4j/pull/150 ). Có thể, nếu mọi người bình luận và yêu cầu, họ sẽ kết hợp thay đổi trong SLF4J :)
Nam

Câu trả lời:


79

Vâng, đây là một vấn đề phổ biến tôi cũng gặp phải. Có một vài cách giải quyết (như cài đặt thủ công, như được mô tả), nhưng lý tưởng nhất là bạn muốn có một giải pháp

  • Đặt MDC một cách nhất quán;
  • Tránh các lỗi ngầm trong đó MDC không chính xác nhưng bạn không biết điều đó; và
  • Giảm thiểu các thay đổi về cách bạn sử dụng nhóm luồng (ví dụ: phân lớp Callablevới MyCallablemọi nơi hoặc độ xấu tương tự).

Đây là một giải pháp mà tôi sử dụng đáp ứng ba nhu cầu này. Mã nên tự giải thích.

(Như một lưu ý phụ, người thực thi này có thể được tạo và cung cấp cho Guava MoreExecutors.listeningDecorator(), nếu bạn sử dụng 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);
                    }
                }
            }
        };
    }
}

Trong trường hợp bối cảnh trước đó không trống, không phải lúc nào nó cũng là rác sao? Tại sao bạn mang nó đi khắp nơi?
djjeck

2
Đúng; nó không nên được thiết lập Nó chỉ có vẻ như vệ sinh tốt, ví dụ nếu phương thức quấn () được phơi bày và sử dụng bởi người khác trên đường.
jlevy

Bạn có thể cung cấp một tài liệu tham khảo về cách MdcThreadPoolExecutor này được đính kèm hoặc tham chiếu bởi Log4J2 không? Có nơi nào mà chúng ta cần tham khảo cụ thể lớp này không, hay nó được "tự động hóa"? Tôi không sử dụng ổi. Tôi có thể, nhưng tôi muốn biết nếu có một số cách khác trước khi sử dụng nó.
jcb

Nếu tôi hiểu chính xác câu hỏi của bạn, câu trả lời là có, đó là các biến cục bộ của luồng "ma thuật" trong SLF4J - hãy xem cách triển khai MDC.setContextMap (), v.v. Ngoài ra, bằng cách này, sử dụng SLF4J, không phải Log4J, nên sử dụng vì nó hoạt động với Log4j, Logback và các thiết lập ghi nhật ký khác.
jlevy

1
Chỉ để hoàn thiện: nếu bạn đang sử dụng Spring ThreadPoolTaskExecutorthay vì Java đơn giản ThreadPoolExecutor, bạn có thể sử dụng MdcTaskDecoratormô tả tại moelholm.com/2017/07/24/ Kẻ
Pino

27

Chúng tôi đã gặp phải một vấn đề tương tự. Bạn có thể muốn mở rộng ThreadPoolExecutor và ghi đè các phương thức trước / afterExecute để thực hiện các cuộc gọi MDC bạn cần trước khi bắt đầu / dừng các luồng mới.


10
Các phương pháp beforeExecute(Thread, Runnable)afterExecute(Runnable, Throwable)có thể hữu ích trong các trường hợp khác nhưng tôi không chắc cách này sẽ hoạt động như thế nào để thiết lập MDC. Cả hai đều được thực hiện dưới luồng sinh sản. Điều này có nghĩa là bạn cần có khả năng giữ bản đồ được cập nhật từ luồng chính trước đó beforeExecute.
Kenston Choi

Tốt hơn là đặt MDC trong bộ lọc, điều đó có nghĩa là khi yêu cầu được xử lý theo logic nghiệp vụ, bối cảnh sẽ không được cập nhật. Tôi không nghĩ rằng chúng ta nên cập nhật MDC ở mọi nơi trên ứng dụng
bỏ qua

15

IMHO giải pháp tốt nhất là:

  • sử dụng ThreadPoolTaskExecutor
  • thực hiện của riêng bạn TaskDecorator
  • sử dụng nó: executor.setTaskDecorator(new LoggingTaskDecorator());

Trình trang trí có thể trông như thế này:

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

}

Xin lỗi, không thực sự chắc chắn những gì bạn có ý nghĩa. CẬP NHẬT: Tôi nghĩ rằng tôi thấy bây giờ, sẽ cải thiện câu trả lời của tôi.
Tomáš Myšík

6

Đây là cách tôi thực hiện với nhóm luồng cố định và người thực thi:

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

Trong phần luồng:

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

2

Tương tự như các giải pháp được đăng trước đó, các newTaskForphương thức cho RunnableCallablecó thể được ghi đè để bọc đối số (xem giải pháp được chấp nhận) khi tạo RunnableFuture.

Lưu ý: Do đó, executorService's submitphương pháp phải được gọi thay cho executephương pháp.

Đối với ScheduledThreadPoolExecutor, các decorateTaskphương thức sẽ được ghi đè thay thế.


2

Trong trường hợp bạn phải đối mặt với vấn đề này trong một môi trường liên quan đến khuôn khổ mùa xuân nơi bạn chạy các tác vụ bằng cách sử dụng @Asyncchú thích bạn có thể trang trí các nhiệm vụ bằng cách sử dụng các TaskDecorator cách tiếp cận. Một mẫu về cách thực hiện được cung cấp tại đây: https://moelholm.com/blog/2017/07/24/spring-43-USE-a-taskdecorator-to-copy-mdc-data-to-async-threads

Tôi đã đối mặt với vấn đề này và bài viết trên đây đã giúp tôi giải quyết nó vì vậy đó là lý do tại sao tôi chia sẻ nó ở đây.


0

Một biến thể khác tương tự như câu trả lời hiện có ở đây là triển khai ExecutorServicevà cho phép một đại biểu được chuyển đến nó. Sau đó, sử dụng generic, nó vẫn có thể hiển thị đại biểu thực tế trong trường hợp người ta muốn lấy một số thống kê (miễn là không sử dụng các phương pháp sửa đổi nào khác).

Mã tham chiếu:

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

Tôi đã có thể giải quyết điều này bằng cách sử dụng phương pháp sau đây

Trong luồng chính (Application.java, điểm vào của ứng dụng của tôi)

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

Trong phương thức chạy của lớp được gọi bởi Executer

MDC.setContextMap(Application.mdcContextMap);
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.