关于 spring 工程中添加 traceID 实践

关于 spring 工程中添加 traceID 实践

我们在做分布式架构时一定会关心系统的可观测性,可观测性中的追踪的实现,就需要靠跨多个系统的 traceID 实现,那有没有合适的框架可以支持在 spring 或者 springboot 工程中添加 traceID 呢

选型

1,现代化标准链路追踪(Micrometer Tracing + Zipkin/Jaeger)

OpenTelemetry 是一个大一统的trace上报标准,而 Micrometer Tracing 是专为 Java/Spring Boot 应用设计的门面工具,且其底层实现依赖 OpenTelemetry

适用场景:Spring Boot 3.x(或 2.x),需要标准的 Trace/Span 语义,需要跨服务调用链可视化、耗时分析

实现原理:基于 W3C traceparent 标准透传上下文,自动拦截 RestTemplate/WebClient/Feign/MQ 等组件,并自动将 TraceID 写入 MDC

2,无侵入式 APM(Apache SkyWalking)

适用场景:中大型企业级微服务架构,不需要改代码(甚至不用加依赖),需要强大的拓扑图、慢 SQL 分析、告警等功能

实现原理:通过 Java Agent 字节码增强技术,在类加载时动态植入追踪逻辑

本文会重点介绍 Micrometer Tracing + Zipkin/Jaeger 的落地实践,如果后续有时间,会将 SkyWalking 补上

落地

Micrometer Tracing 一般的标准选用 w3c,除此之外还有 B3 等等协议

W3C 协议文档:https://www.w3.org/TR/trace-context/#traceparent-header

接下来我们一步步接入

基础

1,引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- Micrometer Tracing (链路追踪核心) -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-bridge-brave</artifactId>
</dependency>
<!-- 将链路数据上报给 Zipkin -->
<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-tracing-reporter-zipkin</artifactId>
</dependency>

2,配置 application.yml

yml 复制代码
management:
  tracing:
    sampling:
      probability: 1.0 # 采样率,生产环境建议 0.1 (10%)
    propagation:
      type: w3c # 使用 W3C 标准透传 Header (traceparent)
  zipkin:
    tracing:
    -- 查看腾讯云文档和阿里云文档获取
      endpoint: http://your-zipkin-server:9411/api/v2/spans

3,修改 log4j.xml

xml 复制代码
<PatternLayout charset="UTF-8" pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} [%t] %-5level (%F\:%L) - [traceId:%X{traceId}] %msg%n"/>

异步链路追踪

关于异步流程,自定义线程池时,主线程的 MDC(log4j 框架存放线程数据的地方)和上下文状态是无法传递给子线程的(因为底层依赖 ThreadLocal)。解决方式:

java 复制代码
    /**
     * 外部业务线程调用此方法提交任务
     */
    public void submitTask(Runnable task) {
        // 【关键1】:在当前业务线程中,捕获当前的 TraceID/MDC 上下文
        ContextSnapshot snapshot = ContextSnapshotFactory.builder().build().captureAll();
        
        // 【关键2】:将原始 task 和 snapshot 绑定在一起,丢进队列
        taskQueue.offer(() -> {
            // 这里什么都不做,只是一个包装壳
        });
        
        // 更优雅的做法是定义一个 TaskWrapper
        taskQueue.offer(new TaskWrapper(task, snapshot));
    }
    
    // 简单的内部类,用于携带上下文
    private static class TaskWrapper implements Runnable {
        private final Runnable delegate;
        private final ContextSnapshot snapshot;

        public TaskWrapper(Runnable delegate, ContextSnapshot snapshot) {
            this.delegate = delegate;
            this.snapshot = snapshot;
        }

        @Override
        public void run() {
            // 省略业务逻辑.....
        }
    }

在 ExecutorThread 执行时恢复上下文,修改 ExecutorThread 内部逻辑。当它从队列里拿出任务准备执行时,用 Micrometer 把上下文穿上:

