您好,登錄后才能下訂單哦!
拓展閱讀:
調用鏈系列(1):解讀UAVStack中的貪吃蛇
調用鏈系列(2):輕調用鏈實現
調用鏈系列(3):如何從零開始捕獲body和header
其實,在調用鏈的繪制過程中,調用鏈上下文的傳遞非常值得關注。各個節點在獲取上層上下文后生成新的上下文并向后傳遞。在傳遞過程中,上下文一旦丟失或出現異常就會導致調用鏈數據缺失,甚至可能會發生斷裂。
本文主要講述UAV中調用鏈上下文傳遞過程中的部分實現細節。
在調用鏈的實現中,主要存在以下幾種調用鏈上下文的傳遞方式:
請求處理前到請求處理后的上下文傳遞;
各個客戶端調用間的上下文傳遞;
各個服務間調用時的上下文傳遞。
在這三種情況中,上下文傳遞過程中所傳遞的信息以及遇到的問題會有所不同。
在請求處理前后的上下文傳遞過程中,需要傳遞的信息一般包括traceID、 spanID、請求開始的時間以及部分請求參數等。相關代碼可能會因為異步執行導致上下文面臨異步線程傳遞的問題。
在客戶端調用間及服務間調用中,需要傳遞的上下文信息一般只包括traceID和spanID。但客戶端調用之間的上下文傳遞可能會遇到跨線程池傳遞的問題,服務間調用則存在跨應用傳遞的問題。
因此,我們把今天所講的上下文傳遞劃分為以下四種場景進行分析:
在同一線程內傳遞
跨線程池傳遞
異步線程傳遞
跨應用傳遞
為了更好地闡述這四種場景,我們假設存在以下業務調用過程:
假設某次請求首先進入服務A,在服務A的業務代碼中發起了一次JDBC請求,訪問了一次數據源;然后又通過httpClient(同步,異步)發起了一次http訪問并返回相應結果。
數字表示所在點存在調用鏈上下文信息的獲取。在大多數的相鄰點之間都會涉及到調用鏈上下文的傳遞。
例如,從2點到3點就是請求前和請求后的上下文傳遞,從3點到4點就是兩次客戶端調用間的上下文傳遞,從4點到5點就是服務間的上下文傳遞。下面我們將在不同的場景下說明各點之間的上下文傳遞過程。
這種場景比較常見,也是最簡單的場景。
假設上述模擬流程中全部為同步操作,業務代碼中不涉及任何的線程池(數據庫連接池不影響)及異步操作,那么服務A中調用鏈的相關代碼均會在同一個線程中執行。
說到這里,想必大家都會想到使用ThreadLocal便可以解決。使用ThreadLocal的確可以解決同線程中的參數共享傳遞問題。在UAV中,一般兩次客戶端調用之間的上下文傳遞都直接使用ThreadLocal(其實并不是原生的ThreadLocal,后文會有所介紹),傳遞過程如下:
但是很多時候,業務代碼中經常會涉及到異步或者提交線程池的操作,此時單單使用ThreadLocal便無法滿足相應的需求。下面我們就來討論有關含有線程池操作和異步請求的上下文傳遞問題。
首次我們來看一下跨線程池上下文傳遞問題。
假設上述的業務場景中在進行JDBC操作時,當前線程僅負責將JDBC操作提交到線程池中,那么此時上下文信息從1點傳遞到2點就會遇到跨線程池的問題,此時使用ThreadLocal無法上下文信息的傳遞。
當然有的同學可能會說用InheritableThreadLocal。但是提交線程和線程池線程本身并不存在父子關系,因此InheritableThreadLocal也是無法完成跨線程池的上下文傳遞的。
為了解決這個問題,我們使用了阿里開源的跨線程池的ThreadLocal組件:transmittable-thread-local(以下簡稱TTL,具體的實現方式有興趣的同學可以去了解下https://github.com/alibaba/transmittable-thread-local)。
通過該組件可以增強ThreadLocal的功能實現跨線程池的傳遞。以下是github中TTL的使用示例:
TransmittableThreadLocal<String> parent =newTransmittableThreadLocal<String>(); 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。
看完上面的跨線程池操作,我們再來看一下異步線程的問題。
假設在上述模擬場景中,我們使用異步HttpClient發送了一個異步的Http請求。由于是異步操作,4點的代碼和7點的代碼(這里7點的上下文是從4點中獲取的屬于請求前后的上下文獲取場景)實際上會在不同的線程中執行,導致7點無法獲取4點放入ThreadLocal中的上下文數據,進而導致調用鏈的數據丟失。
為了解決這個問題,在UAV中我們同時使用了字節碼改寫和動態代理技術。關鍵在于目標劫持函數的選擇,需要能夠獲取到異步線程的回調對象。
下面以異步HttpClient為例介紹UAV中異步線程上下文的傳遞過程。
在異步HttpClient中,我們劫持的是InternalHttpAsyncClient類的execute()方法,該方法聲明如下:
一般情況下,異步的使用方式為傳入一個callback接口對象,在callback中實現相應的異步邏輯;或者使用返回的Future接口對象的get()方法實現一種異步轉同步的操作。
為了能夠在相應的地方獲取到調用鏈的上下文,我們首先通過改寫字節碼的方式,在方法執行前生成調用鏈的上下文信息;然后對FutureCallback接口做動態代理,同時將生成的上下文信息傳入到代理對象中,并替換原來的callback對象。
這樣當異步請求返回調用callback接口時,實際上拿到的是我們的代理對象,此時也就完成了異步線程中上下文的傳遞過程,具體過程如下:
為了支持通過get()方法的異步轉同步操作,在這里我們也對返回的Future接口做了動態代理來完成上下文的傳遞。
說完應用內的上下文傳遞過程,我們來看一下跨應用的上下文傳遞問題。
跨應用的場景也是比較常見的。在這種場景下,上下文傳遞的思路一般是將上下文的信息按照一定的協議反序列化,然后放入到請求的傳輸報文中;在下游服務中劫持請求,獲取其中的信息完成上下文的傳遞。在整個處理過程中,不對應用報文解析造成任何影響。
常見的傳輸協議中如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輸出到業務日志中來實現的。
大家也可以自己在業務代碼中嘗試獲取調用鏈的上下文,將業務數據與調用鏈數據打通,方便數據統計和問題排查。
作者:朱文強
宜信技術學院
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。