您好,登錄后才能下訂單哦!
拓展閱讀:
調(diào)用鏈系列(1):解讀UAVStack中的貪吃蛇
調(diào)用鏈系列(2):輕調(diào)用鏈實(shí)現(xiàn)
調(diào)用鏈系列(3):如何從零開(kāi)始捕獲body和header
其實(shí),在調(diào)用鏈的繪制過(guò)程中,調(diào)用鏈上下文的傳遞非常值得關(guān)注。各個(gè)節(jié)點(diǎn)在獲取上層上下文后生成新的上下文并向后傳遞。在傳遞過(guò)程中,上下文一旦丟失或出現(xiàn)異常就會(huì)導(dǎo)致調(diào)用鏈數(shù)據(jù)缺失,甚至可能會(huì)發(fā)生斷裂。
本文主要講述UAV中調(diào)用鏈上下文傳遞過(guò)程中的部分實(shí)現(xiàn)細(xì)節(jié)。
在調(diào)用鏈的實(shí)現(xiàn)中,主要存在以下幾種調(diào)用鏈上下文的傳遞方式:
請(qǐng)求處理前到請(qǐng)求處理后的上下文傳遞;
各個(gè)客戶端調(diào)用間的上下文傳遞;
各個(gè)服務(wù)間調(diào)用時(shí)的上下文傳遞。
在這三種情況中,上下文傳遞過(guò)程中所傳遞的信息以及遇到的問(wèn)題會(huì)有所不同。
在請(qǐng)求處理前后的上下文傳遞過(guò)程中,需要傳遞的信息一般包括traceID、 spanID、請(qǐng)求開(kāi)始的時(shí)間以及部分請(qǐng)求參數(shù)等。相關(guān)代碼可能會(huì)因?yàn)楫惒綀?zhí)行導(dǎo)致上下文面臨異步線程傳遞的問(wèn)題。
在客戶端調(diào)用間及服務(wù)間調(diào)用中,需要傳遞的上下文信息一般只包括traceID和spanID。但客戶端調(diào)用之間的上下文傳遞可能會(huì)遇到跨線程池傳遞的問(wèn)題,服務(wù)間調(diào)用則存在跨應(yīng)用傳遞的問(wèn)題。
因此,我們把今天所講的上下文傳遞劃分為以下四種場(chǎng)景進(jìn)行分析:
在同一線程內(nèi)傳遞
跨線程池傳遞
異步線程傳遞
跨應(yīng)用傳遞
為了更好地闡述這四種場(chǎng)景,我們假設(shè)存在以下業(yè)務(wù)調(diào)用過(guò)程:
假設(shè)某次請(qǐng)求首先進(jìn)入服務(wù)A,在服務(wù)A的業(yè)務(wù)代碼中發(fā)起了一次JDBC請(qǐng)求,訪問(wèn)了一次數(shù)據(jù)源;然后又通過(guò)httpClient(同步,異步)發(fā)起了一次http訪問(wèn)并返回相應(yīng)結(jié)果。
數(shù)字表示所在點(diǎn)存在調(diào)用鏈上下文信息的獲取。在大多數(shù)的相鄰點(diǎn)之間都會(huì)涉及到調(diào)用鏈上下文的傳遞。
例如,從2點(diǎn)到3點(diǎn)就是請(qǐng)求前和請(qǐng)求后的上下文傳遞,從3點(diǎn)到4點(diǎn)就是兩次客戶端調(diào)用間的上下文傳遞,從4點(diǎn)到5點(diǎn)就是服務(wù)間的上下文傳遞。下面我們將在不同的場(chǎng)景下說(shuō)明各點(diǎn)之間的上下文傳遞過(guò)程。
這種場(chǎng)景比較常見(jiàn),也是最簡(jiǎn)單的場(chǎng)景。
假設(shè)上述模擬流程中全部為同步操作,業(yè)務(wù)代碼中不涉及任何的線程池(數(shù)據(jù)庫(kù)連接池不影響)及異步操作,那么服務(wù)A中調(diào)用鏈的相關(guān)代碼均會(huì)在同一個(gè)線程中執(zhí)行。
說(shuō)到這里,想必大家都會(huì)想到使用ThreadLocal便可以解決。使用ThreadLocal的確可以解決同線程中的參數(shù)共享傳遞問(wèn)題。在UAV中,一般兩次客戶端調(diào)用之間的上下文傳遞都直接使用ThreadLocal(其實(shí)并不是原生的ThreadLocal,后文會(huì)有所介紹),傳遞過(guò)程如下:
但是很多時(shí)候,業(yè)務(wù)代碼中經(jīng)常會(huì)涉及到異步或者提交線程池的操作,此時(shí)單單使用ThreadLocal便無(wú)法滿足相應(yīng)的需求。下面我們就來(lái)討論有關(guān)含有線程池操作和異步請(qǐng)求的上下文傳遞問(wèn)題。
首次我們來(lái)看一下跨線程池上下文傳遞問(wèn)題。
假設(shè)上述的業(yè)務(wù)場(chǎng)景中在進(jìn)行JDBC操作時(shí),當(dāng)前線程僅負(fù)責(zé)將JDBC操作提交到線程池中,那么此時(shí)上下文信息從1點(diǎn)傳遞到2點(diǎn)就會(huì)遇到跨線程池的問(wèn)題,此時(shí)使用ThreadLocal無(wú)法上下文信息的傳遞。
當(dāng)然有的同學(xué)可能會(huì)說(shuō)用InheritableThreadLocal。但是提交線程和線程池線程本身并不存在父子關(guān)系,因此InheritableThreadLocal也是無(wú)法完成跨線程池的上下文傳遞的。
為了解決這個(gè)問(wèn)題,我們使用了阿里開(kāi)源的跨線程池的ThreadLocal組件:transmittable-thread-local(以下簡(jiǎn)稱TTL,具體的實(shí)現(xiàn)方式有興趣的同學(xué)可以去了解下https://github.com/alibaba/transmittable-thread-local)。
通過(guò)該組件可以增強(qiáng)ThreadLocal的功能實(shí)現(xiàn)跨線程池的傳遞。以下是github中TTL的使用示例:
TransmittableThreadLocal<String> parent =newTransmittableThreadLocal<String>(); parent.set("value-set-in-parent"); Runnable task =new Task("1"); // 額外的處理,生成修飾了的對(duì)象ttlRunnable Runnable ttlRunnable = TtlRunnable.get(task); executorService.submit(ttlRunnable); // Task中可以讀取,值是"value-set-in-parent" String value = parent.get();
可以看到,想要TTL起作用,就需要將業(yè)務(wù)代碼中的runnable更換為TtlRunnable。為了實(shí)現(xiàn)對(duì)業(yè)務(wù)代碼的零入侵,我們借助javaagent機(jī)制增加了一個(gè)針對(duì)ThreadPoolExecutor等一些Eexecutor的ClassFileTransformer,將提交到線程池中的Runnable和Callable包裝成相應(yīng)的TtlRunnable和TtlCallable,這樣就實(shí)現(xiàn)了在不修改業(yè)務(wù)代碼的情況下完成跨線程池的上下文傳遞。
另外,由于TTL具備ThreadLocal的所有特性,因此UAV的上下文傳遞過(guò)程中用到的ThreadLocal均是TTL。
看完上面的跨線程池操作,我們?cè)賮?lái)看一下異步線程的問(wèn)題。
假設(shè)在上述模擬場(chǎng)景中,我們使用異步HttpClient發(fā)送了一個(gè)異步的Http請(qǐng)求。由于是異步操作,4點(diǎn)的代碼和7點(diǎn)的代碼(這里7點(diǎn)的上下文是從4點(diǎn)中獲取的屬于請(qǐng)求前后的上下文獲取場(chǎng)景)實(shí)際上會(huì)在不同的線程中執(zhí)行,導(dǎo)致7點(diǎn)無(wú)法獲取4點(diǎn)放入ThreadLocal中的上下文數(shù)據(jù),進(jìn)而導(dǎo)致調(diào)用鏈的數(shù)據(jù)丟失。
為了解決這個(gè)問(wèn)題,在UAV中我們同時(shí)使用了字節(jié)碼改寫和動(dòng)態(tài)代理技術(shù)。關(guān)鍵在于目標(biāo)劫持函數(shù)的選擇,需要能夠獲取到異步線程的回調(diào)對(duì)象。
下面以異步HttpClient為例介紹UAV中異步線程上下文的傳遞過(guò)程。
在異步HttpClient中,我們劫持的是InternalHttpAsyncClient類的execute()方法,該方法聲明如下:
一般情況下,異步的使用方式為傳入一個(gè)callback接口對(duì)象,在callback中實(shí)現(xiàn)相應(yīng)的異步邏輯;或者使用返回的Future接口對(duì)象的get()方法實(shí)現(xiàn)一種異步轉(zhuǎn)同步的操作。
為了能夠在相應(yīng)的地方獲取到調(diào)用鏈的上下文,我們首先通過(guò)改寫字節(jié)碼的方式,在方法執(zhí)行前生成調(diào)用鏈的上下文信息;然后對(duì)FutureCallback接口做動(dòng)態(tài)代理,同時(shí)將生成的上下文信息傳入到代理對(duì)象中,并替換原來(lái)的callback對(duì)象。
這樣當(dāng)異步請(qǐng)求返回調(diào)用callback接口時(shí),實(shí)際上拿到的是我們的代理對(duì)象,此時(shí)也就完成了異步線程中上下文的傳遞過(guò)程,具體過(guò)程如下:
為了支持通過(guò)get()方法的異步轉(zhuǎn)同步操作,在這里我們也對(duì)返回的Future接口做了動(dòng)態(tài)代理來(lái)完成上下文的傳遞。
說(shuō)完應(yīng)用內(nèi)的上下文傳遞過(guò)程,我們來(lái)看一下跨應(yīng)用的上下文傳遞問(wèn)題。
跨應(yīng)用的場(chǎng)景也是比較常見(jiàn)的。在這種場(chǎng)景下,上下文傳遞的思路一般是將上下文的信息按照一定的協(xié)議反序列化,然后放入到請(qǐng)求的傳輸報(bào)文中;在下游服務(wù)中劫持請(qǐng)求,獲取其中的信息完成上下文的傳遞。在整個(gè)處理過(guò)程中,不對(duì)應(yīng)用報(bào)文解析造成任何影響。
常見(jiàn)的傳輸協(xié)議中如HTTP協(xié)議,Dubbo的RPC協(xié)議,RocketMQ的MQ協(xié)議等。這些協(xié)議一般會(huì)含有類似于頭信息的結(jié)構(gòu),用于表示本次請(qǐng)求的額外信息。
我們恰好可以利用這一點(diǎn),將上下文信息放入其中傳輸給下游服務(wù),完成上下文的傳遞。
下面我們?nèi)匀灰援惒紿ttpClient來(lái)介紹UAV跨應(yīng)用上下文的傳遞過(guò)程。
之前我們說(shuō)過(guò),在異步HttpClient中,我們劫持的是execute()方法。在這個(gè)方法中,我們可以拿到HttpAsyncRequestProducer接口對(duì)象,具體接口如下:
通過(guò)其中的generateRequest()方法,我們就可以拿到本次請(qǐng)求將要發(fā)送的request對(duì)象,利用request的setHeader()方法,將調(diào)用鏈的上下文信息放入Header中傳入下游。
這里的上下文一般比較簡(jiǎn)單,基本上都是由traceID和spanID的字符串構(gòu)成,傳輸成本也不高。
至于下游服務(wù)中如何解析該上下文,實(shí)際上之前的調(diào)用鏈系列中有談到,就是借助UAV的中間件增強(qiáng)框架(MOF),在服務(wù)端劫持請(qǐng)求對(duì)應(yīng)的request對(duì)象,然后直接從其頭信息中獲取即可。
其他的RPC或者M(jìn)Q等協(xié)議,在UAV中均是采用這種方式完成,只是具體的API和劫持點(diǎn)有所不同。
例如,Dubbo遠(yuǎn)程調(diào)用過(guò)程中使用是其中的RpcContext,而RocketMQ則是放入到了msg的UserProperty中。感興趣的同學(xué)可以到UAVStack(https://github.com/uavorg/uavstack)中查看相關(guān)的源碼。
了解這些上下文的傳遞過(guò)程后,大家便可以基于調(diào)用鏈實(shí)現(xiàn)更為強(qiáng)大的功能。UAV中,調(diào)用鏈和日志關(guān)聯(lián)功能就是通過(guò)劫持日志輸入部分的相關(guān)代碼,獲取調(diào)用鏈上下文,然后將traceID輸出到業(yè)務(wù)日志中來(lái)實(shí)現(xiàn)的。
大家也可以自己在業(yè)務(wù)代碼中嘗試獲取調(diào)用鏈的上下文,將業(yè)務(wù)數(shù)據(jù)與調(diào)用鏈數(shù)據(jù)打通,方便數(shù)據(jù)統(tǒng)計(jì)和問(wèn)題排查。
作者:朱文強(qiáng)
宜信技術(shù)學(xué)院
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。