9. 异步链路追踪实现设计

前言

在应用进程中,链路信息的传递实际就是Span 的传递,我们之前是基于ThreadLocalSpan 作为线程本地变量来传递的,这在同步的场景下是没有问题的,但是一旦涉及到异步场景例如异步调用下游或者异步查询数据库等,那么Span 信息就丢了,原因就是ThreadLocal 设置的本地变量无法跨线程传递,所以本文将基于TransmittableThreadLocal 实现异步链路追踪以解决异步场景下Span信息无法传递的问题。

关于TransmittableThreadLocal ,可以参考图解Java线程间本地变量传递,本文将不再赘述TransmittableThreadLocal的原理。

transmittable-thread-local 版本:2.11.4

github 地址:honey-tracing

正文

1. 异步链路追踪改造设计与实现

其实要实现异步链路追踪很简单,回顾一下之前的实现中,Span 会被包装为ThreadLocalScope ,然后交由ThreadLocalScopeManager 来进行传递,而ThreadLocalScopeManager 传递ThreadLocalScope 的方式就是将ThreadLocalScope 通过ThreadLocal 设置为线程的本地变量,那么问题就在这里,ThreadLocal 设置的线程本地变量无法跨线程传递,所以要解决这个问题,就是将能够跨线程传递本地变量的TransmittableThreadLocal 替换掉ThreadLocal,问题就完美解决了。

首先引入TransmittableThreadLocal的依赖,如下所示。

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.4</version>
    <scope>provided</scope>
</dependency>

其次提供异步链路追踪的开关配置,修改后的属性配置类HoneyTracingProperties如下所示。

java 复制代码
/**
 * 分布式链路追踪配置属性类。
 */
@ConfigurationProperties("honey.tracing")
public class HoneyTracingProperties {

    private boolean enabled;

    private HttpUrlProperties httpUrl = new HttpUrlProperties();

    private AsyncProperties async = new AsyncProperties();

    public boolean isEnabled() {
        return enabled;
    }

    public void setEnabled(boolean enabled) {
        this.enabled = enabled;
    }

    public HttpUrlProperties getHttpUrl() {
        return httpUrl;
    }

    public void setHttpUrl(HttpUrlProperties httpUrl) {
        this.httpUrl = httpUrl;
    }

    public AsyncProperties getAsync() {
        return async;
    }

    public void setAsync(AsyncProperties async) {
        this.async = async;
    }

    public static class HttpUrlProperties {
        /**
         * 按照/url1,/url2这样配置。
         */
        private String urlPattern = "/*";
        /**
         * 按照/url1|/honey.*这样配置。
         */
        private String skipPattern = "";

        public String getUrlPattern() {
            return urlPattern;
        }

        public void setUrlPattern(String urlPattern) {
            this.urlPattern = urlPattern;
        }

        public String getSkipPattern() {
            return skipPattern;
        }

        public void setSkipPattern(String skipPattern) {
            this.skipPattern = skipPattern;
        }
    }

    public static class AsyncProperties {
        private boolean enabled;

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }
    }

}

由于ThreadLocalScopeThreadLocalScopeManager的内部实现已经写死,所以我们需要自己提供实现类来替换掉它们,我们自己的实现类如下所示。

java 复制代码
/**
 * 依赖{@link TransmittableThreadLocal}的{@link Scope}实现。
 */
public class HoneyTtlScope implements Scope {

    private final HoneyTtlScopeManager scopeManager;
    private final Span wrapped;
    private final HoneyTtlScope toRestore;

    HoneyTtlScope(HoneyTtlScopeManager scopeManager, Span wrapped) {
        this.scopeManager = scopeManager;
        this.wrapped = wrapped;
        this.toRestore = scopeManager.tlsScope.get();
        scopeManager.tlsScope.set(this);
    }

    @Override
    public void close() {
        if (scopeManager.tlsScope.get() != this) {
            return;
        }
        scopeManager.tlsScope.set(toRestore);
    }

    Span span() {
        return wrapped;
    }

}
java 复制代码
/**
 * 基于{@link TransmittableThreadLocal}的{@link ScopeManager}实现。
 */
public class HoneyTtlScopeManager implements ScopeManager {

