概述
之前幾篇文章,我們着重介紹了在對SkyWalking
進行二次開發之前的環境搭建問題,因此本篇文章將基於SkyWalking-8.1.0版本,以開發webflux-webclent
插件為例,分享一下對SkyWalking
插件開發以及貢獻PR的過程(PR地址),以其能為大家了解SkyWalking java agent
插件的開發有所幫助。
概念
span
Span
應該是分布式鏈路追蹤系統一個非常重要而且常見的一個概念。最早源自於Google Dapper 的論文--Dapper, a Large-Scale Distributed Systems Tracing Infrastructure,此處給出論文地址,感興趣的小伙伴可以深入學習。簡單來說,Span可以簡單理解成一次服務的調用。只要是一個具有完整時間周期的程序訪問,都可以簡單看做是一個span。
當然SkyWalking
中的span
與論文中的span
類似,但同時也進行了一些擴展,具體來說,在SkyWalking
中span分成以下三種:
- EntrySapn:代表服務提供者,也就是服務器的端點。我們可以簡單理解成服務的提供方,比如對外提供服務的
Webflux
服務或者MQ的消費則都是EntrySpan。 - ExitSpan:代表服務的消費者,比如一個服務的客戶端或者消息隊列的生產者都可以理解成一個ExitSpan。
- LocalSpan:與前邊的EntrySpan和ExitSpan相比,LocalSpan的概念就比較特殊了,它其實本身與遠程服務調用沒有任何關系,它更多的可能指代的的本地的java方法。它的出現可能是為了解決
SkyWalking
監控本地方法調用的問題。比如說,我們想知道某個本地方法的調用請求,我們便可以將該方法定義成一個LocalSpan
,然后OAP
端便可以收集到對應的span信息,然后在web端清晰的展示該方法的調用情況。
上下文載體(ContextCarrier)
因為分布式追蹤,大部分情況下都是跨進程的,因此為了解決跨進程的鏈路綁定問題,SkyWalking
引入了ContextCarrier
的概念。
以下是有關如何在 A -> B 分布式調用中使用 ContextCarrier
的步驟.
- 在客戶端, 創建一個新的空的 ContextCarrier.
- 通過
ContextManager#createExitSpan
創建一個ExitSpan
或者使用ContextManager#inject
來初始化ContextCarrier
. - 將
ContextCarrier
所有信息放到請求頭 (如 HTTP HEAD), 附件(如 Dubbo RPC 框架), 或者消息 (如 Kafka) 中 - 通過服務調用, 將
ContextCarrier
傳遞到服務端.在服務端, 在對應組件的頭部, 附件或消息中獲取ContextCarrier
所有內容. - 通過
ContestManager#createEntrySpan
創建EntrySpan
或者使用ContextManager#extract
來綁定服務端和客戶端.
異步API
因為官方關於插件具體的開發是給了比較詳細的開發文檔的(戳這里)👈,因此我在此時針對API部分就不詳細來說了,我會重點介紹幾個自己在開發webflux webclient
的過程中用到的異步API。
因為此次是對webflux WebClient
來開發插件,許多方法的調用都需要時跨線程的因此,我們需要使用異步API。
簡單來說異步API的使用步驟如下:
- 在原始上下文中調用
AsyncSpan#PrepareForAsync
; - 將該Span傳遞到其他線程,並江灣城相關屬性比如tag、log、status code等屬性進行設置;
- 全部操作就緒之后,可在任意線程中調用
#asyncFinish
結束調用 - 當所有的
#prepareForAsync
完成之后,追蹤上下文就會結束,並一起被會傳到后端服務(根據API的執行次數來進行判斷)。
插件編寫
確定攔截點
插件本身的開發肯定有一定的業務的邏輯,因此我們在開發之前需要根據插件的業務邏輯的確定合適的插入點位置。以webflux-webclient-plugin
為例,因為該插件本質上是為了獲取webclient在發起請求時的調用信息,因此在確定插入點之前我們首先要分析,它整個的調用過程是怎么的。
因此我對WebClient從發起請求到獲得相應整個過程進行了分析,畫出了如下的:
分析整個過程,我發現,無論WebClient調用的是retrieve()
方法還是調用的exchange()方法,最終在發起請求的時候都是通過org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction
的exchange()
方法實際執行異步請求,並且返回一個Mono<ClientResponse>
類型的響應結果。
因此我們考慮使用DefalutExchangeFunction#exchange()
方法作為插入點方法,但僅僅使用這一個插入點是否是足夠的哪?
這里我們先留下一個小小的懸念,在業務代碼開發部分,我會詳細講解自己在開發過程中所遇到的坑!!
攔截與業務代碼開發
在插入點進行確定之后,我們便可以結合業務邏輯開始代碼部分的開發。
定義攔截點
前邊我們已經確定出了具體的攔截點,下邊我們需要在插件目錄中定義出該攔截點。
在創建的插件目錄的Resourse
目錄,定義一個skywalking-plugin.def
文件,添加插件定義:
spring-webflux-5.x-webclient=org.apache.skywalking.apm.plugin.spring.webflux.v5.webclient.define.BodyInserterRequestInstrumentation
在define
目錄下創建Instrumentation
類,以webflux-webclient插件為例,我創建了一個WebFluxWebClientInterceptor
類,用來指定攔截點的具體方法。
具體代碼如下所示:
public class WebFluxWebClientInstrumentation extends ClassEnhancePluginDefine {
private static final String ENHANCE_CLASS = "org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction";
private static final String INTERCEPT_CLASS = "org.apache.skywalking.apm.plugin.spring.webflux.v5.webclient.WebFluxWebClientInterceptor";
@Override
protected ClassMatch enhanceClass() {
return NameMatch.byName(ENHANCE_CLASS);
}
@Override
public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
return new ConstructorInterceptPoint[0];
}
@Override
public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() {
return new InstanceMethodsInterceptPoint[]{
new InstanceMethodsInterceptPoint() {
@Override
public ElementMatcher<MethodDescription> getMethodsMatcher() {
return named("exchange");
}
@Override
public String getMethodsInterceptor() {
return INTERCEPT_CLASS;
}
@Override
public boolean isOverrideArgs() {
return false;
}
}
};
}
@Override
public StaticMethodsInterceptPoint[] getStaticMethodsInterceptPoints() {
return new StaticMethodsInterceptPoint[0];
}
實現對應的Interceptor類
有了插入點之后,我們還需要通過一個類來對插入點方法做具體增強的工作,因此我們定義了一個WebFluxWebClientInstrumentation
類用來做具體的方法增強工作。
具體來說,在該類中做了如下操作:
- 獲取請求參數,收集鏈路信息
- 創建ContextCarrier,為進程的數據管理做准備。
- 創建ExitSpan
- 設置span相關信息,比如請求方法的類型、訪問的url等內容
- 將ContextCarrier對象進行動態傳遞,傳遞給第二個插入點增強類
- 將當前span進行傳遞,便於后續對響應信息進行判斷和設置
具體代碼如下(org.apache.skywalking.apm.plugin.spring.webflux.v5.webclient
包下WebFluxWebClientInterceptor類)。
同時,我在后續調試的過程中發現,只定義一個攔截點是不夠的,因為request只有在初始化的過程中才能被操作,也就是是說,在該位置違法將span的相關信息放置到request的頭文件中,進行跨鏈傳輸。
因此我在org.springframework.http.client.reactive.ClientHttpRequest
的構造方法處也設置了一個攔截點,負責講span信息放置到request中進行跨鏈傳輸。
具體實現如下所示:
public void beforeMethod(EnhancedInstance objInst, Method method, Object[] allArguments, Class<?>[] argumentsTypes,
MethodInterceptResult result) throws Throwable {
ClientHttpRequest clientHttpRequest = (ClientHttpRequest) allArguments[0];
ContextCarrier contextCarrier = (ContextCarrier) objInst.getSkyWalkingDynamicField();
CarrierItem next = contextCarrier.items();
while (next.hasNext()) {
next = next.next();
clientHttpRequest.getHeaders().set(next.getHeadKey(), next.getHeadValue());
編寫測試用例
在插件編寫完成之后,我們還需要編寫一個測試用例用來做CI測試。插件開發的詳細文檔可以參考戳一下👈
此處我就簡單說一下用例的編寫流程。
用例工程是一個獨立的Maven工程。該工程能將工程打包鏡像, 並要求提供一個外部能夠訪問的Web服務用例測試調用鏈追蹤。
用例工程的目錄圖如下所示:
[plugin_testcase]
|__ [config]
| |__ docker-compse.yml
| |__ expectedData.yaml
|__ [src]
| |__ [main]
| | ...
| |__ [resources]
| | ...
|__ pom.xml
|__ testcase.yml
[] = directory
文件用途說明
以下是用例工程中配置文件的說明:
文件 | 用途 |
---|---|
docker-compose.xml | 定義用例的docker運行容器環境 |
expectedData.yaml | 定義用例期望生成的Segment的數據 |
testcase.yml | 定義用例的基本信息,如: 被測試框架名稱、版本號 |
測試用例編寫流程
- 編寫用例代碼
- 打包並測試用例鏡像,確保在沒有加載探針時的用例鏡像能夠正常運行
- 編寫期望數據文件
- 編寫用例配置文件
- 測試用例
Pull Request
提交前的檢查
- 在正式提交以前一定要保證集成測試在本地通過
- 更新插件文檔
- 插件文檔需要更新:Supported-list.md相關插件信息的支持。
- 插件如果為可選插件需要在agent-optional-plugins可選插件文檔中增加對應的描述。
提交PR
在提交PR時,一定要簡要描述個人對插件的設計思路,這樣有助於社區貢獻者討論完成codereview。
申請自動化測試
測試用例編寫完成后,可以申請自動化測試,了解插件的兼容性等問題
在自動化測試完成之后,會有社區成員進行代碼審查,審查通過后,不出意外最終會被合並到主分支上。
自己在開發過程中遇到的問題
-
在搭建開發環境,完成項目的導入工作之后,maven總報錯。
解決方法:增加了國內的多個maven源之后該問題被解決
-
在確定插入點exchange()方法之后,在調試過程中無法被攔截。
解決方法:由於選擇的增強類屬於內部類,因此在
DefaultExchangeFunction
,因此在選擇該類作為內部類的時候應該使用#
進行連接,而不是通過.
。即應該寫成org.springframework.web.reactive.function.client.ExchangeFunctions$DefaultExchangeFunction
的形式。 -
在插件基本功能編寫完成后,OAP端卻無法收集到鏈路信息。
解決方法:使用最新的OAP收集端程序來進行接收。之前一直使用的本地直接編譯的OAP端,發現不能工作,使用編譯好的OAP端代碼版本過低時也不能使用。
-
同一服務的兩個span不能夠串聯。
原因分析:經過分析出現該問題的原因主要是關閉span的時機不對。由於使用的是異步接口,因此在關閉span的時候必須在
doFinally()
方法體內進行關閉。防治span提前關閉,從而出現同一服務的span不能串聯的情況發成解決方法:修改span的關閉時機,在
doFinally()
方法體中執行span.asyncFinish()
方法 -
在本地跑集成測試時,遇到無法啟動docker的問題。
原因分析:根據保存內容發現是測試腳本在啟動docker的過程中出現權限不足的問題,可能是docker的使用需要用的root的權限。
解決方法:將當前用戶增加到docker的用戶組中,從而使得當前的用戶具有操作docker的權限。
-
在集成測試階段出現SegementNotFoundException問題
原因分析:該問題的出現主要是在對Segment進行驗證的過程中,發現Segement丟失的情況發生
解決方法:該問題在經過深入分析之后發現,實際上就是因為在編寫插件的時候,插入點選擇不充分導致的。
exchange
()這個插入點可以用來收集信息,但卻無法用來進行鏈路信息綁定。因此后續重新設計了插件的插入點,增加了第二個插入點,並且在第二個插入點位置進行鏈路的綁定,至此問題解決。