จะใช้ MDC กับเธรดพูลได้อย่างไร


146

ในซอฟต์แวร์ของเราเราใช้ MDC อย่างกว้างขวางเพื่อติดตามสิ่งต่างๆเช่นรหัสเซสชันและชื่อผู้ใช้สำหรับคำขอเว็บ ทำงานได้ดีในขณะที่ทำงานในเธรดดั้งเดิม อย่างไรก็ตามมีหลายสิ่งหลายอย่างที่ต้องดำเนินการในพื้นหลัง เพื่อที่เราจะใช้java.concurrent.ThreadPoolExecutorและjava.util.Timerคลาสพร้อมกับบริการดำเนินการ async ที่ผ่านการม้วนตัวเอง บริการทั้งหมดเหล่านี้จัดการเธรดพูลของตนเอง

นี่คือสิ่งที่คู่มือ Logbackได้พูดเกี่ยวกับการใช้ MDC ในสภาพแวดล้อมดังกล่าว:

สำเนาของบริบทการวินิจฉัยที่แมปไม่สามารถสืบทอดโดยเธรดผู้ปฏิบัติงานจากเธรดเริ่มต้นได้เสมอไป นี่เป็นกรณีที่เมื่อใช้ java.util.concurrent.Executors สำหรับการจัดการเธรด ตัวอย่างเช่นเมธอด newCachedThreadPool สร้าง ThreadPoolExecutor และเช่นเดียวกับรหัสการรวมเธรดอื่น ๆ มันมีตรรกะการสร้างเธรดที่สลับซับซ้อน

ในกรณีเช่นนี้ขอแนะนำให้เรียกใช้ MDC.getCopyOfContextMap () บนเธรด (ต้นแบบ) ต้นฉบับก่อนส่งงานไปยังผู้ปฏิบัติการ เมื่องานรันตามการดำเนินการครั้งแรกควรเรียกใช้ MDC.setContextMapValues ​​() เพื่อเชื่อมโยงสำเนาที่เก็บไว้ของค่า MDC ดั้งเดิมกับเธรดที่ได้รับการจัดการ Executor ใหม่

สิ่งนี้จะใช้ได้ แต่เป็นการง่ายมากที่จะลืมการเพิ่มสายเหล่านั้นและไม่มีวิธีที่ง่ายในการจดจำปัญหาจนกว่าจะสายเกินไป เครื่องหมายเดียวกับ Log4j คือคุณได้รับข้อมูล MDC ที่ขาดหายไปในบันทึกและด้วย Logback คุณจะได้รับข้อมูล MDC ที่เก่า (เนื่องจากเธรดในพูลของดอกยางนั้นได้รับ MDC จากงานแรกที่รันอยู่) ทั้งสองเป็นปัญหาร้ายแรงในระบบการผลิต

ฉันไม่เห็นสถานการณ์ของเราเป็นพิเศษ แต่อย่างใด แต่ฉันก็ไม่สามารถหาข้อมูลเกี่ยวกับปัญหานี้ได้จากเว็บ เห็นได้ชัดว่านี่ไม่ใช่สิ่งที่คนจำนวนมากชนดังนั้นจึงต้องมีวิธีหลีกเลี่ยง เราทำอะไรผิดที่นี่


1
หากแอ็พพลิเคชันของคุณถูกปรับใช้ภายใต้สภาพแวดล้อม JEE คุณสามารถใช้ตัวดักจับจาวาเพื่อตั้งค่าบริบท MDC ก่อนที่ EJB จะเรียกใช้
Maxim Kirilov

2
ในฐานะของ logback เวอร์ชัน 1.1.5 ค่า MDC จะไม่สืบทอดโดยเธรดลูกอีกต่อไป
Ceki

jira.qos.ch/browse/LOGBACK-422แก้ไขแล้ว
lyjackal

2
@Ceki เอกสารจำเป็นต้องได้รับการอัปเดต: "เธรดย่อยสืบทอดสำเนาของบริบทการวิเคราะห์ที่แม็พของพาเรนต์โดยอัตโนมัติ" logback.qos.ch/manual/mdc.html
steffen

ฉันสร้างคำขอดึงเพื่อ slf4j ที่แก้ปัญหาการใช้ MDC ในหลาย ๆ กระทู้ (ลิงค์github.com/qos-ch/slf4j/pull/150 ) อาจเป็นถ้ามีคนแสดงความคิดเห็นและขอให้พวกเขาจะรวมการเปลี่ยนแปลงใน SLF4J :)
ชาย

คำตอบ:


79

