好的,现在开始我们的讲座,主题是“JAVA 微服务链路追踪信息缺失?Sleuth TraceId 跨线程传递方案”。
大家好,今天我们要探讨一个在微服务架构中经常遇到的问题:在使用Spring Cloud Sleuth进行链路追踪时,由于多线程的存在,导致TraceId和SpanId等关键信息丢失,从而无法完整追踪请求链路。
一、问题背景:Sleuth与多线程
Spring Cloud Sleuth是一个优秀的分布式链路追踪解决方案,它能够自动为我们的微服务应用添加链路追踪所需的HTTP Headers,比如X-B3-TraceId、X-B3-SpanId、X-B3-ParentSpanId、X-B3-Sampled和X-B3-Flags等。这些Header会随着请求在各个微服务之间传递,从而串联起整个调用链。
然而,在多线程环境下,Sleuth的自动传递机制会失效。这是因为Sleuth默认使用ThreadLocal来存储Trace信息。ThreadLocal顾名思义,是线程本地变量,每个线程拥有独立的变量副本。当我们在主线程接收到请求并生成TraceId和SpanId后,如果将任务提交给一个新的线程执行,那么新的线程无法访问到主线程的ThreadLocal变量,导致Trace信息丢失。
举个简单的例子:
@RestController
public class MyController {
    @Autowired
    private ExecutorService executorService;
    @GetMapping("/hello")
    public String hello() {
        System.out.println("Controller Thread: " + Thread.currentThread().getName());
        // 打印当前线程的 TraceId
        System.out.println("Controller TraceId: " + MDC.get("traceId"));
        executorService.submit(() -> {
            System.out.println("Async Thread: " + Thread.currentThread().getName());
            // 打印异步线程的 TraceId
            System.out.println("Async TraceId: " + MDC.get("traceId")); // 可能会是 null
            return null;
        });
        return "Hello";
    }
}
在这个例子中,executorService是一个线程池。当/hello接口被调用时,主线程(也就是Controller所在的线程)会打印出TraceId,然后将一个任务提交给线程池执行。在异步线程中,我们尝试打印TraceId,但很有可能打印出来的是null,因为异步线程无法直接访问到主线程的Trace信息。
二、解决方案:TraceContext传播
解决这个问题的关键在于如何将TraceContext(包含TraceId、SpanId等信息)从主线程传播到子线程。常见的解决方案有以下几种:
- 
使用
TraceContext对象手动传递这是最直接的方式。我们可以从主线程获取
TraceContext对象,然后将其传递给子线程。子线程在执行任务之前,先将TraceContext设置到当前线程的ThreadLocal中。@RestController public class MyController { @Autowired private ExecutorService executorService; @Autowired private Tracer tracer; @GetMapping("/hello") public String hello() { TraceContext traceContext = tracer.currentSpan().context(); // 获取当前TraceContext executorService.submit(() -> { // 设置TraceContext到当前线程 try (Tracer.SpanInScope ws = tracer.withSpan(tracer.nextSpan(traceContext))) { System.out.println("Async Thread: " + Thread.currentThread().getName()); System.out.println("Async TraceId: " + MDC.get("traceId")); // 模拟一些业务逻辑 Thread.sleep(100); tracer.currentSpan().tag("async.task", "completed"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return null; }); return "Hello"; } }这种方式需要手动获取和设置
TraceContext,比较繁琐,容易出错。 - 
使用
TraceRunnable和TraceCallableSleuth提供了一个
TraceRunnable类,它可以自动将当前的TraceContext传递给Runnable对象。类似的,还有一个TraceCallable类,用于传递给Callable对象。@RestController public class MyController { @Autowired private ExecutorService executorService; @Autowired private Tracer tracer; @GetMapping("/hello") public String hello() { executorService.submit(new TraceRunnable(tracer, () -> { System.out.println("Async Thread: " + Thread.currentThread().getName()); System.out.println("Async TraceId: " + MDC.get("traceId")); // 模拟一些业务逻辑 try { Thread.sleep(100); tracer.currentSpan().tag("async.task", "completed"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } })); return "Hello"; } }这种方式更加简洁,但是需要修改现有的Runnable和Callable对象。
 - 
自定义
ThreadFactory这是最优雅的解决方案。我们可以自定义一个
ThreadFactory,在创建新线程时,自动将当前的TraceContext传递给新线程。这样,我们就可以透明地解决Trace信息丢失的问题,而无需修改现有的代码。首先,创建一个自定义的
ThreadFactory:import brave.Tracer; import brave.propagation.TraceContext; import org.slf4j.MDC; import java.util.concurrent.ThreadFactory; import java.util.concurrent.atomic.AtomicInteger; public class TraceableThreadFactory implements ThreadFactory { private final String namePrefix; private final AtomicInteger threadNumber = new AtomicInteger(1); private final Tracer tracer; public TraceableThreadFactory(String namePrefix, Tracer tracer) { this.namePrefix = namePrefix; this.tracer = tracer; } @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement()); // 传递TraceContext TraceContext traceContext = tracer.currentSpan() != null ? tracer.currentSpan().context() : null; if (traceContext != null) { return new TraceableThread(thread, traceContext, tracer); } return thread; } private static class TraceableThread extends Thread { private final TraceContext traceContext; private final Tracer tracer; public TraceableThread(Thread thread, TraceContext traceContext, Tracer tracer) { super(thread.getRunnable(), thread.getName()); this.traceContext = traceContext; this.tracer = tracer; } @Override public void run() { try (Tracer.SpanInScope ws = tracer.withSpan(tracer.nextSpan(traceContext))) { super.run(); } } } }然后,在配置线程池时,使用自定义的
ThreadFactory:@Configuration public class ExecutorConfig { @Autowired private Tracer tracer; @Bean public ExecutorService executorService() { return Executors.newFixedThreadPool(10, new TraceableThreadFactory("my-thread-pool", tracer)); } }最后,在Controller中使用线程池:
@RestController public class MyController { @Autowired private ExecutorService executorService; @Autowired private Tracer tracer; @GetMapping("/hello") public String hello() { executorService.submit(() -> { System.out.println("Async Thread: " + Thread.currentThread().getName()); System.out.println("Async TraceId: " + MDC.get("traceId")); // 模拟一些业务逻辑 try { Thread.sleep(100); tracer.currentSpan().tag("async.task", "completed"); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); return "Hello"; } }这样,我们就可以在异步线程中正确地获取TraceId,从而保证链路追踪的完整性。
 - 
使用
InheritableThreadLocalInheritableThreadLocal是ThreadLocal的一个子类,它允许子线程继承父线程的值。虽然它看似能解决问题,但需要谨慎使用。- 适用场景: 当线程创建后,值只需要传递一次,后续子线程的值不需要再与父线程同步时。
 - 局限性:
- 数据竞争: 如果父线程在创建子线程之后修改了 
InheritableThreadLocal的值,子线程不会得到更新。 - 内存泄漏:  如果线程池中的线程被重用,旧线程的 
InheritableThreadLocal值可能会被错误地传递给新的任务。 - 不适用于线程池:  由于线程池线程的复用特性,
InheritableThreadLocal往往不能正确传递TraceId,因为线程池中的线程可能已经执行过其他的任务,其InheritableThreadLocal中保存的可能是之前的TraceId。 
 - 数据竞争: 如果父线程在创建子线程之后修改了 
 
尽管如此,了解
InheritableThreadLocal仍然是有意义的,因为它在某些特定的场景下可以作为一种简单的解决方案。以下是一个使用
InheritableThreadLocal的示例(不推荐在线程池中使用):private static final InheritableThreadLocal<String> traceIdHolder = new InheritableThreadLocal<>(); @GetMapping("/hello") public String hello() { String traceId = MDC.get("traceId"); traceIdHolder.set(traceId); // 设置到 InheritableThreadLocal new Thread(() -> { String childTraceId = traceIdHolder.get(); System.out.println("Child Thread TraceId: " + childTraceId); }).start(); return "Hello"; }总结:
InheritableThreadLocal在微服务和链路追踪的场景中,尤其是在使用线程池时,通常不是一个可靠的解决方案。 推荐使用前面介绍的TraceableThreadFactory或TraceRunnable/TraceCallable等方式。 
三、不同方案的比较
为了更好地理解各种方案的优缺点,我们将其总结在一个表格中:
| 方案 | 优点 | 缺点 | 适用场景 | 
|---|---|---|---|
手动传递TraceContext | 
简单易懂 | 繁琐,容易出错,需要修改大量代码 | 适用于简单的、少量使用多线程的场景,或者需要对TraceContext进行精细控制的场景 | 
使用TraceRunnable/Callable | 
相对简洁 | 需要修改现有的Runnable和Callable对象 | 适用于可以修改Runnable和Callable对象的场景 | 
自定义ThreadFactory | 
透明,无需修改现有代码,优雅 | 需要自定义ThreadFactory | 
适用于需要大量使用多线程,且希望以最优雅的方式解决Trace信息丢失问题的场景。 尤其是在使用线程池的场景中,此方案最佳。 | 
使用InheritableThreadLocal | 
简单 | 不适用于线程池,可能导致数据竞争和内存泄漏,不推荐使用在链路追踪场景中 | 适用于线程创建后,值只需要传递一次,后续子线程的值不需要再与父线程同步的特定场景。 不推荐在微服务和链路追踪中使用,尤其是在使用线程池时。 | 
四、代码示例:完整的可运行示例
下面提供一个完整的可运行示例,使用自定义ThreadFactory来解决Trace信息丢失的问题。
1. pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>sleuth-trace-propagation</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>sleuth-trace-propagation</name>
    <description>Sleuth Trace Propagation Example</description>
    <properties>
        <java.version>17</java.version>
        <spring-cloud.version>2023.0.0</spring-cloud.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-sleuth</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
2. TraceableThreadFactory.java
package com.example.suthtracepropagation;
import brave.Tracer;
import brave.propagation.TraceContext;
import org.slf4j.MDC;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
public class TraceableThreadFactory implements ThreadFactory {
    private final String namePrefix;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final Tracer tracer;
    public TraceableThreadFactory(String namePrefix, Tracer tracer) {
        this.namePrefix = namePrefix;
        this.tracer = tracer;
    }
    @Override
    public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, namePrefix + "-" + threadNumber.getAndIncrement());
        // 传递TraceContext
        TraceContext traceContext = tracer.currentSpan() != null ? tracer.currentSpan().context() : null;
        if (traceContext != null) {
            return new TraceableThread(thread, traceContext, tracer);
        }
        return thread;
    }
    private static class TraceableThread extends Thread {
        private final TraceContext traceContext;
        private final Tracer tracer;
        public TraceableThread(Thread thread, TraceContext traceContext, Tracer tracer) {
            super(thread.getRunnable(), thread.getName());
            this.traceContext = traceContext;
            this.tracer = tracer;
        }
        @Override
        public void run() {
            try (Tracer.SpanInScope ws = tracer.withSpan(tracer.nextSpan(traceContext))) {
                super.run();
            }
        }
    }
}
3. ExecutorConfig.java
package com.example.suthtracepropagation;
import brave.Tracer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Configuration
public class ExecutorConfig {
    @Autowired
    private Tracer tracer;
    @Bean
    public ExecutorService executorService() {
        return Executors.newFixedThreadPool(10, new TraceableThreadFactory("my-thread-pool", tracer));
    }
}
4. MyController.java
package com.example.suthtracepropagation;
import brave.Tracer;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ExecutorService;
@RestController
public class MyController {
    @Autowired
    private ExecutorService executorService;
    @Autowired
    private Tracer tracer;
    @GetMapping("/hello")
    public String hello() {
        System.out.println("Controller Thread: " + Thread.currentThread().getName());
        System.out.println("Controller TraceId: " + MDC.get("traceId"));
        executorService.submit(() -> {
            System.out.println("Async Thread: " + Thread.currentThread().getName());
            System.out.println("Async TraceId: " + MDC.get("traceId"));
            try {
                Thread.sleep(100);
                tracer.currentSpan().tag("async.task", "completed");
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
        return "Hello";
    }
}
5. SleuthTracePropagationApplication.java
package com.example.suthtracepropagation;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SleuthTracePropagationApplication {
    public static void main(String[] args) {
        SpringApplication.run(SleuthTracePropagationApplication.class, args);
    }
}
运行这个示例,访问/hello接口,你将在控制台中看到异步线程中也正确地打印了TraceId。
五、其他注意事项
- MDC(Mapped Diagnostic Context): Sleuth会将TraceId和SpanId等信息放入MDC中,方便我们在日志中使用。在使用多线程时,需要确保MDC也能够正确传递。自定义ThreadFactory的例子中已经包含了MDC的传递。
 - 异步框架:  如果使用Spring的
@Async注解,Spring会自动处理TraceContext的传递。但需要确保开启了@EnableAsync注解。 - 响应式编程: 在使用Reactor等响应式编程框架时,需要使用Sleuth提供的相应的工具类来传递TraceContext,例如
reactor-context-propagation。 
六、选择合适的方案
选择哪种方案取决于具体的应用场景和需求。一般来说,自定义ThreadFactory是最通用、最优雅的解决方案,能够透明地解决Trace信息丢失的问题。如果只是在少量的地方使用多线程,可以考虑手动传递TraceContext或者使用TraceRunnable/Callable。 但是一定要避开 InheritableThreadLocal 在线程池中的使用。
确保链路追踪完整
TraceId在多线程环境下的正确传递,对于保证微服务链路追踪的完整性和准确性至关重要。选择合适的TraceContext传播方案,能够帮助我们更好地监控和诊断分布式系统的性能问题。