    final InheritableThreadLocal<HoneyTtlScope> tlsScope = new TransmittableThreadLocal<>();

    @Override
    public Scope activate(Span span) {
        return new HoneyTtlScope(this, span);
    }

    @Override
    public Span activeSpan() {
        HoneyTtlScope scope = tlsScope.get();
        return scope == null ? null : scope.span();
    }

}

然后我们需要将HoneyTtlScopeManager 替换掉ThreadLocalScopeManager ,这一步在创建Tracer 的时候完成,所以修改HoneyTracingConfig如下所示。

java 复制代码
/**
 * 分布式链路追踪配置类。
 */
@Configuration
@EnableConfigurationProperties({HoneyTracingProperties.class})
@ConditionalOnProperty(prefix = "honey.tracing", name = "enabled", havingValue = "true", matchIfMissing = true)
public class HoneyTracingConfig {

    @Bean
    @ConditionalOnMissingBean(Sampler.class)
    public Sampler sampler() {
        return new ProbabilisticSampler(DEFAULT_SAMPLE_RATE);
    }

    @Bean
    @ConditionalOnMissingBean(Reporter.class)
    public Reporter reporter() {
        return new HoneySpanReporter();
    }

    @Bean
    @ConditionalOnProperty(prefix = "honey.tracing.async", name = "enabled", havingValue = "true")
    public ScopeManager honeyTtlScopeManager() {
        return new HoneyTtlScopeManager();
    }

    @Bean
    @ConditionalOnProperty(prefix = "honey.tracing.async", name = "enabled", havingValue = "false", matchIfMissing = true)
    public ScopeManager threadLocalScopeManager() {
        return new ThreadLocalScopeManager();
    }

    @Bean
    @ConditionalOnMissingBean(Tracer.class)
    public Tracer tracer(Sampler sampler, Reporter reporter, ScopeManager scopeManager) {
        return new JaegerTracer.Builder(HONEY_TRACER_NAME)
                .withTraceId128Bit()
                .withZipkinSharedRpcSpan()
                .withSampler(sampler)
                .withReporter(reporter)
                .withScopeManager(scopeManager)
                .build();
    }

}

那么至此,异步链路追踪改造完成。

二. 异步链路追踪使用说明

我们对之前搭建好的example-service-1进行改造,来演示异步链路追踪的使用。

在开始演示前,需要先引入TransmittableThreadLocal的依赖,如下所示。

xml 复制代码
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>transmittable-thread-local</artifactId>
    <version>2.11.4</version>
</dependency>

然后通过配置打开异步链路追踪,如下所示。

yaml 复制代码
honey:
  tracing:
    async:
      enabled: true

1. new Thread

首先演示直接通过new 一个Thread 的方式来异步执行任务,在RestTemplateController新增如下代码。

java 复制代码
@RestController
public class RestTemplateController {

    @Autowired
    private RestTemplate restTemplate;

    ......

    @GetMapping("/async/thread/send")
    public void syncSendByThread(String url) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        new Thread(new Runnable() {
            @Override
            public void run() {
                restTemplate.getForEntity(url, Void.class);
                countDownLatch.countDown();
            }
        }).start();
        countDownLatch.await();
    }

    ......

}

同时把example-service-1example-service-2启动起来,并调用如下接口。

txt 复制代码
http://localhost:8080/async/thread/send?url=http://localhost:8081/receive

example-service-1中打印如下链路日志。

json 复制代码
{
    "traceId": "96effadb892bd423c19d32ed70b9a406",
    "spanId": "c19d32ed70b9a406",
    "parentSpanId": "0000000000000000",
    "timestamp": "1708599959132",
    "duration": "5",
    "httpCode": "200",
    "host": "http://localhost:8080",
    "requestStacks": [
        {
            "subSpanId": "43731381f988131a",
            "subHttpCode": "200",
            "subTimestamp": "1708599959133",
            "subDuration": "3",
            "subHost": "localhost:8081"
        }
    ]
}

example-service-2中打印如下链路日志。

json 复制代码
{
    "traceId": "96effadb892bd423c19d32ed70b9a406",
    "spanId": "43731381f988131a",
    "parentSpanId": "c19d32ed70b9a406",
    "timestamp": "1708599959134",
    "duration": "0",
    "httpCode": "200",
    "host": "http://localhost:8081",
    "requestStacks": []
}

