前言
在应用进程中,链路信息的传递实际就是Span 的传递,我们之前是基于ThreadLocal 将Span 作为线程本地变量来传递的,这在同步的场景下是没有问题的,但是一旦涉及到异步场景例如异步调用下游或者异步查询数据库等,那么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;
}
}
}
由于ThreadLocalScope 和ThreadLocalScopeManager的内部实现已经写死,所以我们需要自己提供实现类来替换掉它们,我们自己的实现类如下所示。
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-1 和example-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-1 和example-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-1 和example-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-1 和example-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-1 和example-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的规则来对线程池或者任务做一些改造包装。