【MDC】微服务分布式日志追踪与多线程上下文传递

微服务分布式日志追踪与多线程上下文传递

    • 功能特性
    • 快速开始
      • 1. 依赖引入
      • 2. 核心配置
        • (1) MDC 上下文管理(TTL 增强)
        • (2) 拦截器注册
    • 微服务间调用
      • Feign 客户端配置
    • 多线程处理
      • 1. 异步任务(@Async)
      • 2. 线程池手动管理
    • 日志配置(logback-spring.xml)
    • 常见问题排查
    • 最佳实践

本项目演示如何在微服务架构中使用 MDC (Mapped Diagnostic Context) 实现分布式日志追踪,并解决异步线程中的上下文传递问题。


功能特性

  • 分布式链路追踪: 通过 trace_id 串联跨服务请求日志。
  • 多线程支持: 支持线程池、@Async 等异步场景下的上下文传递。
  • 集成 Feign: 自动透传 trace_id 到下游服务。
  • 性能优化: 使用 TransmittableThreadLocal 避免内存泄漏。

快速开始

1. 依赖引入


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>


<dependency>
    <groupId>com.alibabagroupId>
    <artifactId>transmittable-thread-localartifactId>
    <version>2.14.2version>
dependency>

2. 核心配置

(1) MDC 上下文管理(TTL 增强)
package org.example;

import org.slf4j.MDC;
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.HashMap;
import java.util.Map;


public class TtlMDCAdapter {

    private static final TransmittableThreadLocal<Map<String, String>> context = new TransmittableThreadLocal<Map<String, String>>() {
        // 复制父线程的上下文
        @Override
        public Map<String, String> copy(Map<String, String> parentValue) {
            return parentValue != null ? new HashMap<>(parentValue) : null;
        }

        // 子线程执行任务前:恢复 MDC 上下文
        @Override
        protected void beforeExecute() {
            super.beforeExecute();
            Map<String, String> map = get();
            if (map != null) {
                MDC.setContextMap(map); // 同步到 MDC
            }
        }

        // 子线程任务执行后:清理 MDC 上下文
        @Override
        protected void afterExecute() {
            super.afterExecute();
            MDC.clear();
        }
    };

    public static void put(String key, String value) {
        Map<String, String> map = context.get();
        if (map == null) {
            map = new HashMap<>();
            context.set(map);
        }
        map.put(key, value);
        MDC.put(key, value); // 主线程同步到 MDC
    }

    public static String get(String key) {
        Map<String, String> map = context.get();
        return map != null ? map.get(key) : null;
    }

    public static void clear() {
        context.remove();
        MDC.clear();
    }
}

(2) 拦截器注册
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoggingInterceptor()).addPathPatterns("/**");
    }
}

public class LoggingInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String traceId = Optional.ofNullable(request.getHeader("X-Trace-Id"))
                                .orElse(UUID.randomUUID().toString());
        TtlMDCAdapter.put("trace_id", traceId);
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        TtlMDCAdapter.clear();
    }
}

微服务间调用

Feign 客户端配置

public class FeignClientConfig {
    @Bean
    public RequestInterceptor requestInterceptor() {
        return template -> {
            String traceId = TtlMDCAdapter.get("trace_id");
            if (traceId != null) {
                template.header("X-Trace-Id", traceId);
            }
        };
    }
}

// Feign 客户端使用
@FeignClient(name = "user-service", configuration = FeignClientConfig.class)
public interface UserServiceClient {
    @GetMapping("/user")
    String getUserInfo();
}

多线程处理

1. 异步任务(@Async)

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setTaskDecorator(runnable -> 
            () -> {
                Map<String, String> context = TtlMDCAdapter.getCopyOfContextMap();
                TtlMDCAdapter.setContextMap(context);
                runnable.run();
                TtlMDCAdapter.clear();
            });
        return executor;
    }
}

@Service
public class OrderService {
    @Async
    public void asyncProcess() {
        log.info("Async task with trace_id"); // 将正确携带 trace_id
    }
}

2. 线程池手动管理

// 使用 TTL 包装的线程池
private static final ExecutorService executor = 
    TtlExecutors.getTtlExecutorService(Executors.newFixedThreadPool(10));

public void submitTask() {
    executor.submit(() -> {
        log.info("Thread pool task"); // trace_id 自动传递
    });
}

日志配置(logback-spring.xml)

<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] [trace_id=%X{trace_id}] %-5level %logger{36} - %msg%npattern>
        encoder>
    appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE" />
    root>
configuration>

常见问题排查

现象 原因 解决方案
日志中无 trace_id 拦截器未正确注册 检查 WebMvcConfig 配置
异步任务丢失上下文 未使用 TTL 线程池 TtlExecutors 包装线程池
Feign 调用未传递 ID 未配置 Feign 拦截器 检查 FeignClientConfig
内存泄漏 未调用 clear() 确保 finally 块中清理上下文

最佳实践

  1. 入口服务:生成初始 trace_id
  2. 下游服务:优先使用请求头中的 trace_id
  3. 线程池任务:始终用 TTL 包装
  4. 日志分析:通过 trace_id 在 ELK 或 Loki 中聚合日志

你可能感兴趣的:(微服务,分布式,java)