引言

在日常項目中,我們常常會遇到線上性能問題,尤其在微服務的場景下,調用鏈錯綜複雜,如何才能快速的定位和解決問題,然後享受美好的夏日時光。枯藤老樹昏鴉,空調WiFi西瓜,葛優同款沙發,夕陽西下,我就往上一趴。豈不美哉?

SkyWalking是一個觀察性分析平臺和應用性能管理系統(APM)。由華爲吳晟等人開發,目前已經是Apache頂級項目。SkyWalking提供分佈式追蹤、服務網格遙測分析、度量聚合和可視化一體化解決方案。能非常方便的定位線上性能問題。

本文將基於SkyWalking 7.0.0,開發Firefly服務端、客戶端的agent插件爲例,與大家分享SkyWalking的插件開發流程。

基本概念

SkyWalking整體架構主要分三層,探針、後端和界面。

探針(Probe)

在應用端收集性能度量數據併發送給後端。SkyWalking支持三種探針:

  • Agent – 基於ByteBuddy字節碼增強技術實現,通過jvm的agent參數加載,並在程序啓動時攔截指定的方法來收集數據。
  • SDK – 程序中顯式調用SkyWalking提供的SDK來收集數據,對應用有侵入。
  • Service Mesh – 通過Service mesh的網絡代理來收集數據。

後端(Backend)

接受探針發送過來的數據,進行度量分析,調用鏈分析和存儲。後端主要分爲兩部分:

  • OAP(Observability Analysis Platform)- 進行度量分析和調用鏈分析的後端平臺,並支持將數據存儲到各種數據庫中,如:ElasticSearch,MySQL,InfluxDB等。
  • OAL(Observability Analysis Language)- 用來進行度量分析的DSL,類似於SQL,用於查詢度量分析結果和警報。

界面(UI)

  • RocketBot UI – SkyWalking 7.0.0 的默認web UI
  • CLI – 命令行界面

這三個模塊的交互流程:

構建SkyWalking

各種編程語言開發的應用(JVM、.Net、Go、Python等)都可以使用SkyWalking探針來收集數據。平時我們用的最多的是基於JVM的技術棧(Java、Scala、Kotlin),對於JVM技術棧,使用agent plugin的方式來收集性能數據有如下優勢:

  • 對應用代碼無侵入。
  • 對應用進程性能開銷小,可以做到線上實時性能分析和調用鏈追蹤。
  • 支持任意粒度性能數據的收集, 比如:API級別、方法級別等。
  • 支持同步、異步、跨線程(Thread)、協程(Coroutine)的性能追蹤。

SkyWalking本身已經提供了足夠多agent plugins,支持了JVM技術棧常用的開源框架和庫(Spring Cloud、各種網絡框架、Dubbo、各種數據庫等),但是JVM的生態系統非常的龐大和活躍,各種開源框架和庫層出不窮,成熟框架的版本更新也非常快,SkyWalking本身不一定能夠及時的追蹤這些新的框架或者新版本的庫,所以有時候需要根據具體的項目或者工具來做定製的plugin開發。

SkyWalking提供的完整的plugin開發和自動測試框架,開發一個新的plugin需要下載SkyWalking源碼進行構建。構建步驟如下:

開發插件

源碼構建成功之後,就可以開始插件開發了。首先我們在源代碼的apm-sdk-plugin目錄下建立自己的plugin module,目錄結構如下:

[skywalking]

|-[apm-sniffer]

|-[apm-sdk-plugin]

    |-[firefly-5.x-plugins]

        |-[firefly-5.x-net-http-client-plugin]

            |-pom.xml

        |-[firefly-5.x-net-http-server-plugin]

            |-pom.xml

    |-pom.xml

定義攔截點

Agent plugin是使用ByteBuddy做字節碼增強,類似於AOP,相當於給目標類增加了一個代理。SkyWalking封裝了相關的操作,形成了自己的開發框架。

上述代碼定義了一個針對firefly http server的攔截器,用來收集firefly http server的性能數據。

enhanceClass

表示需要攔截的類,這裏攔截的是AsyncHttpServerConnectionListener,因爲firefly http server接收到的所有請求都會進入這個listener,只需要對這個類做一個代理就可以拿到所有請求的性能數據了。這裏除了可以按類名去攔截,還可以按照Annotation、前綴等方式去攔截目標類,具體可以參考ClassMatch接口的子類:

