0%

Open Tracing(Jaeger) 遭遇多线程

我们知道在Java技术体系中,链路跟踪严重依赖ThreadLocal;因此在多线程的场景下会导致链路跟踪失效.

起因

前几天一位开发同学反馈了一个问题,在链路跟踪UI上看到某个链路Rpc的span数量比实际调用少了很多;我听完第一反映是我们最近升级的SDK出问题了? 根据该同学反馈该接口是一个新接口,上线后一直没关注过链路;我们随即在链路跟踪UI上将多个系统的常用接口都检查了一遍发现一切正常,基本排除了SDK可能引起的问题。接着开始检查开发同学的代码,顺着该请求发现代码里面用到了线程池 ,该同学解释该接口内部需要多次调用多个Rpc接口,为了提升效率所以采用了多线程。

分析

分析opentracing的源码发现tracing相关的的信息保存在io.opentracing.util.ThreadLocalScopeManager的ThreadLocal变量里:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ThreadLocalScopeManager implements ScopeManager {
final ThreadLocal<ThreadLocalScope> tlsScope = new ThreadLocal<ThreadLocalScope>();

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

@Override
public Scope active() {
return tlsScope.get();
}
}

这就可以解释为什么多线程中的Rpc请求没有将相关Tracing信息传递下去;

解决方法

  1. 多线程中ThreadLocal变量的传递:
    关于多线程中ThreadLocal变量的传递我们可以用阿里巴巴的transmittable-thread-local ,所以整个思路就是自己实现一遍ThreadLocalScopeManagerTransmittableThreadLocal替换ThreadLocal,以及跟它关联的类ThreadLocalScope(它里面申明了ThreadLocalScopeManager变量);

  2. 修改Opentrcing里面默认的ThreadLocalScopeManager为用户自定义ThreadLocalScopeManager
    检查Jaeger的Springboot自动配置文件io.opentracing.contrib.java.spring.jaeger.starter.JaegerAutoConfiguration源码, 在构造Bean io.opentracing.Tracer时有用户自定义方法: tracerCustomizers.forEach(c -> c.customize(builder))

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    public class JaegerAutoConfiguration {

    @Autowired(required = false)
    private List<TracerBuilderCustomizer> tracerCustomizers = Collections.emptyList();

    @Bean
    public io.opentracing.Tracer tracer(Sampler sampler,
    Reporter reporter,
    Metrics metrics,
    JaegerConfigurationProperties properties) {

    final JaegerTracer.Builder builder =
    new JaegerTracer.Builder(properties.getServiceName())
    .withReporter(reporter)
    .withSampler(sampler)
    .withTags(properties.determineTags())
    .withMetrics(metrics);

    tracerCustomizers.forEach(c -> c.customize(builder));

    return builder.build();
    }
    .....

    于是,我们只需要自定义TracerBuilderCustomizer调用builder.withScopeManager方法即可

    1
    2
    3
    4
    5
    6
    public class TtlTracerBuilderCustomizer implements TracerBuilderCustomizer {
    @Override
    public void customize(JaegerTracer.Builder builder) {
    builder.withScopeManager(new TracingScopeManager());
    }
    }

    在SpringBoot自动配置文件中构造Bean:

    1
    2
    3
    4
    @Bean
    public List<TracerBuilderCustomizer> tracerCustomizers(){
    return Arrays.asList(new TtlTracerBuilderCustomizer());
    }

    至此,问题解决; 代码参考request-tracing

关于多线程

我不建议在互联网高并发请求接口内部采用多线程;以Java技术体系为例,请求到达系统后一般会有容器(Tomcat之类)或者Rpc框架先接收,然而这些框架本来就是多线程在运行,如果系统本来已经到瓶颈了,即使增加线程也不会提升效率;如果系统需要增加线程,首先我们应该增加容器或者Rpc框架的线程数量;另外如果接口性能差,我们首先应该考虑是Sql的问题还是代码逻辑的问题;或者系统达到了瓶颈是否可以通过增加机器提升性能;如果接口逻辑本身太复杂,可能是我们的方案或者设计有问题,或许可以考虑按照离线请求的模式设计接口。

最后

这个问题虽然没有对业务造成影响,但收集到链路肯定是有问题的;这也暴露出了我们开发过程中的一些问题,对非功能性验证做的不到位,这方面还需要加强。