Spring Cloud架构下的日志追踪:传统MDC vs 王炸SkyWalking

今天给大家分享一个在微服务架构中非常重要的技术------日志追踪。

为什么我们需要日志追踪?

想象一下,在一个大型的微服务架构中,一次用户的请求可能会经过网关、用户服务、订单服务、支付服务等多个微服务。当出现问题时,如果没有日志追踪,我们看到的就是一堆杂乱无章的日志,根本不知道哪个请求对应哪些日志,排查问题简直比登天还难!

这就是我们引入日志追踪的根本原因:让每一次请求都有迹可循,让问题定位变得简单高效!

优秀的日志追踪,我们要解决哪些问题?

  • Servlet请求: 单服务内,串联request到response之间主线程的所有日志
  • 微服务: RPC调用传递traceId至下游服务,下游服务同样完成Servlet请求
  • 消息队列: 生产者传递traceId至消费者
  • 定时任务: 类似Servlet请求,本质上是单独的触发点
  • 异步线程池: 并发编程中父线程传递traceId给子线程

要解决以上这些问题,我有两种全局性方案 分享给大家(文末附代码链接

  1. 传统MDC(Mapped Diagnostic Context)方案
  2. 王炸级方案SkyWalking

传统MDC方案

1. Servlet请求处理:过滤器从请求头获取traceId(自生成或来自上游服务)

scala 复制代码
@Component
@Order(CommonConstants.FilterOrdered.TRACE)
public class TraceFilter extends OncePerRequestFilter {
    @Autowired
    private TraceManager traceManager;
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String traceId = request.getHeader(HeaderConstants.TRACE_ID);
        traceManager.put(traceId);
        try {
            chain.doFilter(request, response);
        } finally {
            traceManager.remove();
        }
    }
}

2. 微服务间的RPC调用

  • Feign:通过拦截器传递traceId
typescript 复制代码
@Component
public class TraceHeaderInterceptor implements RequestInterceptor {
    @Autowired
    private TraceManager traceManager;
    @Override
    public void apply(RequestTemplate template) {
        traceManager.headers().forEach((template::header));
    }
}
  • RestTemplate:通过拦截器传递traceId
java 复制代码
1.实现拦截器
public class TraceHeaderClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    private final TraceManager traceManager;
    public TraceHeaderClientHttpRequestInterceptor(TraceManager traceManager) {
        this.traceManager = traceManager;
    }

    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
        throws IOException {
        Map<String, List<String>> headers = Maps.newHashMap();
        // 日志追踪ID
        headers.putAll(traceManager.headers());
        HttpHeaders httpHeaders = request.getHeaders();
        if (!headers.isEmpty()) {
            httpHeaders.putAll(headers);
        }
        return execution.execute(request, body);
    }
}

2.装配拦截器
@LoadBalanced
@Bean("restTemplate")
public RestTemplate restTemplate(TraceManager traceManager) {
    RestTemplate restTemplate = new RestTemplate(httpRequestFactory());
    List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
    // 将拦截器添加到restTemplate
    interceptors.add(new TraceHeaderClientHttpRequestInterceptor(traceManager));
    restTemplate.setInterceptors(interceptors);
    return restTemplate;
}

3. 消息队列:通过切面编程实现

typescript 复制代码
1.生产者将traceId放入header,传给消费者
public class RocketmqMessageSender implements MessageSender {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @Autowired
    private TraceManager traceManager;
    private void sendMessage(Object toJson, String topic, String tag) {
        String paramsStr = JsonUtil.toJsonString(toJson);
        Map<String, Object> headers = Maps.newHashMap();
        // 生产者将traceId放入header
        headers.put(HeaderConstants.HEADER_MESSAGE_ID, traceManager.get());
        MessageHeaders messageHeaders = new MessageHeaders(headers);
        Message<String> message = MessageBuilder.createMessage(paramsStr, messageHeaders);
        // 发送消息...
    }
}

2.消费者取出traceId并放入消费者的上下文
public class TraceAspect {
    @Autowired
    private TraceManager traceManager;
    // 执行之前塞入日志ID,用于追踪整个执行链路
    @Before("execution(* org.apache.rocketmq.spring.core.RocketMQListener+.onMessage(org.apache.rocketmq.common.message.MessageExt)) && @within(org.apache.rocketmq.spring.annotation.RocketMQMessageListener)")
    public void setTraceId(JoinPoint joinPoint) {
        Object[] args = joinPoint.getArgs();// 获取方法参数
        MessageExt messageExt = (MessageExt) args[0];
        Map<String, String> properties = messageExt.getProperties();
        traceManager.put(properties.get(HeaderConstants.HEADER_MESSAGE_ID));
        log.info("初始化日志ID");
    }
    // 执行完毕之后清理日志ID
    @After("execution(* org.apache.rocketmq.spring.core.RocketMQListener+.onMessage(..)) && @within(org.apache.rocketmq.spring.annotation.RocketMQMessageListener)")
    public void clearTraceId() {
        log.info("清除日志ID");
        traceManager.remove();
    }
}