可见异步链路是正常工作的。

2. 直接使用ThreadPoolExecutor

通常我们做异步操作,是不会直接new 一个Thread 来执行异步任务的,而是会把任务丢到线程池,让线程池来执行任务,假如我们的任务是一个Runnable,那么应该像下面这样来使用。

java 复制代码
@RestController
public class RestTemplateController {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private ThreadPoolExecutor threadPoolExecutor;

    ......

    @GetMapping("/async/thread-pool/send")
    public void asyncSendByThreadPool(String url) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        threadPoolExecutor.execute(TtlRunnable.get(new Runnable() {
            @Override
            public void run() {
                restTemplate.getForEntity(url, Void.class);
                countDownLatch.countDown();
            }
        }));
        countDownLatch.await();
    }

    ......

}

其中使用的线程池由ThreadPoolExecutorConfig 注册到了Spring容器中,如下所示。

java 复制代码
@Configuration
public class ThreadPoolExecutorConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        return new ThreadPoolExecutor(
                1,
                1,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable runnable) {
                        return new Thread(runnable, "Pool-Thread-1");
                    }
                });
    }

}

所以直接使用ThreadPoolExecutor 来执行Runnable 时,需要先将Runnable 转换为TtlRunnable ,然后再丢到ThreadPoolExecutor 中执行,同理,如果是直接使用ThreadPoolExecutor 来执行Callable ,那么需要先将Callable 转换为TtlCallable

同时把example-service-1example-service-2启动起来,并调用如下接口。

txt 复制代码
http://localhost:8080/async/thread-pool/send?url=http://localhost:8081/receive

example-service-1中打印如下链路日志。

json 复制代码
{
    "traceId": "2a198cc0a513f2ca455c98d9a320ebc7",
    "spanId": "455c98d9a320ebc7",
    "parentSpanId": "0000000000000000",
    "timestamp": "1708601760228",
    "duration": "10",
    "httpCode": "200",
    "host": "http://localhost:8080",
    "requestStacks": [
        {
            "subSpanId": "7332de988308e305",
            "subHttpCode": "200",
            "subTimestamp": "1708601760232",
            "subDuration": "5",
            "subHost": "localhost:8081"
        }
    ]
}

example-service-2中打印如下链路日志。

json 复制代码
{
    "traceId": "2a198cc0a513f2ca455c98d9a320ebc7",
    "spanId": "7332de988308e305",
    "parentSpanId": "455c98d9a320ebc7",
    "timestamp": "1708601760235",
    "duration": "1",
    "httpCode": "200",
    "host": "http://localhost:8081",
    "requestStacks": []
}

可见异步链路是正常工作的。

3. 将ThreadPoolExecutor包装为ExecutorServiceTtlWrapper

如果每次异步执行任务时,都要将Runnable 包装为TtlRunnable 才能实现异步链路追踪,这样的代码写起来实在是太繁琐了,此时既然不想包装Runnable ,那么我们可以选择包装ThreadPoolExecutor,就像下面这样。

java 复制代码
@Configuration
public class ThreadPoolExecutorConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        return new ThreadPoolExecutor(
                1,
                1,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable runnable) {
                        return new Thread(runnable, "Pool-Thread-1");
                    }
                });
    }

    @Bean
    public ExecutorService wrappedThreadPool() {
        return TtlExecutors.getTtlExecutorService(new ThreadPoolExecutor(
                1,
                1,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable runnable) {
                        return new Thread(runnable, "Pool-Thread-1");
                    }
                }));
    }

}

通过TtlExecutors 可以将我们提供的ThreadPoolExecutor 包装为ExecutorServiceTtlWrapper ,后续直接将Runnable 丢给ExecutorServiceTtlWrapper,也能实现异步链路追踪,如下所示。

java 复制代码
@RestController
public class RestTemplateController {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private ExecutorService wrappedThreadPool;

    ......

    @GetMapping("/async/wrapped-thread-pool/send")
    public void asyncSendByWrappedThreadPool(String url) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        wrappedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                restTemplate.getForEntity(url, Void.class);
                countDownLatch.countDown();
            }
        });
        countDownLatch.await();
    }

    ......

}

