【Java项目技术亮点】日志链路追踪MDC与TraceId

写在前面

说实话,排查线上Bug最痛苦的不是问题本身,而是面对一台机器上成千上万条日志,根本分不清哪条是哪次请求的。我记得有次用户反馈"下单失败了",我查日志查到凌晨3点,眼睛都看花了才找到那条报错------就因为没有TraceId。后来我们全链路接入了TraceId,排查问题的效率提升了不止10倍。这篇文章把我实战中的完整方案,包括踩过的坑,全部整理出来。

文章目录


一、为什么需要日志链路追踪?

1.1 一个真实的排查噩梦

用户反馈:"我下单失败了,钱扣了没订单。"

你打开服务器,看到日志是这样的:

复制代码
2024-01-10 09:23:15 INFO  [http-nio-8080-exec-5] c.o.s.OrderService - 创建订单开始
2024-01-10 09:23:15 INFO  [http-nio-8080-exec-3] c.o.s.PayService   - 调用支付接口
2024-01-10 09:23:16 ERROR [http-nio-8080-exec-7] c.o.s.OrderService - 库存不足
2024-01-10 09:23:16 INFO  [http-nio-8080-exec-5] c.o.s.OrderService - 订单创建成功
2024-01-10 09:23:17 ERROR [http-nio-8080-exec-3] c.o.s.PayService   - 支付超时
2024-01-10 09:23:17 INFO  [http-nio-8080-exec-8] c.o.s.UserService  - 查询用户信息

你告诉我,哪几条日志属于同一个请求?

根本分不清。 如果有10台机器、每台上千条日志,排查一个Bug能花3小时。

1.2 生活类比:快递单号

想象你寄了个快递:

  • 没有单号:你只能打电话问"我的包裹到哪了",对方问你"什么包裹?谁的?"------完全没法查。
  • 有了单号:每个中转站扫描一次,你输入单号就能看到完整路径:"已揽收→到达A转运中心→到达B转运中心→派送中→已签收"。

TraceId就是这个"快递单号"。 一次请求生成一个唯一的TraceId,沿途每个服务、每个方法都把TraceId打在日志里,你就能完整串起这条请求的"一生"。

1.3 链路追踪的核心价值

价值 具体表现
排查问题效率提升10倍 按TraceId一秒定位所有相关日志
请求全链路可视化 网关→A服务→B服务→数据库,完整路径
性能瓶颈定位 看每个节点的耗时,慢在哪里一目了然
故障影响范围评估 一个下游故障影响了哪些上游请求

二、MDC原理

2.1 MDC是什么?

MDC(Mapped Diagnostic Context),翻译过来叫"映射诊断上下文"。

说白了,它就是一个线程级别的Map ,底层用的是ThreadLocal。你可以在当前线程里存一些键值对,日志框架在输出日志的时候自动把这些值取出来打到日志里。

java 复制代码
// 核心API就三个
MDC.put("traceId", "abc123");   // 放入值
String traceId = MDC.get("traceId");  // 取出值
MDC.clear();  // 清理当前线程的所有值

2.2 MDC与ThreadLocal的关系

复制代码
Thread对象
    └── ThreadLocalMap
            └── MDC的ThreadLocal
                    ├── traceId = abc123
                    ├── userId = 10086
                    └── requestUri = /api/order/create

关键点 :MDC的数据是和线程绑定的。线程A的MDC,线程B访问不到。这也带来一个问题------异步线程里MDC会丢失,后面会讲怎么解决。

2.3 日志框架集成

Logback通过%X{key}占位符来读取MDC中的值。

xml 复制代码
<!-- logback-spring.xml -->
<configuration>
    <!-- 控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <!-- %X{traceId} 就是读取MDC中traceId的值 -->
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%X{traceId}] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- 文件输出,JSON格式方便ELK解析 -->
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/var/log/app/application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/var/log/app/application.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LogstashEncoder">
            <!-- LogstashEncoder会自动把MDC的所有key打到json里 -->
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="FILE"/>
    </root>
