分布式鏈路跟蹤-跨線程鏈路跟蹤

背景

新的工作新的開始,先描述下問題的背景,項目中為了解決多數據源聚合快速響應問題,啟用線程池并發調用多數據源服務獲取數據,做聚合接口對外輸出,同時也帶來了問題,日志跟蹤需要跟蹤線程池服務調用及數據處理,為了不影響原有的方法參數列表,采用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 還沒研究過,后續研究透徹補充進去

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容