同时把example-service-1example-service-2启动起来,并调用如下接口。

txt 复制代码
http://localhost:8080/async/wrapped-thread-pool/send?url=http://localhost:8081/receive

example-service-1中打印如下链路日志。

json 复制代码
{
    "traceId": "075b13d4cb10d633fc7eb71ca8d0079a",
    "spanId": "fc7eb71ca8d0079a",
    "parentSpanId": "0000000000000000",
    "timestamp": "1708602413533",
    "duration": "7",
    "httpCode": "200",
    "host": "http://localhost:8080",
    "requestStacks": [
        {
            "subSpanId": "eee3dae02dd9139e",
            "subHttpCode": "200",
            "subTimestamp": "1708602413535",
            "subDuration": "4",
            "subHost": "localhost:8081"
        }
    ]
}

example-service-2中打印如下链路日志。

json 复制代码
{
    "traceId": "075b13d4cb10d633fc7eb71ca8d0079a",
    "spanId": "eee3dae02dd9139e",
    "parentSpanId": "fc7eb71ca8d0079a",
    "timestamp": "1708602413538",
    "duration": "0",
    "httpCode": "200",
    "host": "http://localhost:8081",
    "requestStacks": []
}

可见异步链路是正常工作的。

4. 将ScheduledThreadPoolExecutor包装为ScheduledExecutorServiceTtlWrapper

不单可以把ThreadPoolExecutor 包装为ExecutorServiceTtlWrapper ,也能将ScheduledThreadPoolExecutor 包装为ScheduledExecutorServiceTtlWrapper,就像下面这样。

java 复制代码
@Configuration
public class ThreadPoolExecutorConfig {

    @Bean
    public ThreadPoolExecutor threadPoolExecutor() {
        return new ThreadPoolExecutor(
                1,
                1,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable runnable) {
                        return new Thread(runnable, "Pool-Thread-1");
                    }
                });
    }

    @Bean
    public ExecutorService wrappedThreadPool() {
        return TtlExecutors.getTtlExecutorService(new ThreadPoolExecutor(
                1,
                1,
                60,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable runnable) {
                        return new Thread(runnable, "Pool-Thread-1");
                    }
                }));
    }

    @Bean
    public ScheduledExecutorService wrappedScheduledThreadPool() {
        return TtlExecutors.getTtlScheduledExecutorService(new ScheduledThreadPoolExecutor(
                1,
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable runnable) {
                        return new Thread(runnable, "Pool-Thread-1");
                    }
                }));
    }

}

后续直接将Runnable 丢给ScheduledExecutorServiceTtlWrapper,也是能实现异步链路追踪的,如下所示。

java 复制代码
@RestController
public class RestTemplateController {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private ScheduledExecutorService wrappedScheduledThreadPool;

    ......

    @GetMapping("/async/wrapped-scheduled-thread-pool/send")
    public void asyncSendByWrappedScheduledThreadPool(String url) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        wrappedScheduledThreadPool.schedule(new Runnable() {
            @Override
            public void run() {
                restTemplate.getForEntity(url, Void.class);
                countDownLatch.countDown();
            }
        }, 1, TimeUnit.SECONDS);
        countDownLatch.await();
    }

    ......

}

同时把example-service-1example-service-2启动起来,并调用如下接口。

txt 复制代码
http://localhost:8080/async/wrapped-scheduled-thread-pool/send?url=http://localhost:8081/receive

example-service-1中打印如下链路日志。

json 复制代码
{
    "traceId": "8726f2fcbe845aa884d1beee49950cdf",
    "spanId": "84d1beee49950cdf",
    "parentSpanId": "0000000000000000",
    "timestamp": "1708602842299",
    "duration": "1009",
    "httpCode": "200",
    "host": "http://localhost:8080",
    "requestStacks": [
        {
            "subSpanId": "372acae257de2b85",
            "subHttpCode": "200",
            "subTimestamp": "1708602843302",
            "subDuration": "4",
            "subHost": "localhost:8081"
        }
    ]
}

example-service-2中打印如下链路日志。