</configuration>

2.4 日志输出效果

配置了TraceId后,日志变成了这样:

复制代码
2024-01-10 09:23:15.123 [http-nio-8080-exec-5] [a1b2c3d4e5f6] INFO  c.o.s.OrderService - 创建订单开始, userId=10086
2024-01-10 09:23:15.234 [http-nio-8080-exec-5] [a1b2c3d4e5f6] INFO  c.o.s.PayService   - 调用支付网关, amount=199.00
2024-01-10 09:23:15.456 [http-nio-8080-exec-5] [a1b2c3d4e5f6] INFO  c.o.s.OrderService - 订单创建成功, orderId=888888

看到没,同一个请求的三条日志,[a1b2c3d4e5f6]这个TraceId把她们串起来了!


三、TraceId生成与传递

3.1 网关层生成TraceId

TraceId最好在最外层生成,一般是网关(Spring Cloud Gateway)或者Nginx。这样整个链路从头到位都有一个统一的标识。

java 复制代码
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.UUID;

/**
 * Spring Cloud Gateway全局过滤器
 * 在所有请求的入口生成TraceId
 */
@Slf4j
@Component
public class TraceIdGatewayFilter implements GlobalFilter, Ordered {

    private static final String TRACE_ID_HEADER = "X-Trace-Id";
    private static final String TRACE_ID_MDC_KEY = "traceId";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String traceId = exchange.getRequest().getHeaders().getFirst(TRACE_ID_HEADER);
        
        // 如果请求头里没有traceId,就生成一个
        if (traceId == null || traceId.isEmpty()) {
            traceId = generateTraceId();
        }
        
        // 把traceId放入MDC(注意:WebFlux是异步的,需要用Reactor的Context传递)
        final String finalTraceId = traceId;
        
        ServerHttpRequest request = exchange.getRequest().mutate()
                .header(TRACE_ID_HEADER, finalTraceId)
                .build();
        
        return chain.filter(exchange.mutate().request(request).build())
                .doFinally(signalType -> MDC.remove(TRACE_ID_MDC_KEY));
    }

    /**
     * 生成TraceId,UUID vs Snowflake?后面踩坑篇细说
     */
    private String generateTraceId() {
        return UUID.randomUUID().toString().replace("-", "");
    }

    @Override
    public int getOrder() {
        // 最高优先级,确保最先执行
        return Ordered.HIGHEST_PRECEDENCE;
    }
}

3.2 TraceId在HTTP Header中透传

服务A调用服务B时,必须把TraceId放在请求头里带过去。如果你用RestTemplate:

java 复制代码
import org.slf4j.MDC;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * RestTemplate拦截器:把TraceId透传到下游服务
 */
@Component
public class TraceIdRestTemplateInterceptor implements ClientHttpRequestInterceptor {

    private static final String TRACE_ID_HEADER = "X-Trace-Id";

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                        ClientHttpRequestExecution execution) throws IOException {
        String traceId = MDC.get("traceId");
        
        if (traceId != null) {
            // 关键:把当前线程的traceId放入请求头
            HttpHeaders headers = request.getHeaders();
            headers.add(TRACE_ID_HEADER, traceId);
        }
        
        return execution.execute(request, body);
    }
}

3.3 Feign客户端透传TraceId

如果你用Feign调用下游服务:

java 复制代码
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.slf4j.MDC;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Feign全局配置:透传TraceId
 */
@Configuration
public class FeignTraceIdConfig {

    private static final String TRACE_ID_HEADER = "X-Trace-Id";

    @Bean
    public RequestInterceptor traceIdInterceptor() {
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate template) {
                String traceId = MDC.get("traceId");
                if (traceId != null) {
                    template.header(TRACE_ID_HEADER, traceId);
                }
            }
        };
    }
}

3.4 下游服务接收TraceId

