项目中使用了@Async线程池进行异步调用,为了便于重现问题,把线程池线程数设置为1。
观察发现:第一次调用的时候异步线程打印的traceId跟主线程一样,然后后续的traceId都跟第一次一样,一直重复使用。
直接查看源码进行研究,首先定位到异步线程池的traceId处理是在org.springframework.cloud.sleuth.instrument.async.TraceAsyncAspect这个类:
这里使用了aop进行处理,逻辑看起来也很简单,如果当前span不为null就直接使用当前的span,否则就创建新的。不过这里每次进来的时候,currentSpan都不为null,并且都是一样。
然后发现另一个项目是没问题的,对比发现主要是sleuth版本不同,有问题的版本比较低,是2.0.0.RELEASE,没有问题的版本是2.2.7.RELEASE。初步怀疑是sleuth低版本的bug。
新版本有个很明显的不同,就是每次currentSpan进来都是null,所以会创建新的,这才是合理的。
继续跟tracer.currentSpan()方法,发现这里核心是ThreadLocalCurrentTraceContext类中有个ThreadLocal字段,在线程中缓存trace上下文信息。
这块代码使用了一些设计模式,看着有点绕,核心就在下面这里:
这里是使用了InheritableThreadLocal来进行父子线程变量传递,但是InheritableThreadLocal在线程池中是有个致命问题的,就是只能获取第一次调用的父线程的值,后续再获取的都是第一次的值
详细了解可以看这篇文章:
https://blog.csdn.net/hewenbo111/article/details/80487252
知道问题了那怎么处理呢,一个思路是升级sleuth版本,不够贸然升级还是有点风险的。另一个思路是把InheritableThreadLocal替换成普通的ThreadLocal,那不就行了吗。
添加如下代码,覆盖sleuth中的Bean定义:
@Bean
public CurrentTraceContext slf4jSpanLogger() {
CurrentTraceContext delegate = ThreadLocalCurrentTraceContext.newBuilder().build();
return Slf4jCurrentTraceContext.create(delegate);
}
测试traceId确实不会再重复了。不过还有个问题就是,父子线程的traceId不能传递,异步线程的traceId是随机的~~
那再换个思路,自己把traceId传递过去不就好了吗?
利用@Async异步线程支持TaskDecorator,在这里做文章,代码如下:
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.task.TaskDecorator;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
public class AsyncConfig extends AsyncConfigurerSupport {
@Autowired
ZipkinHelper zipkinHelper;
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(100);
executor.setQueueCapacity(200);
//用来设置线程池关闭的时候等待所有任务都完成再继续销毁其他的Bean
executor.setWaitForTasksToCompleteOnShutdown(true);
//该方法用来设置线程池中任务的等待时间,如果超过这个时候还没有销毁就强制销毁,以确保应用最后能够被关闭,而不是阻塞住。
executor.setAwaitTerminationSeconds(60);
executor.setTaskDecorator(new AsyncTaskDecorator(zipkinHelper));
executor.initialize();
return executor;
}
}
@Slf4j
class AsyncTaskDecorator implements TaskDecorator{
private ZipkinHelper zipkinHelper;
public ExtraParamTaskDecorator(ZipkinHelper zipkinHelper) {
this.zipkinHelper = zipkinHelper;
}
@Override
public Runnable decorate(Runnable runnable) {
return zipkinHelper.wrap(runnable);
}
}
import brave.Span;
import brave.Tracer;
import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.Callable;
@Component
public class ZipkinHelper {
@Autowired
private Tracer tracer;
public Runnable wrap(Runnable runnable) {
Span currentSpan = this.tracer.currentSpan();
return () -> {
try (Tracer.SpanInScope scope = tracer.withSpanInScope(currentSpan)) {
Span span = tracer.nextSpan();
MDC.put(ZipkinKeys.TRACE_ID, span.context().traceIdString());
MDC.put(ZipkinKeys.SPAN_ID, span.context().spanIdString());
MDC.put(ZipkinKeys.PARENT_SPAN_ID, span.context().parentIdString());
span.name("new_thread_started").kind(Span.Kind.SERVER)
.tag("thread_id", Thread.currentThread().getId() + "")
.tag("thread_name", Thread.currentThread().getName() + "");
span.start();
try (Tracer.SpanInScope ws = tracer.withSpanInScope(span)) {
runnable.run();
} catch (RuntimeException | Error e) {
span.error(e);
throw e;
} finally {
span.finish();
}
}
};
}
}
这里AsyncConfig配置是早就有了的,我这里只是在原来基础上修改,ZipkinHelper类是很久前某位大佬写的,经过测试是OK的,我这里直接拿来用,完美解决了问题。
而且因为ZipkinHelper.wrap()是会覆盖InheritableThreadLocal缓存的值的,所以前面的Bean配置也没必要了,改几行代码轻松收工。
补充
如果要自定义AsyncConfig,官方的推荐写法是:
@Configuration
@EnableAutoConfiguration
@EnableAsync
// add the infrastructure role to ensure that the bean gets auto-proxied
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static class CustomExecutorConfig extends AsyncConfigurerSupport {
@Autowired
BeanFactory beanFactory;
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// CUSTOMIZE HERE
executor.setCorePoolSize(7);
executor.setMaxPoolSize(42);
executor.setQueueCapacity(11);
executor.setThreadNamePrefix("MyExecutor-");
// DON'T FORGET TO INITIALIZE
executor.initialize();
return new LazyTraceExecutor(this.beanFactory, executor);
}
}
https://cloud.spring.io/spring-cloud-sleuth/reference/html/#asynchronous-communication