ใช่นี่เป็นปัญหาทั่วไปที่ฉันพบเช่นกัน มีวิธีแก้ไขปัญหาเล็กน้อย (เช่นการตั้งค่าด้วยตนเองดังอธิบาย) แต่เป็นการดีที่คุณต้องการโซลูชันที่

  • กำหนด MDC อย่างสม่ำเสมอ
  • หลีกเลี่ยงข้อบกพร่องโดยปริยายที่ MDC ไม่ถูกต้อง แต่คุณไม่รู้ และ
  • ลดการเปลี่ยนแปลงวิธีการใช้เธรดพูล (เช่นการแบ่งคลาสย่อยCallableด้วยMyCallableทุกที่หรือความอัปลักษณ์ที่คล้ายคลึงกัน)

นี่เป็นวิธีแก้ปัญหาที่ฉันใช้ซึ่งตรงกับความต้องการทั้งสามนี้ รหัสควรอธิบายตนเอง

(ในฐานะที่เป็นข้อความด้านข้างตัวจัดการนี้สามารถสร้างและป้อนให้กับ Guava ได้MoreExecutors.listeningDecorator()หากคุณใช้ 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);
                    }
                }
            }
        };
    }
}

ในกรณีที่บริบทก่อนหน้าไม่ว่างเปล่ามันจะเป็นขยะหรือไม่ ทำไมคุณพกติดตัวไปไหนมาไหน?
djjeck

2
ขวา; ไม่ควรตั้งค่า ดูเหมือนว่าสุขอนามัยที่ดีเช่นถ้ามีการเปิดเผยและใช้วิธีการห่อ () โดยคนอื่น ๆ ตามถนน
jlevy

คุณสามารถให้ข้อมูลอ้างอิงว่า MdcThreadPoolExecutor นี้เชื่อมต่อหรืออ้างอิงโดย Log4J2 ได้อย่างไร มีบางที่ที่เราต้องการอ้างอิงชั้นนี้โดยเฉพาะหรือทำ "automagically" หรือไม่? ฉันไม่ได้ใช้ Guava ทำได้ แต่ฉันต้องการทราบว่ามีวิธีอื่นก่อนที่จะใช้หรือไม่
jcb

ถ้าฉันเข้าใจคำถามของคุณถูกต้องคำตอบคือใช่มันคือตัวแปรเวทมนต์แบบ "local" ใน SLF4J - ดูการใช้งานของ MDC.setContextMap () ฯลฯ นอกจากนี้ยังใช้ SLF4J ไม่ใช่ Log4J ซึ่งเป็นที่นิยมมากกว่า มันทำงานได้กับ Log4j, Logback และการตั้งค่าการบันทึกอื่น ๆ
jlevy

1
เพียงเพื่อความสมบูรณ์: ถ้าคุณกำลังใช้ฤดูใบไม้ผลิThreadPoolTaskExecutorแทนธรรมดา Java ThreadPoolExecutor, คุณสามารถใช้MdcTaskDecoratorอธิบายไว้ที่moelholm.com/2017/07/24/...
Pino

27

เราพบปัญหาที่คล้ายกัน คุณอาจต้องการขยาย ThreadPoolExecutor และแทนที่เมธอด before / afterExecute เพื่อทำการเรียก MDC ที่คุณต้องการก่อนเริ่ม / หยุดเธรดใหม่


10
วิธีการbeforeExecute(Thread, Runnable)และafterExecute(Runnable, Throwable)อาจเป็นประโยชน์ในกรณีอื่น ๆ แต่ฉันไม่แน่ใจว่าจะใช้งานได้อย่างไรสำหรับการตั้งค่า MDC พวกเขาทั้งสองดำเนินการภายใต้ด้ายวางไข่ beforeExecuteซึ่งหมายความว่าคุณจะต้องสามารถที่จะได้รับถือของแผนที่ที่ปรับปรุงจากหัวข้อหลักก่อน
Kenston Choi

ดีกว่าที่จะตั้ง MDCs ในตัวกรองซึ่งหมายความว่าเมื่อคำขออยู่ภายใต้การประมวลผลโดยตรรกะทางธุรกิจบริบทจะไม่ได้รับการปรับปรุง ฉันไม่คิดว่าเราควรอัปเดต MDC ทุกที่ในแอปพลิเคชัน
dereck

15

IMHO ทางออกที่ดีที่สุดคือ:

  • ใช้ 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();
            }
        };
    }

}

ขออภัยไม่แน่ใจจริงๆว่าคุณหมายถึงอะไร ปรับปรุง: ฉันคิดว่าฉันเห็นตอนนี้จะปรับปรุงคำตอบของฉัน
TomášMyšík

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

ในวิธีการเรียกใช้ของคลาสที่ถูกเรียกโดย Executer

MDC.setContextMap(Application.mdcContextMap);
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.