下游服务(被调用方)需要把请求头里的TraceId放进自己的MDC:

java 复制代码
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * TraceId接收过滤器
 * 每个微服务都要配一个,从请求头中读取TraceId并放入MDC
 */
@Component
public class TraceIdReceiveFilter extends OncePerRequestFilter {

    private static final String TRACE_ID_HEADER = "X-Trace-Id";
    private static final String TRACE_ID_MDC_KEY = "traceId";

    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                    HttpServletResponse response, 
                                    FilterChain filterChain) throws ServletException, IOException {
        String traceId = request.getHeader(TRACE_ID_HEADER);
        
        if (traceId != null && !traceId.isEmpty()) {
            MDC.put(TRACE_ID_MDC_KEY, traceId);
        } else {
            // 兜底:如果没有上游传过来的TraceId,自己生成一个
            MDC.put(TRACE_ID_MDC_KEY, generateTraceId());
        }
        
        try {
            filterChain.doFilter(request, response);
        } finally {
            // 这个坑我踩过!一定要清理MDC,否则线程复用会导致traceId串了
            MDC.clear();
        }
    }
    
    private String generateTraceId() {
        return java.util.UUID.randomUUID().toString().replace("-", "");
    }
}

3.5 跨线程传递:线程池+MDC

这个坑我踩过!你把任务丢进线程池,MDC里的TraceId就丢了,因为线程池里的线程不是当前线程。

解决方案:包装Runnable/Callable,提交任务前把MDC复制过去,执行完再清理。

java 复制代码
import org.slf4j.MDC;

import java.util.Map;
import java.util.concurrent.*;

/**
 * MDC线程池包装器
 * 支持跨线程传递TraceId的线程池
 */
public class MdcThreadPoolExecutor extends ThreadPoolExecutor {

    public MdcThreadPoolExecutor(int corePoolSize, int maximumPoolSize,
                                  long keepAliveTime, TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue) {
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    public void execute(Runnable command) {
        // 提交任务时,把当前线程的MDC上下文复制一份
        Map<String, String> contextMap = MDC.getCopyOfContextMap();
        super.execute(new MdcRunnable(command, contextMap));
    }

    /**
     * MDC Runnable包装类
     */
    public static class MdcRunnable implements Runnable {
        
        private final Runnable delegate;
        private final Map<String, String> contextMap;

        public MdcRunnable(Runnable delegate, Map<String, String> contextMap) {
            this.delegate = delegate;
            this.contextMap = contextMap;
        }

        @Override
        public void run() {
            // 任务执行前,把MDC上下文设置到当前线程
            if (contextMap != null) {
                MDC.setContextMap(contextMap);
            }
            
            try {
                delegate.run();
            } finally {
                // 任务执行完,一定要清理!
                MDC.clear();
            }
        }
    }
}

使用方式:

java 复制代码
// 用MdcThreadPoolExecutor替代普通线程池
ExecutorService executor = new MdcThreadPoolExecutor(
    4, 8, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100)
);

// 提交任务,TraceId会自动传递
executor.submit(() -> {
    log.info("异步任务执行,traceId还在!");
    // 日志输出:... [a1b2c3d4e5f6] INFO ... - 异步任务执行,traceId还在!
});

四、TraceId集成日志与ELK

4.1 Logback完整配置

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!-- 彩色控制台输出 -->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%yellow(%d{yyyy-MM-dd HH:mm:ss.SSS}) %green([%thread]) %highlight([%X{traceId}]) %highlight(%-5level) %cyan(%logger{36}) - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- JSON格式文件输出,用于ELK收集 -->
    <appender name="JSON_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/var/log/app/app.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>/var/log/app/app.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxHistory>30</maxHistory>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
        </rollingPolicy>
        <encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
            <providers>
                <timestamp/>
                <logLevel/>
                <loggerName/>
                <message/>
                <mdc/>
                <threadName/>
                <stackTrace/>
            </providers>
        </encoder>
    </appender>

    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
        <appender-ref ref="JSON_FILE"/>
    </root>