json 复制代码
{
    "traceId": "8726f2fcbe845aa884d1beee49950cdf",
    "spanId": "372acae257de2b85",
    "parentSpanId": "84d1beee49950cdf",
    "timestamp": "1708602843305",
    "duration": "1",
    "httpCode": "200",
    "host": "http://localhost:8081",
    "requestStacks": []
}

可见异步链路是正常工作的。

5. @Async注解

如果是使用@Async 注解来执行异步操作,异步链路追踪同样也是支持的,只需要把我们注册到了Spring 容器中的包装好的ExecutorServiceTtlWrapper 设置给@Async注解,就能实现异步链路追踪。

首先编写一个AsyncService如下所示。

java 复制代码
@Service
public class AsyncService {

    @Async("wrappedThreadPool")
    public void send(RestTemplate restTemplate, CountDownLatch countDownLatch, String url) {
        restTemplate.getForEntity(url, Void.class);
        countDownLatch.countDown();
    }

}

然后RestTemplateController中添加如下接口。

java 复制代码
@RestController
public class RestTemplateController {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private AsyncService asyncService;

    @Autowired
    private ExecutorService wrappedThreadPool;

    ......

    @GetMapping("/async/annotation/send")
    public void asyncSendByAsyncAnnotation(String url) throws Exception {
        CountDownLatch countDownLatch = new CountDownLatch(1);
        asyncService.send(restTemplate, countDownLatch, url);
        countDownLatch.await();
    }

}

最后需要在启动类上添加@EnableAsync 注解来开启对@Async注解的支持,如下所示。

java 复制代码
@EnableAsync
@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

}

同时把example-service-1example-service-2启动起来,并调用如下接口。

txt 复制代码
http://localhost:8080/async/annotation/send?url=http://localhost:8081/receive

example-service-1中打印如下链路日志。

json 复制代码
{
    "traceId": "026fb86839e40a54026924f8fa247e7b",
    "spanId": "026924f8fa247e7b",
    "parentSpanId": "0000000000000000",
    "timestamp": "1708604677860",
    "duration": "5",
    "httpCode": "200",
    "host": "http://localhost:8080",
    "requestStacks": [
        {
            "subSpanId": "b90af03e0bed026c",
            "subHttpCode": "200",
            "subTimestamp": "1708604677860",
            "subDuration": "3",
            "subHost": "localhost:8081"
        }
    ]
}

example-service-2中打印如下链路日志。

json 复制代码
{
    "traceId": "026fb86839e40a54026924f8fa247e7b",
    "spanId": "b90af03e0bed026c",
    "parentSpanId": "026924f8fa247e7b",
    "timestamp": "1708604677861",
    "duration": "0",
    "httpCode": "200",
    "host": "http://localhost:8081",
    "requestStacks": []
}

可见异步链路是正常工作的。

总结

异步链路追踪实现的关键,就是使用TransmittableThreadLocal 来实现ScopeManager ,然后替换掉默认的ThreadLocalScopeManager 。异步链路追踪主要就是基于TransmittableThreadLocal 提供的跨线程传递本地变量的方式来跨线程传递了Span ,所以在实际使用时,也得遵循TransmittableThreadLocal的规则来对线程池或者任务做一些改造包装。

相关推荐
大梦百万秋26 分钟前
Spring Boot实战:构建一个简单的RESTful API
spring boot·后端·restful
忒可君39 分钟前
C# winform 报错:类型“System.Int32”的对象无法转换为类型“System.Int16”。
java·开发语言
斌斌_____1 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
路在脚下@1 小时前
Spring如何处理循环依赖
java·后端·spring
一个不秃头的 程序员1 小时前
代码加入SFTP JAVA ---(小白篇3)
java·python·github
丁总学Java2 小时前
--spring.profiles.active=prod
java·spring
上等猿2 小时前
集合stream
java
java1234_小锋2 小时前
MyBatis如何处理延迟加载?
java·开发语言
菠萝咕噜肉i2 小时前
MyBatis是什么?为什么有全自动ORM框架还是MyBatis比较受欢迎?
java·mybatis·框架·半自动
海绵波波1072 小时前
flask后端开发(1):第一个Flask项目
后端·python·flask