調用鏈系列四:調用鏈上下文傳遞

前言java

在調用鏈的實現中,主要存在如下幾種調用鏈上下文的傳遞方式:git

  • 請求處理前到請求處理後的上下文傳遞;
  • 各個客戶端調用間的上下文傳遞;
  • 各個服務間調用時的上下文傳遞。

在這三種狀況中,上下文傳遞過程當中所傳遞的信息以及遇到的問題會有所不一樣。github

  • 在請求處理先後的上下文傳遞過程當中,須要傳遞的信息通常包括traceID、 spanID、請求開始的時間以及部分請求參數等。相關代碼可能會由於異步執行致使上下文面臨異步線程傳遞的問題。
  • 在客戶端調用間及服務間調用中,須要傳遞的上下文信息通常只包括traceID和spanID。但客戶端調用之間的上下文傳遞可能會遇到跨線程池傳遞的問題,服務間調用則存在跨應用傳遞的問題。

所以,咱們把今天所講的上下文傳遞劃分爲如下四種場景進行分析:數據庫

  1. 在同一線程內傳遞
  2. 跨線程池傳遞
  3. 異步線程傳遞
  4. 跨應用傳遞

爲了更好地闡述這四種場景,咱們假設存在如下業務調用過程:架構

假設某次請求首先進入服務A,在服務A的業務代碼中發起了一次JDBC請求,訪問了一次數據源;而後又經過httpClient(同步,異步)發起了一次http訪問並返回相應結果。框架

數字表示所在點存在調用鏈上下文信息的獲取。在大多數的相鄰點之間都會涉及到調用鏈上下文的傳遞。異步

例如,從2點到3點就是請求前和請求後的上下文傳遞,從3點到4點就是兩次客戶端調用間的上下文傳遞,從4點到5點就是服務間的上下文傳遞。下面咱們將在不一樣的場景下說明各點之間的上下文傳遞過程。函數

1.在同一線程內的上下文傳遞網站

這種場景比較常見,也是最簡單的場景。spa

假設上述模擬流程中所有爲同步操做,業務代碼中不涉及任何的線程池(數據庫鏈接池不影響)及異步操做,那麼服務A中調用鏈的相關代碼均會在同一個線程中執行。

說到這裏,想必你們都會想到使用ThreadLocal即可以解決。使用ThreadLocal的確能夠解決同線程中的參數共享傳遞問題。在UAV中,通常兩次客戶端調用之間的上下文傳遞都直接使用ThreadLocal(其實並非原生的ThreadLocal,後文會有所介紹),傳遞過程以下:

可是不少時候,業務代碼中常常會涉及到異步或者提交線程池的操做,此時單單使用ThreadLocal便沒法知足相應的需求。下面咱們就來討論有關含有線程池操做和異步請求的上下文傳遞問題。

2.跨線程池的上下文傳遞

首次咱們來看一下跨線程池上下文傳遞問題。

假設上述的業務場景中在進行JDBC操做時,當前線程僅負責將JDBC操做提交到線程池中,那麼此時上下文信息從1點傳遞到2點就會遇到跨線程池的問題,此時使用ThreadLocal沒法上下文信息的傳遞。

固然有的同窗可能會說用InheritableThreadLocal。可是提交線程和線程池線程自己並不存在父子關係,所以InheritableThreadLocal也是沒法完成跨線程池的上下文傳遞的。