定義方法攔截點

InstanceMethodsInterceptPoint用來定義,這個類中對哪些方法進行攔截,這裏直接按方法名攔截onHttpRequestComplete方法。除了按方法名攔截,SkyWalking還封裝了各種方式匹配要攔截方法,這裏就不在贅述,可以參考ElementMatchers類相關源碼或者文檔。

第57行getMethodsInterceptor定義了攔截器的實現類的類名。一會兒我們就要實現這個類來記錄性能數據。

實現攔截器

在實現攔截器之前我們需要了解分佈式追蹤系統中的一些關鍵數據結構。

上圖展示了一個簡單的場景,server3通過遠程調用server2返回結果給用戶。右側圖表中的每一行稱爲一個跨度(Span),在攔截器中,我們就需要構造對應的Span發送給SkyWalking後端,那麼SkyWalking的後端就能夠根據Span的相關信息做調用鏈和度量分析。

跨度(Span)

Span是分佈式追蹤的主要構造塊,表示⼀個獨⽴的⼯作單元,它包含如下狀態記錄:

  • 操作名稱(Operation Name)。
  • 開始和結束時間戳。
  • 自定義標籤(Tags),k-v結構,一般記錄操作的一些信息如:http狀態碼,http方法、ip、port、db.statement等。
  • Logs,k-v結構,用來記錄錯誤日誌。
  • SpanContext,用來記錄trace_id、parent_id等用作調用鏈分析。能夠把一個API中的跨線程、跨進程調用連接起來。

用yml表示span的結構如下:

其中spanType分三種:

  • Entry – 服務端入口,比如Spring的Controller、MQ的Consumer等。
  • Exit – 客戶端遠程調用,如http client、JDBC、redis client,MQ producer等。
  • Local – 本地方法調用,用來記錄本地方法的執行時間。

Span的Context記錄分兩種:

  • ContextCarrier – 用於跨進程傳遞上下文數據。
  • ContextSnapshot – 用於跨線程傳遞上下文數據。

方法攔截器實現

瞭解了Span的基本概念,我們就可以開始實現剛纔定義的方法攔截器AsyncHttpServerConnectionListenerInterceptor了。

首先看一下我們準備去攔截的目標方法onHttpRequestComplete的代碼:

Firefly 5.x是一個基於Coroutine的網絡框架,所有的http request都會進入這個callback然後找到相關的router來處理request,router的handler會運行在一個coroutine上,爲了保持和java的兼容性,異步結果通過CompletableFuture返回。

AsyncHttpServerConnectionListenerInterceptor的實現:

beforeMethod在進入onHttpRequesComplete方法之前執行。

  • 49-57行。創建ContextCarrier,並且判斷http header中是否有需要恢復的上下文數據,如果有則填充到ContextCarrier中。
  • 62-69行。創建一個Entry類型的Span,並用Tags記錄一些請求相關的信息。
  • 71行。捕獲上下文快照,並記錄到Firefly的RoutingContext中,因爲目標方法是異步返回的,我們需要保持當前請求的上下文快照後續可以在另外一個線程中恢復。
  • 72行。設置Span的狀態爲異步執行。

afterMethod方法在onHttpRequestComplete方法執行之後執行。這裏可以拿到該方法的返回值,CompletableFuture,然後在future執行完成之後調用span.asyncFinish()來結束當前span並把span的相關數據發送給SkyWalking的後端OAP平臺。

handleMethodException用來處理目標方法拋異常的情況,這裏直接記錄一下錯誤日誌。

至此Firefly 5.x http server的plugin就開發完成了,http client的插件也類似,就不在贅述,區別就在於client的方法攔截器中是先創建ExitSpan,然後吧ContextCarrier中的信息存儲到http request中發送給server,流程和server是正好相反的。

組件定義配置

開發完所有代碼之後,還需要在ComponentsDefine類和component-libraries.yml配置文件中增加新plugin的id、名稱等信息的配置。

構建插件

由於我們在下載源碼後,已經對SkyWalking做過一次全量構建,開發新的插件之後,就不需要對整個項目進行構建,這裏可以只構建agent模塊即可。運行命令:mvn clean package -Pagent,dist -DskipTests=true