4. 定时任务:通过切面编程实现

java 复制代码
public class TraceAspect {
    @Autowired
    private TraceManager traceManager;
    // 在jobHander执行之前塞入日志ID,用于追踪整个执行链路
    @Before("@annotation(com.xxl.job.core.handler.annotation.XxlJob)")
    public void setTraceId() {
        traceManager.put();
        log.info("初始化日志ID");
    }
    // 执行完毕之后清理日志ID
    @After("@annotation(com.xxl.job.core.handler.annotation.XxlJob)")
    public void clearTraceId() {
        log.info("清除日志ID");
        traceManager.remove();
    }
}

5. 异步线程池(难点)

  • ThreadPoolTaskExecutor(Spring环境推荐使用):通过其TaskDecorator装饰器
typescript 复制代码
1.自定义Runnable
public class TraceRunnable implements Runnable {
    private final Runnable target;
    private final TraceManager traceManager;
    private String traceId;
    public TraceRunnable(Runnable target, TraceManager traceManager, String traceId) {
        // 这里是主线程执行,提交Runnable时通过参数方式将traceId保存到成员变量
        this.target = target;
        this.traceManager = traceManager;
        this.traceId = traceId;
    }

    @Override
    public void run() {
        try {
            // 这里是子线程执行,将成员变量traceId赋值给MDC
            traceManager.put(traceId);
            target.run();
        } finally {
            traceManager.remove();
        }
    }
}

2.实现TaskDecorator
public class TraceTaskDecorator implements TaskDecorator {
    protected TraceManager traceManager;
    public TraceTaskDecorator(TraceManager traceManager) {
        this.traceManager = traceManager;
    }

    @Override
    public Runnable decorate(Runnable runnable) {
        // 这里已经是子线程了,将成员变量traceId赋值给MDC
        return new TraceRunnable(runnable, traceManager, traceManager.get());
    }
}

3.装配taskDecorator
    @Bean
    @ConditionalOnMissingBean
    public TaskDecorator taskDecorator(TraceManager traceManager) {
        return new TraceTaskDecorator(traceManager);// 传递日志id
    }

    @Bean
    @ConditionalOnMissingBean
    public AsyncTaskExecutor taskExecutor(TaskDecorator taskDecorator) {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 将TraceTaskDecorator实例设置到线程池taskDecorator
        executor.setTaskDecorator(taskDecorator);
        // 设置线程池其他参数...
        return executor;
    }
  • ThreadPoolExecutor:参考ThreadPoolTaskExecutor通过TaskDecorator装饰器
java 复制代码
1.ThreadPoolExecutor没有TaskDecorato参数,自定义线程池参考ThreadPoolTaskExecutor实现
public class TaskDecoratorThreadPoolExecutor extends ThreadPoolExecutor {
    private TaskDecorator taskDecorator;
    public TaskDecoratorThreadPoolExecutor(父构造函数参数..., TaskDecorator taskDecorator) {
        super(父构造函数参数...);
        this.taskDecorator = taskDecorator;
    }
    @Override
    public void execute(Runnable command) {
        // 核心重写execute方法,使用taskDecorator处理Runnable
        Runnable decorated = taskDecorator.decorate(command);
        super.execute(decorated);
    }
}

@Bean // SpringBoot中建议使用ThreadPoolTaskExecutor
@ConditionalOnMissingBean
public ExecutorService executorService(TaskDecorator taskDecorator) {
  // 将TraceTaskDecorator实例设置到线程池taskDecorator
  ExecutorService executor = 
    new TaskDecoratorThreadPoolExecutor(线程池其他参数..., taskDecorator);
  return executor;
}
  • ForkJoinPool(难点中的难点,parallelStream、CompletableFuture底层线程池):通过TTL(TransmittableThreadLocal)解决
xml 复制代码
1.引入transmittable-thread-local解决ThreadLocal无法父子线程上下文
<dependency>
  <groupId>com.alibaba</groupId>
  <artifactId>transmittable-thread-local</artifactId>
  <version>2.14.5</version>
</dependency>
注:启动参数需加上:-javaagent:path/to/transmittable-thread-local-2.x.x.jar

2.引入logback-mdc-ttl,核心逻辑是自定义TtlMDCAdapter替换掉默认的LogbackMDCAdapter
<dependency>
  <groupId>com.ofpay</groupId>
  <artifactId>logback-mdc-ttl</artifactId>
  <version>1.0.2</version>
</dependency>

3.logback-spring.xml配置中添加contextListener
<!-- 替换普通MDC,利用transmittable-thread-local解决ForkJoinPool日志ID传递问题,启动参数加上:-javaagent:path/to/transmittable-thread-local-2.x.x.jar -->
<contextListener class="com.ofpay.logback.TtlMdcListener"/>

关于ForkJoinPool的详细可阅读我另外一篇文章《一次解决ForkJoinPool日志追踪的辛酸经历

王炸级方案SkyWalking