</configuration>

4.2 ELK按TraceId搜索

JSON格式的日志打到Elasticsearch后,Kibana里可以直接搜:

复制代码
mdc.traceId: a1b2c3d4e5f6

一条搜索语句,就能把这个TraceId对应的所有服务的所有日志全找出来。这就是TraceId的魔力。

4.3 与SkyWalking/Pinpoint关联

如果你用了SkyWalking或Pinpoint,它们也有自己的TraceId。建议把SkyWalking的TraceId和MDC的TraceId打通:

java 复制代码
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * SkyWalking TraceId与MDC打通
 */
@Component
public class SkyWalkingTraceIdFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        // 获取SkyWalking的TraceId
        String swTraceId = TraceContext.traceId();
        
        // 如果SkyWalking有TraceId,优先用它(更规范)
        if (swTraceId != null && !"N/A".equals(swTraceId)) {
            MDC.put("traceId", swTraceId);
        } else {
            // 兜底:用请求头里的或自己生成
            String traceId = request.getHeader("X-Trace-Id");
            if (traceId == null) {
                traceId = generateTraceId();
            }
            MDC.put("traceId", traceId);
        }
        
        try {
            filterChain.doFilter(request, response);
        } finally {
            MDC.clear();
        }
    }
    
    private String generateTraceId() {
        return java.util.UUID.randomUUID().toString().replace("-", "");
    }
}

五、进阶:链路追踪信息扩展

5.1 MDC里除了TraceId还能放什么?

java 复制代码
// 在过滤器里一次性放入所有上下文信息
MDC.put("traceId", traceId);
MDC.put("userId", String.valueOf(userId));
MDC.put("requestUri", request.getRequestURI());
MDC.put("requestMethod", request.getMethod());
MDC.put("clientIp", getClientIp(request));

日志输出就会变成:

复制代码
2024-01-10 09:23:15.123 [http-nio-8080-exec-5] [a1b2c3d4] INFO  c.o.s.OrderService - 创建订单开始

同时JSON日志里会有完整的字段:

json 复制代码
{
  "timestamp": "2024-01-10T09:23:15.123+08:00",
  "level": "INFO",
  "logger": "c.o.s.OrderService",
  "message": "创建订单开始",
  "mdc": {
    "traceId": "a1b2c3d4",
    "userId": "10086",
    "requestUri": "/api/order/create",
    "requestMethod": "POST",
    "clientIp": "192.168.1.100"
  },
  "thread": "http-nio-8080-exec-5"
}

5.2 全链路SpanId

TraceId标识一次请求,SpanId标识请求内的某个具体调用。比如一个请求调用了3个下游服务,每个调用就是一个Span:

复制代码
Trace: a1b2c3d4
  ├─ Span 1.1: Gateway接收请求
  ├─ Span 1.2: OrderService处理
  │   ├─ Span 1.2.1: 查询用户信息 (50ms)
  │   ├─ Span 1.2.2: 扣减库存 (100ms)
  │   └─ Span 1.2.3: 调用支付服务 (300ms)  ← 瓶颈在这里!
  └─ Span 1.3: Gateway返回响应

SkyWalking会自动生成SpanId。如果你只用MDC做简单日志追踪,一般TraceId就够了。

5.3 日志采样策略

全量采集日志压力太大,可以按策略采样:

采样策略 适用场景
100%采样 核心业务链路、排查期
按比例采样(如10%) 日常流量大的接口
错误全采+成功按比例 平衡成本和问题发现
按用户白名单 VIP用户全链路追踪

六、踩坑指南

坑1:MDC的ThreadLocal在异步线程中丢失

这个坑我踩过!主线程MDC里有traceId,一提交到线程池就没了。必须用我上面写的MdcThreadPoolExecutor包装一下,或者手动MDC.getCopyOfContextMap() + MDC.setContextMap()
坑2:TraceId在MQ消费端的传递

