美文网首页
调用链跨线程传递数据分析

调用链跨线程传递数据分析

作者: Yang0503 | 来源:发表于2019-05-12 21:17 被阅读0次

    背景

    新的工作新的开始,先描述下问题的背景,项目中为了解决多数据源聚合快速响应问题,启用线程池并发调用多数据源服务获取数据,做聚合接口对外输出,同时也带来了问题,日志跟踪需要跟踪线程池服务调用及数据处理,为了不影响原有的方法参数列表,采用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<String> context = new TransmittableThreadLocal<String>();
    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依赖

    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>transmittable-thread-local</artifactId>
        <version>2.10.2</version>
    </dependency>
    

    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 还没研究过,后续研究透彻补充进去

    相关文章

      网友评论

          本文标题:调用链跨线程传递数据分析

          本文链接:https://www.haomeiwen.com/subject/arlcaqtx.html