天天看點

微服務調用鍊日志追蹤分析

一、技術原理

微服務架構是一個分布式架構,它按業務劃分服務單元,一個分布式系統往往有很多個服務單元。由于服務單元數量衆多,業務的複雜性,如果出現了錯誤和異常,很難去定位。主要展現在,一個請求可能需要調用很多個服務,而内部服務的調用複雜性,決定了問題難以定位。是以微服務架構中,必須實作分布式鍊路追蹤,去跟進一個請求到底有哪些服務參與,參與的順序又是怎樣的,進而達到每個請求的步驟清晰可見,出了問題,很快定位。

舉個例子,在微服務系統中,一個來自使用者的請求,請求先達到前端A(如前端界面),然後通過遠端調用,達到系統的中間件B、C(如負載均衡、網關等),最後達到後端服務D、E,後端經過一系列的業務邏輯計算最後将資料傳回給使用者。對于這樣一個請求,經曆了這麼多個服務,怎麼樣将它的請求過程的資料記錄下來呢?這就需要用到服務鍊路追蹤。

Google開源的 Dapper鍊路追蹤元件,并在2010年發表了論文《Dapper, a Large-Scale Distributed Systems Tracing Infrastructure》,這篇文章是業内實作鍊路追蹤的标杆和理論基礎,具有非常大的參考價值。

中文翻譯參考:http://bigbully.github.io/Dapper-translation/

目前,鍊路追蹤元件有Google的Dapper,Twitter 的Zipkin,以及阿裡的Eagleeye (鷹眼)等,它們都是非常優秀的鍊路追蹤開源元件。

微服務鍊路追蹤系統實作時,需設定一些關鍵節點記錄資訊,鍊路追蹤相關名詞如下:

Span:基本工作單元,發送一個遠端排程任務 就會産生一個Span,Span是一個64位ID唯一辨別的,Trace是用另一個64位ID唯一辨別的,Span還有其他資料資訊,比如摘要、時間戳事件、Span的ID、以及進度ID。

Trace:一系列Span組成的一個樹狀結構。請求一個微服務系統的API接口,這個API接口,需要調用多個微服務,調用每個微服務都會産生一個新的Span,所有由這個請求産生的Span組成了這個Trace。

Annotation:用來及時記錄一個事件的,一些核心注解用來定義一個請求的開始和結束 。這些注解包括以下:

cs - Client Sent -用戶端發送一個請求,這個注解描述了這個Span的開始

sr - Server Received -服務端獲得請求并準備開始處理它,如果将其sr減去cs時間戳便可得到網絡傳輸的時間。

ss - Server Sent (服務端發送響應)–該注解表明請求處理的完成(當請求傳回用戶端),如果ss的時間戳減去sr時間戳,就可以得到伺服器請求的時間。

cr - Client Received (用戶端接收響應)-此時Span的結束,如果cr的時間戳減去cs時間戳便可以得到整個請求所消耗的時間。

一個服務調用過程如下圖所示:

微服務調用鍊日志追蹤分析

二、技術實作

調用方每一次向系統服務發起請求時,會生成這一次調用産生的相關調用鍊日志,生成一個全局的traceId,生成不同節點的span資訊。其中當首個服務生成全局編碼後,放入到header中,基于http傳遞給下級服務(其他模式類似)。下級服務可通過設定Filter過濾器(其他方案也可以),接收鍊路日志編碼,并記錄調用的日志資訊。在将全局編碼繼續傳遞給下級服務。最終本次業務調用完成後,記錄調用日志并清空本次調用鍊産生的全局編碼。簡易流程如下圖所示:

微服務調用鍊日志追蹤分析

調用方請求服務A,進入服務A過濾器;

服務A過濾器判斷請求的header中是否攜帶了TraceId,ParentSpanId,有則使用攜帶的,沒有就自動生成。

過濾器前置部分記錄初始請求的一些資訊,如請求位址,參數,請求時間等;

過濾器轉發請求進入到Service方法;

過濾器後置部分再次記錄Service方法執行完成後的一些資訊,如傳回内容,結束時間;

過濾器前後分别記錄了資訊,組合生成調用鍊路日志;

請求完成後,清空本次産生的TraceId;

服務A調用鍊日志資訊參考:

微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析

View Code

多個服務與單個服務對比,是在不同的微服務裡面分别記錄對應的Trace資訊,Span資訊。同一個調用請求,所有微服務記錄的TraceId一緻,父服務的SpanId為子服務的ParentSpanId。

舉例兩個服務間的調用流程如下:

調用方發起調用,請求服務A,進入服務A過濾器;

服務A過濾器判斷請求的header中是否攜帶了TraceId,ParentSpanId,有則使用攜帶的,沒有就自動生成;

服務A過濾器前置部分記錄初始請求的一些資訊,如請求位址,參數,請求時間等;