SkyWalking作为一款开源的APM(应用性能管理)工具,为我们提供了更优雅的解决方案:

  1. 自动化追踪: 无需修改业务代码,通过字节码增强技术自动完成追踪
  2. 全场景覆盖: 无论是同步请求、异步处理还是中间件调用,都能完美支持
  3. 丰富的插件生态: 支持主流框架和中间件

1. Servlet请求处理: 无需修改代码

2. 微服务间的RPC调用

  • Feign:无需修改代码
  • RestTemplate:无需修改代码

3. 消息队列: 无需修改代码

skywalking-agent/plugins 目录已包含 apm-rocketMQ-5.x-plugin-8.16.0.jar

4. 定时任务: 无需修改代码

skywalking-agent/plugins 目录已包含 apm-xxl-job-2.x-plugin-8.16.0.jar

5. 异步线程池(不难了)

  • ThreadPoolTaskExecutor(Spring环境推荐使用): 无需修改代码

/skywalking-agent/bootstrap-plugins/apm-jdk-threadpool-plugin-8.16.0.jar复制到 /plugins 目录

  • ThreadPoolExecutor: 无需修改代码

/skywalking-agent/bootstrap-plugins/apm-jdk-threadpool-plugin-8.16.0.jar复制到 /plugins 目录

  • ForkJoinPool(不难了,parallelStream、CompletableFuture底层线程池): 无需修改代码

/skywalking-agent/bootstrap-plugins/apm-jdk-forkjoinpool-plugin-8.16.0.jar复制到 /plugins 目录

MDC vs SkyWalking:优劣对比

方案 优势 劣势
MDC 无外部依赖灵活可控,可根据业务需求定制 代码侵入性强,维护成本高
SkyWalking 零侵入,全场景覆盖,功能强大 依赖外部服务服务不可用traceId输出N/A

可尝试:两种方案结合使用

MDC的traceId生成策略中,读取SkyWalking的traceId,如果SkyWalking不可用,则回退到随机生成traceId,这样就可以保证traceId永远有值

java 复制代码
public class SkywalkingProvider implements TraceIdProvider {
    private final TraceIdProvider fallbackProvider;
    public SkywalkingProvider(TraceIdProvider fallbackProvider) {
        this.fallbackProvider = fallbackProvider;
    }
    @Override
    public String generateTraceId() {
        String traceId = TraceContext.traceId();
        if (StringUtils.isBlank(traceId) || "Ignored_Trace".equalsIgnoreCase(traceId) || "N/A".equalsIgnoreCase(traceId)) {
            traceId = fallbackProvider.generateTraceId(); // 回退到传统方案
        }
        return traceId;
    }
}

总结与展望

通过本文的介绍,我们看到了SkyWalking在微服务日志追踪领域的巨大价值。从传统的MDC手动配置到SkyWalking的自动化追踪,这不仅是技术的进步,更是开发效率的显著提升。

对于正在构建或维护微服务架构的团队来说,SkyWalking绝对是一个值得投入的技术选型。它不仅能帮助我们快速定位问题,还能提供全面的性能监控数据,为系统的优化提供有力支撑。

记住:好的日志追踪不是锦上添花,而是微服务架构的必备基础设施!

如果你也在使用微服务架构,不妨试试SkyWalking,相信它会给你带来意想不到的惊喜!

附:涉及的代码目录

gitee:gitee.com/jq_di/sprin... github:github.com/jqdi/spring...

sql 复制代码
springcloud-template
└── template-framework
     └── trace -- MDC日志追踪核心代码包
          └── feign -- 处理feign传递traceId
          └── filter -- 处理Servlet请求生成或传递traceId
          └── provider -- traceId提供器,多种策略
          └── resttemplate -- 处理resttemplate传递traceId
          └── threadpool -- TaskDecorator处理线程池传递traceId
     └── logback-conf-base.xml -- 基础日志配置(未启用)
     └── logback-conf-base-skywalking. xml -- skywalking日志配置(启用)
└── cicd
     └── plugins -- 插件
          └── skywalking-agent -- skywalking的agent,启动参数用到
相关推荐
爱玩泥巴的小t2 小时前
new Thread().start()底层做了什么?
java
前端拿破轮2 小时前
从0到1搭建个人网站(三):用 Cloudflare R2 + PicGo 搭建高速图床
前端·后端·面试
树獭叔叔2 小时前
深度拆解 DiT:扩散模型与 Transformer 的巅峰结合
后端·aigc·openai
ZhengEnCi2 小时前
08c. 检索算法与策略-混合检索
后端·python·算法
用户7344028193422 小时前
Java 8 Stream 的终极技巧——Collectors 操作
后端
树獭叔叔3 小时前
深度拆解 VAE:生成式 AI 的潜空间大门
后端·aigc·openai
任沫3 小时前
字符串
数据结构·后端
lizhongxuan3 小时前
AI小镇 - 涌现
算法·架构
Java编程爱好者5 小时前
2026 大厂 Java 八股文面试题库|附答案(完整版)
后端