MQ消费是一个新线程(或者线程池里的线程),消息头里如果没有带TraceId,消费端就断了链路。解决方案:生产者在消息属性里塞入TraceId,消费者收到后取出来放到MDC。

java 复制代码
// 生产者
Message msg = new Message("topic", "tag", "body".getBytes());
msg.putUserProperty("traceId", MDC.get("traceId"));

// 消费者
String traceId = msg.getUserProperty("traceId");
MDC.put("traceId", traceId);

坑3:线程池复用导致MDC未清理

这个坑最隐蔽!如果finally里没调MDC.clear(),线程池里的线程下次被复用时,MDC里还残留着上次的traceId,导致日志串了。我见过有人排查Bug,发现日志里一个请求有两套traceId,就是因为这个。
坑4:TraceId长度与性能

UUID是36位(带横线),去掉横线32位。每行日志都带32个字符,日志量大了对存储和传输都有影响。追求性能可以用Snowflake生成18位的Long型ID,或者自己实现更短的随机字符串。不过说实话,32位UUID在现代硬件上影响微乎其微,简单稳定优先。


七、问题与解答

Q1:MDC和普通的ThreadLocal有什么区别?

MDC底层就是ThreadLocal,但它封装了一层标准API,并且和日志框架(Logback/Log4j2/Log4j)深度集成,可以通过%X{key}占位符直接输出到日志。你当然可以直接用ThreadLocal存traceId,但日志框架不认识你的ThreadLocal,MDC是行业标准方案。

Q2:WebFlux响应式编程中MDC还能用吗?

WebFlux的线程模型和传统Servlet不一样,一个请求可能在不同线程上执行。这种情况下MDC的ThreadLocal机制会失效。解决方案是用Reactor的Context来传递,或者引入context-propagation库。这是响应式编程的一个痛点,如果团队对WebFlux不熟,建议传统MVC项目先用MDC方案。

Q3:如果下游系统是老系统,不支持接收TraceId怎么办?

这是个现实问题。有几种应对:

  1. 推动下游改造,加个Filter读请求头很简单
  2. 在接口文档里约定X-Trace-Id透传
  3. 实在透传不了的,至少在本系统内部打好TraceId,排查时能缩小范围
  4. 通过日志时间戳+请求参数辅助定位(效率低但比没有强)

八、面试高频考点汇总

考点1:MDC是什么?底层原理是什么?

MDC(Mapped Diagnostic Context)是日志框架提供的线程级诊断上下文工具,底层基于ThreadLocal实现。通过MDC.put(key, value)在当前线程存储键值对,日志框架通过%X{key}占位符输出。典型应用是存储TraceId实现全链路日志追踪。