服務A過濾器轉發請求進入到Service方法;

服務A的Service方法内部執行部分邏輯後,開始通過中間件調用服務B;

将服務A中已生成的TraceId,ParentSpanId資訊,通過header設定參數(其他類似)的模式傳遞給服務B;

進入服務B過濾器,服務B過濾器擷取header中傳遞過來的TraceId,ParentSpanId;

服務B過濾器前置部分記錄初始請求的一些資訊,如請求位址,參數,請求時間等

服務B過濾器轉發請求進入到Service方法;

服務B過濾器後置部分再次記錄Service方法執行完成後的一些資訊,如傳回内容,結束時間;

服務B過濾器前後分别記錄了資訊,組合生成調用鍊路日志;

服務B基于中間件傳回調用的請求資訊處理結果給服務A;

服務A清空本次接收到的TraceId等編碼資訊。

服務A過濾器後置部分再次記錄Service方法執行完成後的一些資訊,如傳回内容,結束時間;

服務A過濾器前後分别記錄了資訊,組合生成調用鍊路日志;

服務A清空本次請求産生的TraceId。

中間件是否需要記錄Span資訊

上述舉例并未記錄服務的Service方法執行一段時間後,何時通過中間件發起調用其他服務的Span資訊。現實業務中,服務調用經常存在這種情況,服務A中某一個方法,先調用了服務B,擷取到服務B的傳回結果後,後續還又調用了服務C,服務D。此刻若不記錄中間件的Span資訊,在分析部分調用鍊逾時情況時,會難以定位分析。隻能擷取到接受方的接收時間,不知道某一個服務調用時具體的發起時間(如服務D最終接收請求時的時間與最初進入服務A記錄的請求時間相差一分鐘,但這并不能說服務A調用服務D的接口就耗時一分鐘)。

是以,中間件子產品記錄Span資訊也至關重要。比如一個http請求的中間件,可重寫他的Client實作類,記錄開始發起請求和請求完成(類似于Filter)這一段時間的Span資訊。

為什麼每次服務調用完成後,需要清空traceId?

多個請求同時發起時,如何保證調用鍊日志在不同線程中隔離,互不影響?

每一個請求過來時,産生一個獨立的子線程,在這個子線程内部設定對應的traceId,可基于ThreadLocal存儲調用鍊相關資訊,達到子線程資訊隔離的目的。

了解調用鍊資訊基本原理後,自定義編碼實作一套基于traceId的調用鍊追蹤技術方案,需解決如下問題:

全局traceId的生成和清空;

traceId調用鍊路傳遞與追蹤;

traceId基于Filter接收;

Span生成與管理;

調用鍊路日志存儲;

三、技術細節分析

traceId:全局調用鍊日志id編碼,在多個服務調用的一條調用鍊日志中,為同一個日志編碼

spanId:spanId節點的唯一編碼

requestId:本次請求生成的唯一id編碼,在多個服務調用的一條調用鍊日志中,為不同的日志編碼

每一次發起業務調用完成後,需清空本次産生的編碼。同時,不同線程的調用鍊日志應互不影響。故調用鍊資訊可基于MDC技術實作,檢視MDC的實作原理,本質還是基于ThreadLocal實作。本例直接基于ThreadLocal實作,部分僞代碼如下:

微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析

擷取traceId:String traceId = LoggerUtil.traceId();

單次調用過程中存儲traceId:ThreadHolderUtil.setValue(TRACD_ID, traceId );

整個調用完成後,清空整個變量:ThreadHolderUtil.clearValueMap();

調用鍊編碼傳遞主要是一個請求涉及到多個微服務時,一般是從網關(或首個請求的微服務)生成調用鍊編碼後,該編碼在不同微服務中的流轉過程。本文主要介紹Feign和線程池中traceId的鍊路傳遞

參考文檔:基于TraceId鍊路追蹤

Feign傳遞編碼-重寫RequestInterceptor

網上介紹方案大多是通過重寫實作RequestInterceptor接口實作的。參考代碼如下:

微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析

該方案是把調用鍊編碼通過header傳遞給下級服務了,但并沒有記錄Feign處的Span資訊。參考模型如下圖所示:

微服務調用鍊日志追蹤分析

Feign傳遞編碼-重新實作内部調用的 Http Client

擴充方案是需要記錄每一次調用Feign時,記錄Feign處的Span資訊。Feign最終可通過在http發起請求時,調整内部的Http Client擴充實作,達到記錄Span資訊的目的。(整體方案偏複雜,要考慮負載均衡時,池化請求等模式時,都可以記錄資訊)

Feign添加自定義注解

目的是為了記錄Feign在執行方法前後的調用鍊資訊,可采用加入注解,在Feign類上面标記,記錄方法執行前後時的情況。調用鍊資訊還是通過重寫RequestInterceptor實作傳遞給下級服務。