java 复制代码
public class ExecutorThread implements Runnable {
    @Override
    public void run() {
        try {
                // 【关键】:在 ExecutorThread (子线程) 中,使用 snapshot.wrap 恢复上下文并执行
                // 这一步会自动把打包进来的 TraceID 设置到当前子线程的 ThreadLocal(MDC) 里
                snapshot.wrap(taskWrapper).run();
                
                /*
                 * 注意:不需要手动 MDC.put()!
                 * snapshot.wrap() 内部在执行前会自动设置 MDC,执行后会自动清理 MDC。
                 */
            }
        } catch (Exception e) {
            // 异常处理
        }
    }
}

如果是线程池,需要使用封装器,将调用线程的上下文传入

java 复制代码
    private static final ExecutorService RAW_HTTP_CLIENT_EXECUTOR  =
            ContextExecutorService.wrap(原本的线程池, ContextSnapshotFactory.builder().build()::captureAll);

如果是虚拟线程,micrometer-tracing 也为其做了很完善的封装

java 复制代码
Thread.startVirtualThread(ContextSnapshotFactory.builder().build().captureAll().wrap(virtualThread));

这里额外提一嘴虚拟线程的底层运作机制:载体线程的窃取与共享

在传统线程里,主线程创建子线程,会自动复制一份 ThreadLocal 过去。但在虚拟线程里,虚拟线程是极度轻量级的,它可能在虚拟线程 A(载体)上跑了一半,遇到 I/O 阻塞,被卸载。等 I/O 结束,它可能被装载到虚拟线程 B(载体)上继续跑

虚拟线程没有固定的父线程,所以 InheritableThreadLocal 在虚拟线程里完全是个摆设,你无法靠它传递上下文。同时也会造成载体线程上下文污染问题

JDK 官方也意识到了 ThreadLocal 在虚拟线程时代的毒点,所以推出了 ScopedValue(作用域值)。它绑定在虚拟线程的生命周期上,挂起时自动释放,不占用载体线程资源。在官方的 wrap 源码里,有一个至关重要的 finally 代码块。当 ioVirtualThread 执行完毕(或者抛出异常),那个 finally 会百分之百把当前载体线程上的 ThreadLocal 清理干净。这就保证了虚拟线程无论怎么在载体线程之间蹦跶,都不会留下垃圾

我们建议将这些线程池封装到 common 包中,为业务侧屏蔽这些逻辑

跨服务链路追踪

使用 new HttpPost(url)、new CloseableHttpClient() 发请求,Spring 和 Micrometer 无感知,它没有任何机会去加 Header。因此需要使用 RestTemplate 发送 http 请求,此时框架会自动完成 TraceID 的生成、透传

java 复制代码
@Bean
public RestTemplate aiInterfaceRestTemplate(RestTemplateBuilder restTemplateBuilder) {
    HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
    // 建立连接超时
    factory.setConnectTimeout(CONNECT_TIMEOUT);
    // 等待响应超时
    factory.setReadTimeout(SOCKET_TIMEOUT);
    factory.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT);

    return restTemplateBuilder
            .requestFactory(() -> factory)
            .build();
}
相关推荐
李白的粉2 小时前
基于springboot+vue的旅游民宿管理系统
java·spring boot·vue·毕业设计·课程设计·源代码·旅游民宿管理系统
ictI CABL2 小时前
Spring Boot与MyBatis
spring boot·后端·mybatis
摇滚侠2 小时前
搭建前端开发环境 安装 nodejs 设置淘宝镜像 最简化最标准版本 不使用 NVM NVM 高版本无法安装低版本 nodejs
java·开发语言·node.js
花千树-0102 小时前
兼容 ThreadLocal 的用户上下文透传方案:WebFlux 项目改造实践
java·spring boot·servlet·jetty
小江的记录本4 小时前
【Linux】《Linux常用命令汇总表》
linux·运维·服务器·前端·windows·后端·macos
无人机9014 小时前
Delphi 网络编程实战:TIdTCPClient 与 TIdTCPServer 类深度解析
java·开发语言·前端
lclcooky4 小时前
Spring 中使用Mybatis,超详细
spring·tomcat·mybatis
TeDi TIVE4 小时前
Spring Cloud Gateway
java
:mnong5 小时前
Superpowers 项目设计分析
java·c语言·c++·python·c#·php·skills