考点2:TraceId如何在微服务之间传递?

  1. 网关层生成 TraceId,放入HTTP请求头(如X-Trace-Id
  2. 调用方通过RestTemplate拦截器或Feign拦截器,从MDC读取TraceId放入请求头
  3. 被调用方通过Filter从请求头读取TraceId,放入自己的MDC
  4. 跨线程场景需要用包装类(如MdcRunnable)复制MDC上下文

考点3:线程池里TraceId丢失怎么解决?

因为MDC基于ThreadLocal,线程池的线程和提交任务的线程不是同一个。解决方案是包装Runnable/Callable:

java 复制代码
public class MdcRunnable implements Runnable {
    private final Runnable delegate;
    private final Map<String, String> contextMap;
    
    @Override
    public void run() {
        MDC.setContextMap(contextMap);
        try {
            delegate.run();
        } finally {
            MDC.clear();
        }
    }
}

考点4:为什么每次请求结束后要调用MDC.clear()?

Tomcat等Web容器使用线程池处理请求,线程会被复用。如果不清除MDC,下一次请求复用这个线程时,MDC里还残留着上一次的数据,导致traceId串了、日志混乱。

考点5:TraceId和SkyWalking的TraceId是什么关系?

两者都是用于链路追踪的标识,但实现层级不同:

  • MDC TraceId:日志层面的手动/半自动方案,轻量、简单、成本低
  • SkyWalking TraceId:APM层面的全自动方案,功能强大但引入额外组件

实际项目中两者可以共存,甚至把SkyWalking的TraceId同步到MDC中,实现日志和APM的打通。


九、模拟面试官提问

场景题1:你们项目的日志链路追踪是怎么做的?

参考答案:

我们用的是MDC + TraceId的方案。具体流程:

  1. Spring Cloud Gateway层通过GlobalFilter生成TraceId,放入HTTP Header
  2. 每个微服务通过OncePerRequestFilter读取Header里的TraceId,放入MDC
  3. RestTemplate和Feign都配置了拦截器,自动把MDC里的TraceId透传到下游
  4. 线程池用自定义的MdcThreadPoolExecutor,保证异步任务里TraceId不丢
  5. 日志输出JSON格式,通过Filebeat收集到ELK,Kibana里按traceId搜索
  6. 同时也集成了SkyWalking,把SkyWalking的TraceId同步到MDC里

这套方案跑下来,排查问题的效率比之前提升了至少10倍。

场景题2:用户反馈"下单失败",你怎么快速定位问题?

参考答案:

  1. 先问用户要订单号或者操作时间
  2. 在Kibana里搜:mdc.userId:10086 AND message:"下单",找到对应的traceId
  3. 用traceId搜全链路日志:mdc.traceId:a1b2c3d4
  4. 按时间顺序看日志,定位到哪一步报错:是参数校验失败?库存不足?支付超时?还是下游服务异常?
  5. 如果是下游服务问题,继续用traceId去下游服务的日志里查
  6. 整个过程基本在5分钟内能定位到根因

场景题3:如果异步线程里的任务报错了,怎么关联到原始请求?

参考答案:

这就体现了MdcThreadPoolExecutor的价值。提交异步任务时,主线程的MDC上下文会被复制到任务线程里。所以异步任务里的日志也会带原始traceId。排查时直接用同一个traceId搜索就行,主线程和异步任务的日志能完整串起来。

场景题4:设计一个高并发场景下的日志追踪方案,要求对性能影响最小。

参考答案:

  1. TraceId生成:用Snowflake替代UUID,减少长度和生成开销
  2. 日志采样:非核心接口按10%比例采样,错误日志100%采集
  3. 异步日志 :Logback配置<appender>为异步模式,避免日志IO阻塞业务线程
  4. 避免过度扩展MDC:MDC里只放最关键的字段(traceId、userId),不要什么都塞
  5. 批量收集:Filebeat/Fluentd批量发送日志到ES,减少网络开销
  6. 日志级别动态调整:生产环境默认WARN/ERROR,排查时临时开启DEBUG

场景题5:MQ消费端怎么保证TraceId不丢?

参考答案:

  1. 生产者在发送消息时,把traceId放到消息的属性(UserProperty)里
  2. 消费者监听消息后,先从消息属性里取出traceId
  3. 把traceId放入当前消费线程的MDC
  4. 执行业务逻辑,日志自动带上traceId
  5. 消费完成后,finally里清理MDC
  6. 如果消费端还要调用下游服务,通过Feign/RestTemplate拦截器继续透传

关键是MQ作为异步中间件,要主动承担起TraceId的"接力"工作。


十、互动话题

你们项目现在是怎么排查线上问题的?是手动一台台机器搜grep,还是已经接入了TraceId或者SkyWalking?如果让你从零搭建一套日志链路追踪方案,你觉得最大的阻力会是什么?欢迎在评论区交流。


十一、参考资料

  1. Logback官方文档 - MDC
  2. Spring Cloud Sleuth官方文档 - 分布式追踪

如果这篇文章对你有帮助,欢迎点赞收藏!关注我,持续输出Java后端实战经验。