采用Feign調用其他服務,記錄Fegin的Span資訊,可通過方案:(Feign傳遞編碼重寫RequestInterceptor, Feign請求添加注解,組合實作。)

編寫一個注解,并記錄調用方法前後的時間資訊,參考僞代碼:

微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析

線程池傳遞編碼

主線程中記錄的調用鍊資訊通過線程池執行時,子線程會擷取不到主線程的調用鍊資訊(子線程擷取traceId為null)。是以,需要在子線程執行時,主線程向子線程傳遞調用鍊相關編碼資訊。參考文檔:

多線程相關知識:多線程-JUC線程池

Spring 回調方法裝飾器:多線程調用如何傳遞上下文

JDK原生擴充Callable,Runnable:traceId跟蹤請求全流程日志

其他方法:Transmittable ThreadLocal(TTL) 支援緩存線程池的 ThreadLocal

上遊服務向下遊服務發起調用請求時,下遊服務接收到請求時,加入一個基礎過濾器(設定過濾器order值小于其他業務的order值,保證優先執行),擷取上遊服務請求資訊中的調用鍊資訊,擷取出來後,記錄請求Trace日志資訊,并通過ThreadLocal模式,記錄調用鍊資訊。參考實作部分僞代碼如下:

微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析

通過技術原理分析,生成Span的場景為每一個微服務請求開始至請求完成時,記錄一個Span節點資訊。若服務執行過程中,通過中間件調用了其他微服務時,每一次中間件調用時,再記錄一個Span節點資訊(調用多少次,記錄多少個)。

發起一次調用後,會生成Trace請求資訊,Span節點資訊,針對這些日志資訊,可以通過寫入到Log4g2日志中。或者寫入到其他資料庫等系統中做日志資訊存儲,便于後續分析問題。

舉例一個場景:

發起請求,先調用服務A,服務A通過Feign調用一次服務B,整體記錄日志參考如下:

服務A對應traceLog

生成全局traceId: 2bf002c7-c140-4304-9c42-98ec0e359e1a314225。

服務A調用起止時間:1612344583.027557~ 1612344589.716305。

微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析

服務A對應spanLog

服務A本身具備一個span節點資訊。且服務A的spanId,為sub_spans的parentSpanId。因為服務A通過Feign調用了一次服務B,記錄中間件的Span資訊一次。(調用多少次,記錄多少個孩子span節點。)

孩子節點的span資訊,内部的開始請求時間,結束請求時間,小于上級節點的起止時間。

全局traceId: 2bf002c7-c140-4304-9c42-98ec0e359e1a314225。

sub_spans 節點下面,所有相關的子節點,他的parentId為上級span節點的spanId,值為e495b1e3-72e3-4dfc-92ad-8526c1c05e68901528。

微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析

服務B對應traceLog

服務B接收上級的傳入的TraceId,全局編碼:2bf002c7-c140-4304-9c42-98ec0e359e1a314225。

服務B調用起止時間:1612344586.914167~ 1612344587.162829.

服務A通過Feign發起的時間為: 1612344583090733,服務B接收到的請求時間1612344586914167,表明中間件到服務B中還是存在細微的時間差。

微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析

服務B對應spanLog

服務B接收上級的傳入的TraceId,全局編碼:2bf002c7-c140-4304-9c42-98ec0e359e1a314225.

服務B沒有再次調用其他的服務了,故不存在下級sub_spans節點。

服務B節點資訊中的parent_id,為服務A中的孩子節點spanId,值為:6a112df7-762d-4467-aab5-8d4ea8d30e34265554。

微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析

自定義一個調用鍊插件,便于根據項目需求,充分的定制化開發。

結合公司項目的需求,調整調用鍊方案,在調用鍊子產品成熟後,可做為中間件子產品,應用于公司的其他項目;

實作一個調用鍊插件,有利于了解整個調用鍊技術體系的技術關鍵點,技術細節。後續就算切換為其他的成熟的調用鍊産品,當使用中出現問題時,也能從原理層面分析問題。

自定義調用鍊插件在日志管理方面更靈活,便于後期業務日志分析,日志存儲切換方案等可以做出快速調整。

随着Spring體系的更新,中間件的更新,自定義的調用鍊插件受到影響時,也需要更新。存在一定的維護成本。

在更加多元化的日志分析中,如權重管理,比例攔截日志等方面,自定義的插件都需要開發才能支援。

自定義插件的性能,技術實作方案與開發者掌握的技術密切相關。同開源的優秀調用鍊工具對比,肯定還是存在差異,需要開發者更新和替換。 

參考完整實作代碼:https://github.com/wuya11/TraceDemo

運作截圖參考:

微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析
微服務調用鍊日志追蹤分析