分布式链路跟踪-跨线程链路跟踪

背景

新的工作新的开始,先描述下问题的背景,项目中为了解决多数据源聚合快速响应问题,启用线程池并发调用多数据源服务获取数据,做聚合接口对外输出,同时也带来了问题,日志跟踪需要跟踪线程池服务调用及数据处理,为了不影响原有的方法参数列表,采用ThreadLocal进行了日志链路追踪,有时候会产生根据ThreadLocal设置的traceId线程池执行后无法快速定位单个服务调用所产生的日志,只能通过上下文去人肉排查,对于程序员是件很苦逼的事情。感谢儿子今天借我用一下他的电脑...

调研步骤:

1.ThreadLocal 为什么没有能传递traceId?
2.jdk本身是否有适应这种场景的线程变量去处理父子线程的问题?
3.是否有开源框架已经解决了这样的问题?
4.总结


1.TheadLocal为啥没能传递traceId?

这个问题很好解释,ThreadLocal本身是线程的内部变量,隶属于线程本身,不能跨线程传输数据。

2.jdk本身是否有适应这种场景的线程变量去处理父子线程的问题?

Thread.class中提供了另外一个变量InheritableThreadLocal,通过分析Thread源码可看到如下代码片段:
Thread类中声明了一个ThreadLocalMap 变量

  /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

在子线程创建时,会将父线程的inheritableThreadLocals赋值到子线程中。

 /**
     * Initializes a Thread.
     *
     * @param g the Thread group
     * @param target the object whose run() method gets called
     * @param name the name of the new Thread
     * @param stackSize the desired stack size for the new thread, or
     *        zero to indicate that this parameter is to be ignored.
     * @param acc the AccessControlContext to inherit, or
     *            AccessController.getContext() if null
     * @param inheritThreadLocals if {@code true}, inherit initial values for
     *            inheritable thread-locals from the constructing thread
     */
    private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
                            .........
         /**
          *   此处为父子线程在初始化线程时赋值的过程
          */
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
                            ........
        /* Set thread ID */
        tid = nextThreadID();
    }

代码块第二个地方Thread#init方法,说明只能在父线程创建子线程时,能够实现父子线程之间通过threadLocal传值。如果像线程池这种有可能复用线程的情形,则会出现无法传递的问题。到此发现问题可能没有想象的简单。

3.是否有开源框架已经解决了这样的问题?

既然问题这么明显,是否有前辈已经解决了呢,如果有的话是否有热心大神开源了,找了一下果然找到了大神的真迹。transmittable-thread-local 阿里开源
TransmittableThreadLocal是阿里开源的库,继承了InheritableThreadLocal,优化了在使用线程池等会池化复用线程的情况下传递ThreadLocal的使用。
简单来说,有个专门的TtlRunnable和TtlCallable包装类,用于读取原Thread的ThreadLocal对象及值并存于Runnable/Callable中,在执行run或者call方法的时候再将存于Runnable/Callable中的ThreadLocal对象和值读取出来,存入调用run或者call的线程中。
TransmittableThreadLocal 调用时序如下:

TransmittableThreadLocal库时序图.jpg
有了TransmittableThreadLocal作为基础,调用链跨线程传递trace信息也不再困难,只需将trace信息均存于TransmittableThreadLocal中,使用异步线程池时使用Ttl相关类修饰即可。


TransmittableThreadLocal的通过修饰线程池的使用方式

省去每次RunnableCallable传入线程池时的修饰,这个逻辑可以在线程池中完成。通过工具类com.alibaba.ttl.threadpool.TtlExecutors完成,有下面的方法:

  • getTtlExecutor:修饰接口Executor
  • getTtlExecutorService:修饰接口ExecutorService
  • getTtlScheduledExecutorService:修饰接口ScheduledExecutorService
    TtlExecutors使用demo.png

使用Java Agent植入修饰代码

Java Agent(Instrumentation)是JDK1.5引入的技术,基于JVM TI机制,使得开发者可以构建一个独立于应用程序的代理(Agent),用来监测和协助运行在 JVM 上的程序,以及替换和修改某些类的定义。开发者可以在一个普通 Java 程序运行时,通过 – javaagent 参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动相应的代理程序,植入自己扩展的修饰代码以实现功能。

// ## 1. 框架上层逻辑,后续流程框架调用业务 ##
TransmittableThreadLocal context = new TransmittableThreadLocal();
context.set("value-set-in-parent");

// ## 2. 应用逻辑,后续流程业务调用框架下层逻辑 ##
ExecutorService executorService = Executors.newFixedThreadPool(3);

Runnable task = new Task("1");
Callable call = new Call("2");
executorService.submit(task);
executorService.submit(call);

// ## 3. 框架下层逻辑 ##
// Task或是Call中可以读取,值是"value-set-in-parent"
String value = context.get();

Maven依赖


    com.alibaba
    transmittable-thread-local
    2.10.2

Java的启动参数配置
在Java的启动参数加上:-javaagent:path/to/transmittable-thread-local-2.x.x.jar。
如果修改了下载的TTL的Jar的文件名(transmittable-thread-local-2.x.x.jar),则需要自己手动通过-Xbootclasspath JVM参数来显式配置:
比如修改文件名成ttl-foo-name-changed.jar,则还加上Java的启动参数:
-Xbootclasspath/a:path/to/ttl-foo-name-changed.jar
Java命令行示例如下:

java -javaagent:path/to/transmittable-thread-local-2.x.x.jar \
    -cp classes \
    com.alibaba.ttl.threadpool.agent.demo.AgentDemo
或是
java -javaagent:path/to/ttl-foo-name-changed.jar \
    -Xbootclasspath/a:path/to/ttl-foo-name-changed.jar \
    -cp classes \
    com.alibaba.ttl.threadpool.agent.demo.AgentDemo

将封装好的TransmittableThreadLocal Jar包放在类目录下的某个文件夹下,例如agent,那么只需在启动参数加入:-javaagent:agent/transmittable-thread-local-xxx.jar即可完成修饰代码的植入。

4.总结

1、引入TransmittableThreadLocalj.jar ,通过TtlExecutors包装现有线程池,使用TransmittableThreadLocal代替InheritableThreadLocal传值,解决线程池复用导致的threadLocal值丢失问题,有一定的工作量。
2、通过java Agent 无侵入解决此问题,工作量小,效率高,需要运维的支持,而且对探针技术未实际使用过,存在一定风险。
ps:对于java Agent 还没研究过,后续研究透彻补充进去

你可能感兴趣的:(分布式链路跟踪-跨线程链路跟踪)