spring cloud sleuth+zipkin使用异步线程池traceId重复问题

项目中使用了@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上下文信息。

这块代码使用了一些设计模式,看着有点绕,核心就在下面这里:
在这里插入图片描述
跟进到CurrentTraceContext.Default.inheritable()这个方法

这里是使用了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

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值