這裏總結一下插件的開發流程如下:

  • apm-sdk-plugin目錄下建立自己的plugin module
  • 定義攔截點(實現ClassInstanceMethodsEnhancePluginDefine)
  • 實現方法攔截器(InstanceMethodsAroundInterceptor)
  • 在ComponentsDefine和component-libraries.yml中配置插件信息
  • 構建agent模塊mvn clean package -Pagent,dist -DskipTests=true

運行插件

插件代碼構建完成之後,我們可以在實際的代碼中加載一下新開發的插件看看運行是否正常。

啓動SkyWalking

構建完成的SkyWalking目錄結構如下:

運行bin/startup.sh啓動SkyWalking的後端服務和UI界面。看logs目錄中的日誌啓動成功後,打開瀏覽器 http://localhost:8080 就可以訪問SkyWalking的UI界面了

啓動應用代碼

Server3應用代碼如下:

啓動的時候要在啓動參數,配置agent路徑、服務名稱、SkyWalking後端地址等。配置如下:

  • -javaagent指定到剛纔構建好的skywalking-agent.jar
  • SW_AGENT_NAME – 指定服務名稱
  • SW_AGENT_COLLECTOR_BACKEND_SERVICES – 指定OAP server 的地址和端口

配置完成後用IDEA運行server3

同樣的方法配置server2並啓動。

瀏覽器訪問 http://localhost:7997/coroutine/hello ,瀏覽器顯示

Server 3 -> server2 coroutine

這個時候我們就可以通過SkyWalking看到剛纔請求的調用棧了。

同時也可以在拓補圖中看到服務間的調用關係。

這樣就說明插件工作正常了。

插件自動測試框架

剛剛我們已經成功的構建並通過啓動實際的應用運行了新開發的SkyWalking plugin。這種手工啓動應用來進行插件的測試效率較低,SkyWalking自身已經提供了一套全自動的插件測試框架,來將剛纔的構建、運行應用、發起測試請求、觀察測試結果等步驟自動化運行。

創建自動測試腳手架

自動測試框架在源碼的test目錄下,目錄結構如下:

[skywalking]

|-[plugin]

|-[scenarios]

|-generator.sh

|-run.sh

|-pom.xml

運行generator.sh命令,根據提示輸入scenarios名稱、類型等信息。完成後,測試腳手架會放到scenarios目錄下,腳手架目錄結構如下。

[plugin-scenario]

|- [bin]

|- startup.sh

|- [config]

|- expectedData.yaml

|- [src]

|- [main]

|- ...

|- [resource]

|- log4j2.xml

|- pom.xml

|- configuration.yaml

|- support-version.list

配置測試場景

創建完腳手架之後需要在configuration.yaml中對測試場景進行一些配置。

  • type - 測試類型,jvm表示應用直接通過main函數啓動,tomcat表示應用在tomcat中加載。Firefly http server直接在main函數啓動所以這裏配置jvm。
  • entryService - 測試請求的URL,測試場景啓動成功後,首先進行健康檢查,檢查成功後會向此URL發起測試請求。
  • healthCheck – 健康檢查URL,場景啓動後會向此URL發送一個HEAD請求。返回200表示檢查成功。
  • startScript – 場景啓動腳本。

開發測試場景

把剛纔運行的server3和server2的代碼移植到測試場景中即可。代碼如下:

配置斷言

在expectedData.yaml中,我們可以配置一些斷言來測試plugin向後端發送的Span數據是否正確來測試plugin的功能。

斷言配置中,數字類型的字段可以用 nq、eq、ge、gt等表達式,字符串類型的字段可以用not null、null、eq等表達式。

運行測試場景

SkyWalking的插件自動測試框架會把我們測試應用打包成docker鏡像然後運行,所以在運行測試場景之前需要在本機啓動docker,然後運行:

./test/pugin/run.sh -f ${scenario_name}

就可以運行剛纔開發的測試場景了。

寫在最後

在錯綜複雜的微服務架構環境下,SkyWalking可以對整個應用的各項性能指標以及調用鏈進行追蹤和分析,能夠幫助我們快速的定位和發現性能瓶頸。本文分享了SkyWalking插件開發的完整步驟和流程,希望對大家有所幫助。

相關文章