爲了解決這個問題,咱們使用了阿里開源的跨線程池的ThreadLocal組件:transmittable-thread-local(如下簡稱TTL,具體的實現方式有興趣的同窗能夠去了解下https://github.com/alibaba/transmittable-thread-local)。

經過該組件能夠加強ThreadLocal的功能實現跨線程池的傳遞。如下是github中TTL的使用示例:

TransmittableThreadLocal parent =newTransmittableThreadLocal(); parent.set("value-set-in-parent");

Runnable task =new Task("1");

// 額外的處理,生成修飾了的對象ttlRunnable

Runnable ttlRunnable = TtlRunnable.get(task);

executorService.submit(ttlRunnable);

// Task中能夠讀取,值是"value-set-in-parent"

String value = parent.get();

能夠看到,想要TTL起做用,就須要將業務代碼中的runnable更換爲TtlRunnable。爲了實現對業務代碼的零入侵,咱們藉助javaagent機制增長了一個針對ThreadPoolExecutor等一些Eexecutor的ClassFileTransformer,將提交到線程池中的Runnable和Callable包裝成相應的TtlRunnable和TtlCallable,這樣就實現了在不修改業務代碼的狀況下完成跨線程池的上下文傳遞。

另外,因爲TTL具有ThreadLocal的全部特性,所以UAV的上下文傳遞過程當中用到的ThreadLocal均是TTL。

3.異步線程中上下文傳遞

看完上面的跨線程池操做,咱們再來看一下異步線程的問題。

假設在上述模擬場景中,咱們使用異步HttpClient發送了一個異步的Http請求。因爲是異步操做,4點的代碼和7點的代碼(這裏7點的上下文是從4點中獲取的屬於請求先後的上下文獲取場景)實際上會在不一樣的線程中執行,致使7點沒法獲取4點放入ThreadLocal中的上下文數據,進而致使調用鏈的數據丟失。

爲了解決這個問題,在UAV中咱們同時使用了字節碼改寫和動態代理技術。關鍵在於目標劫持函數的選擇,須要可以獲取到異步線程的回調對象。

下面以異步HttpClient爲例介紹UAV中異步線程上下文的傳遞過程。

在異步HttpClient中,咱們劫持的是InternalHttpAsyncClient類的execute()方法,該方法聲明以下:

通常狀況下,異步的使用方式爲傳入一個callback接口對象,在callback中實現相應的異步邏輯;或者使用返回的Future接口對象的get()方法實現一種異步轉同步的操做。

爲了可以在相應的地方獲取到調用鏈的上下文,咱們首先經過改寫字節碼的方式,在方法執行前生成調用鏈的上下文信息;而後對FutureCallback接口作動態代理,同時將生成的上下文信息傳入到代理對象中,並替換原來的callback對象。

這樣當異步請求返回調用callback接口時,實際上拿到的是咱們的代理對象,此時也就完成了異步線程中上下文的傳遞過程,具體過程以下:

爲了支持經過get()方法的異步轉同步操做,在這裏咱們也對返回的Future接口作了動態代理來完成上下文的傳遞。

4.跨應用上下文傳遞

說完應用內的上下文傳遞過程,咱們來看一下跨應用的上下文傳遞問題。

跨應用的場景也是比較常見的。在這種場景下,上下文傳遞的思路通常是將上下文的信息按照必定的協議反序列化,而後放入到請求的傳輸報文中;在下游服務中劫持請求,獲取其中的信息完成上下文的傳遞。在整個處理過程當中,不對應用報文解析形成任何影響。

常見的傳輸協議中如HTTP協議,Dubbo的RPC協議,RocketMQ的MQ協議等。這些協議通常會含有相似於頭信息的結構,用於表示本次請求的額外信息。

咱們剛好能夠利用這一點,將上下文信息放入其中傳輸給下游服務,完成上下文的傳遞。

下面咱們仍然以異步HttpClient來介紹UAV跨應用上下文的傳遞過程。

以前咱們說過,在異步HttpClient中,咱們劫持的是execute()方法。在這個方法中,咱們能夠拿到HttpAsyncRequestProducer接口對象,具體接口以下:

經過其中的generateRequest()方法,咱們就能夠拿到本次請求將要發送的request對象,利用request的setHeader()方法,將調用鏈的上下文信息放入Header中傳入下游。

這裏的上下文通常比較簡單,基本上都是由traceID和spanID的字符串構成,傳輸成本也不高。

至於下游服務中如何解析該上下文,實際上以前的調用鏈系列中有談到,就是藉助UAV的中間件加強框架(MOF),在服務端劫持請求對應的request對象,而後直接從其頭信息中獲取便可。

其餘的RPC或者MQ等協議,在UAV中均是採用這種方式完成,只是具體的API和劫持點有所不一樣。

例如,Dubbo遠程調用過程當中使用是其中的RpcContext,而RocketMQ則是放入到了msg的UserProperty中。感興趣的同窗能夠到UAVStack(https://github.com/uavorg/uavstack)中查看相關的源碼。

總結

瞭解這些上下文的傳遞過程後,你們即可以基於調用鏈實現更爲強大的功能。UAV中,調用鏈和日誌關聯功能就是經過劫持日誌輸入部分的相關代碼,獲取調用鏈上下文,而後將traceID輸出到業務日誌中來實現的。

你們也能夠本身在業務代碼中嘗試獲取調用鏈的上下文,將業務數據與調用鏈數據打通,方便數據統計和問題排查。

官方網站

開源地址

UAVStack已在Github上開放源碼,並提供了安裝部署、架構說明和用戶指南等雙語文檔,歡迎訪問-給星-拉取~~~

掃一掃下方二維碼,關注一個不會讓你失望的公衆號

相關文章
相關標籤/搜索