一口氣說出“分布式追蹤系統(tǒng)”原理!
在微服務架構中,一次請求往往涉及到多個模塊,多個中間件,多臺機器的相互協(xié)作才能完成。
圖片來自 Pexels
這一系列調用請求中,有些是串行的,有些是并行的,那么如何確定這個請求背后調用了哪些應用,哪些模塊,哪些節(jié)點及調用的先后順序?如何定位每個模塊的性能問題?本文將為你揭曉答案。
本文將會從以下幾個方面來闡述:
- 分布式追蹤系統(tǒng)原理及作用
- SkyWalking 的原理及架構設計
- 我司在分布式調用鏈上的實踐
分布式追蹤系統(tǒng)的原理及作用
如何衡量一個接口的性能好壞,一般我們至少會關注以下三個指標:
- 接口的 RT 你怎么知道?
- 是否有異常響應?
- 主要慢在哪里?
單體架構
在初期,公司剛起步的時候,可能多會采用如下單體架構,對于單體架構我們該用什么方式來計算以上三個指標呢?
最容易想到的顯然是用 AOP:
使用 AOP 在調用具體的業(yè)務邏輯前后分別打印一下時間即可計算出整體的調用時間,使用 AOP 來 catch 住異常也可知道是哪里的調用導致的異常。
微服務架構
在單體架構中由于所有的服務,組件都在一臺機器上,所以相對來說這些監(jiān)控指標比較容易實現(xiàn)。
不過隨著業(yè)務的快速發(fā)展,單體架構必然會朝微服務架構發(fā)展,如下圖:
一個稍微復雜的微服務架構
如果有用戶反饋某個頁面很慢,我們知道這個頁面的請求調用鏈是A→C→B→D,此時如何定位可能是哪個模塊引起的問題。
每個服務 Service A,B,C,D 都有好幾臺機器。怎么知道某個請求調用了服務的具體哪臺機器呢?
可以明顯看到,由于無法準確定位每個請求經過的確切路徑,在微服務這種架構下有以下幾個痛點:
- 排查問題難度大,周期長。
- 特定場景難復現(xiàn)。
- 系統(tǒng)性能瓶頸分析較難。
分布式調用鏈就是為了解決以上幾個問題而生,它主要的作用如下:
- 自動采取數(shù)據(jù)。
- 分析數(shù)據(jù)產生完整調用鏈:有了請求的完整調用鏈,問題有很大概率可復現(xiàn)。
- 數(shù)據(jù)可視化:每個組件的性能可視化,能幫助我們很好地定位系統(tǒng)的瓶頸,及時找出問題所在。
通過分布式追蹤系統(tǒng)能很好地定位如下請求的每條具體請求鏈路,從而輕易地實現(xiàn)請求鏈路追蹤,每個模塊的性能瓶頸定位與分析。
分布式調用鏈標準:OpenTracing
知道了分布式調用鏈的作用,那我們來看下如何實現(xiàn)分布式調用鏈的實現(xiàn)及原理。
首先為了解決不同的分布式追蹤系統(tǒng) API 不兼容的問題,誕生了 OpenTracing 規(guī)范。
OpenTracing 是一個輕量級的標準化層,它位于應用程序/類庫和追蹤或日志分析程序之間。
這樣 OpenTracing 通過提供平臺無關,廠商無關的 API,使得開發(fā)人員能夠方便地添加追蹤系統(tǒng)的實現(xiàn)。
說到這大家是否想過 Java 中類似的實現(xiàn)?還記得 JDBC 吧,通過提供一套標準的接口讓各個廠商去實現(xiàn),程序員即可面對接口編程,不用關心具體的實現(xiàn)。
這里的接口其實就是標準,所以制定一套標準非常重要,可以實現(xiàn)組件的可插拔。
接下來我們來看 OpenTracing 的數(shù)據(jù)模型,主要有以下三個:
- Trace:一個完整請求鏈路。
- Span:一次調用過程(需要有開始時間和結束時間)。
- SpanContext:Trace 的全局上下文信息,如里面有 TraceId。
理解這三個概念非常重要,為了讓大家更好地理解這三個概念,我特意畫了一張圖:
如圖示,一次下單的完整請求完整就是一個 Trace,顯然對于這個請求來說,必須要有一個全局標識來標識這一個請求,每一次調用就稱為一個 Span,每一次調用都要帶上全局的 TraceId。
這樣才可把全局 TraceId 與每個調用關聯(lián)起來,這個 TraceId 就是通過 SpanContext 傳輸?shù)?,既然要傳輸顯然都要遵循協(xié)議來調用。
如圖示,我們把傳輸協(xié)議比作車,把 SpanContext 比作貨,把 Span 比作路應該會更好理解一些。
理解了這三個概念,接下來我看看分布式追蹤系統(tǒng)如何采集統(tǒng)一圖中的微服務調用鏈。
我們可以看到底層有一個 Collector 一直在默默無聞地收集數(shù)據(jù),那么每一次調用 Collector 會收集哪些信息呢?
- 全局 trace_id:這是顯然的,這樣才能把每一個子調用與最初的請求關聯(lián)起來。
- span_id:圖中的 0,1,1.1,2,這樣就能標識是哪一個調用。
- parent_span_id:比如 b 調用 d 的 span_id 是 1.1,那么它的 parent_span_id 即為 a 調用 b 的 span_id 即 1,這樣才能把兩個緊鄰的調用關聯(lián)起來。
有了這些信息,Collector 收集的每次調用的信息如下:
根據(jù)這些圖表信息顯然可以據(jù)此來畫出調用鏈的可視化視圖如下:
于是一個完整的分布式追蹤系統(tǒng)就實現(xiàn)了。
以上實現(xiàn)看起來確實簡單,但有以下幾個問題需要我們仔細思考一下:
- 怎么自動采集 Span 數(shù)據(jù):自動采集,對業(yè)務代碼無侵入。
- 如何跨進程傳遞 Context。
- TraceId 如何保證全局唯一。
- 請求量這么多采集會不會影響性能。
接下我來看看 SkyWalking 是如何解決以上四個問題的。
SkyWalking 的原理及架構設計
怎么自動采集 Span 數(shù)據(jù)
SkyWalking 采用了插件化+javaagent 的形式來實現(xiàn)了 Span 數(shù)據(jù)的自動采集。
這樣可以做到對代碼的無侵入性,插件化意味著可插拔,擴展性好(后文會介紹如何定義自己的插件)。
如何跨進程傳遞 Context
我們知道數(shù)據(jù)一般分為 Header 和 Body,就像 HTTPhttp 有 Header 和 Body,RocketMQ 也有 MessageHeader,Message Body。
Body 一般放著業(yè)務數(shù)據(jù),所以不宜在 Body 中傳遞 Context,應該在 Header 中傳遞 Context,如圖示:
Dubbo 中的 Attachment 就相當于 Header,所以我們把 Context 放在 attachment 中,這樣就解決了 Context 的傳遞問題。
小提示:這里的傳遞 Context 流程均是在 Dubbo Plugin 處理的,業(yè)務無感知,這個 Plugin 是怎么實現(xiàn)的呢,下文會分析。
TraceId 如何保證全局唯一
要保證全局唯一 ,我們可以采用分布式或者本地生成的 ID,使用分布式話需要有一個發(fā)號器,每次請求都要先請求一下發(fā)號器,會有一次網絡調用的開銷。
所以 SkyWalking 最終采用了本地生成 ID 的方式,它采用了大名鼎鼎的 Snowflow 算法,性能很高。
Snowflake 算法生成的 id
不過 Snowflake 算法有一個眾所周知的問題:時間回撥,這個問題可能會導致生成的 id 重復。那么 SkyWalking 是如何解決時間回撥問題的呢。
每生成一個 id,都會記錄一下生成 id 的時間(lastTimestamp),如果發(fā)現(xiàn)當前時間比上一次生成 id 的時間(lastTimestamp)還小,那說明發(fā)生了時間回撥,此時會生成一個隨機數(shù)來作為 TraceId。
這里可能就有同學要較真了,可能會覺得生成的這個隨機數(shù)也會和已生成的全局 id 重復,是否再加一層校驗會好點。
這里要說一下系統(tǒng)設計上的方案取舍問題了,首先如果針對產生的這個隨機數(shù)作唯一性校驗無疑會多一層調用,會有一定的性能損耗。
但其實時間回撥發(fā)生的概率很小(發(fā)生之后由于機器時間紊亂,業(yè)務會受到很大影響,所以機器時間的調整必然要慎之又慎),再加上生成的隨機數(shù)重合的概率也很小,綜合考慮這里確實沒有必要再加一層全局惟一性校驗。
對于技術方案的選型,一定要避免過度設計,過猶不及。
全部采集會不會影響性能?
請求這么多,如果對每個請求調用都采集,那毫無疑問數(shù)據(jù)量會非常大,但反過來想一下,是否真的有必要對每個請求都采集呢。
其實沒有必要,我們可以設置采樣頻率,只采樣部分數(shù)據(jù),SkyWalking 默認設置了 3 秒采樣 3 次,其余請求不采樣,如圖示:
這樣的采樣頻率其實足夠我們分析組件的性能了,按 3 秒采樣 3 次這樣的頻率來采樣數(shù)據(jù)會有啥問題呢。
理想情況下,每個服務調用都在同一個時間點(如下圖示)這樣的話每次都在同一時間點采樣確實沒問題。
但在生產上,每次服務調用基本不可能都在同一時間點調用,因為期間有網絡調用延時等,實際調用情況很可能是下圖這樣:
這樣的話就會導致某些調用在服務 A 上被采樣了,在服務 B,C 上不被采樣,也就沒法分析調用鏈的性能,那么 SkyWalking 是如何解決的呢。
它是這樣解決的:如果上游有攜帶 Context 過來(說明上游采樣了),則下游強制采集數(shù)據(jù)。這樣可以保證鏈路完整。
SkyWalking 的基礎架構
SkyWalking 的基礎如下架構,可以說幾乎所有的的分布式調用都是由以下幾個組件組成的:
首先當然是節(jié)點數(shù)據(jù)的定時采樣,采樣后將數(shù)據(jù)定時上報,將其存儲到 ES, MySQL 等持久化層,有了數(shù)據(jù)自然而然可根據(jù)數(shù)據(jù)做可視化分析。
SkyWalking 的性能如何
接下來大家肯定比較關心 SkyWalking 的性能,那我們來看下官方的測評數(shù)據(jù):
圖中藍色代表未使用 SkyWalking 的表現(xiàn),橙色代表使用了 SkyWalking 的表現(xiàn),以上是在 TPS 為 5000 的情況下測出的數(shù)據(jù)。
可以看出,不論是 CPU,內存,還是響應時間,使用 SkyWalking 帶來的性能損耗幾乎可以忽略不計。
接下來我們再來看 SkyWalking 與另一款業(yè)界比較知名的分布式追蹤工具 Zipkin,Pinpoint 的對比(在采樣率為 1 秒 1 個,線程數(shù) 500,請求總數(shù)為 5000 的情況下做的對比)。
可以看到在關鍵的響應時間上 Zipkin(117ms),PinPoint(201ms)遠遜色于 SkyWalking(22ms)!
從性能損耗這個指標上看,SkyWalking 完勝!
再看下另一個指標:對代碼的侵入性如何,ZipKin 是需要在應用程序中埋點的,對代碼的侵入強,而 SkyWalking 采用 javaagent+插件化這種修改字節(jié)碼的方式可以做到對代碼無任何侵入。
除了性能和對代碼的侵入性上 SkyWaking 表現(xiàn)不錯外,它還有以下優(yōu)勢幾個優(yōu)勢:
- 對多語言的支持,組件豐富:目前其支持 Java,.Net Core,PHP,NodeJS,Golang,LUA 語言,組件上也支持 Dubbo,MySQL 等常見組件,大部分能滿足我們的需求。
- 擴展性:對于不滿足的插件,我們按照 SkyWalking 的規(guī)則手動寫一個即可,新實現(xiàn)的插件對代碼無入侵。
我司在分布式調用鏈上的實踐
SkyWalking 在我司的應用架構
由上文可知 SkyWalking 有很多優(yōu)點,那么是不是我們用了它的全部組件了呢,其實不然,來看下其在我司的應用架構:
從圖中可以看出我們只采用了 SkyWalking 的 Agent 來進行采樣,放棄了另外的「數(shù)據(jù)上報及分析」,「數(shù)據(jù)存儲」,「數(shù)據(jù)可視化」三大組件。那為啥不直接采用 SkyWalking 的整套解決方案呢?
因為在接入 SkyWalking 之前我們的 Marvin 監(jiān)控生態(tài)體系已經相對比較完善了。
如果把其整個替換成 SkyWalking,一來沒有必要,Marvin 在大多數(shù)場景下都能滿足我們的需求,二來系統(tǒng)替換成本高,三來如果重新接入用戶學習成本很高。
這也給我們一個啟示:任何產品搶占先機很重要,后續(xù)產品的替換成本會很高,搶占先機,也就是搶占了用戶的心智,這就像微信雖然 UI,功能上制作精良,但在國外照樣干不過 Whatsapp 一樣,因為先機已經沒了。
從另一方面來看,對架構來說,沒有最好的,最有最合適的,結合當前業(yè)務場景去平衡折中才是架構設計的本質。
我司對 SkyWalking 的改造實踐
我司主要作了以下改造和實踐:
- 預發(fā)環(huán)境由于調試需要強制采樣
- 實現(xiàn)更細粒度的采樣
- 日志中嵌入 traceId
- 自研實現(xiàn)了 SkyWalking 插件
①預發(fā)環(huán)境由于調試需要強制采樣
從上文分析可知 Collector 是在后臺定時采樣的,這不挺好的嗎,為啥要實現(xiàn)強制采樣呢。
還是為了排查定位問題,有時線上出現(xiàn)問題,我們希望在預發(fā)上能重現(xiàn),希望能看到這個請求的完整調用鏈,所以在預發(fā)上實現(xiàn)強制采樣很有必要。
所以我們對 Skywalking 的 Dubbo 插件進行了改造,實現(xiàn)強制采樣。
我們在請求的 Cookie 上帶上一個類似 force_flag=true 這樣的鍵值對來表示我們希望強制采樣。
在網關收到這個 Cookie 后,就會在 Dubbo 的 Attachment 里帶上force_flag=true 這個鍵值對。
然后 Skywalking 的 Dubbo 插件就可以據(jù)此來判斷是否是強制采樣了,如果有這個值即強制采樣,如果沒有這個值,則走正常的定時采樣。
②實現(xiàn)更細粒度的采樣?
哈叫更細粒度的采樣。先來看下 Skywalking 默認的采樣方式 ,即統(tǒng)一采樣。
我們知道這種方式默認是 3 秒采樣前 3 次,其他請求都丟棄,這樣的話有個問題。
假設在這臺機器上在 3 秒內有多個 Dubbo,MySQL,Redis 調用,但在如果前三次都是 Dubbo 調用的話,其他像 MySQL,Redis 等調用就采樣不到了。
所以我們對 Skywalking 進行了改造,實現(xiàn)了分組采樣,如下:
就是說 3 秒內進行 3 次 Redis,Dubbo,MySQL 等的采樣,也就避免了此問題。
③日志中如何嵌入 TraceId?
輸出日志中嵌入 TraceId 便于我們排查問題,所以打出出 TraceId 非常有必要,該怎么在日志中嵌入 TraceId 呢?
我們用的是 log4j,這里就要了解一下 log4j 的插件機制了,log4j 允許我們自定義插件來輸出日志的格式,首先我們需要定義日志的格式,在自定義的日志格式中嵌入 %traceId,作為占位符,如下:
然后我們再實現(xiàn)一個 log4j 的插件,如下:
首先 log4j 的插件要定義一個類,這個類要繼承 LogEventPatternConverter 這個類,并且用標準 Plugin 將其自身聲明為 Plugin。
通過 @ConverterKeys 這個注解指定了要替換的占位符,然后在 format 方法里將其替換掉。
這樣在日志中就會出現(xiàn)我們想要的 TraceId,如下:
④我司自研了哪些 Skywalking 插件
SkyWalking 實現(xiàn)了很多插件,不過未提供 Memcached 和 Druid 的插件,所以我們根據(jù)其規(guī)范自研了這兩者的插件:
插件如何實現(xiàn)呢,可以看到它主要由三個部分組成:
- 插件定義類:指定插件的定義類,最終會根據(jù)這里的定義類打包生成 plugin。
- Instrumentation:指定切面,切點,要對哪個類的哪個方法進行增強。
- Interceptor:指定步驟 2 中要在方法的前置,后置還是異常中寫增強邏輯。
可能大家看了還是不懂,那我們以 Dubbo Plugin 來簡單講解一下,我們知道在 Dubbo 服務中,每個請求從 Netty 接收到消息,遞交給業(yè)務線程池處理開始,到真正調用到業(yè)務方法結束,中間經過了十幾個 Filter 的處理:
而 MonitorFilter 可以攔截所有客戶端發(fā)出請求或者服務端處理請求,所以我們可以對 MonitorFilter 作增強。
在其調用 Invoke 方法前,將全局 TraceId 注入到其 Invocation 的 Attachment 中,這樣就可以確保在請求到達真正的業(yè)務邏輯前就已經存在全局 TraceId。
所以顯然我們需要在插件中指定我們要增強的類(MonitorFilter),對其方法(Invoke)做增強,要對這個方法做哪些增強呢?
這就是攔截器(Inteceptor)要做的事,來看看 Dubbo 插件中的 instrumentation(DubboInstrumentation):
我們再看看下代碼中描寫的攔截器(Inteceptor)干了什么事,以下列出關鍵步驟:
首先 beforeMethod 代表在執(zhí)行 MonitorFilter 的 invoke 方法前會調用這里的方法,與之對應的是 afterMethod,代表在執(zhí)行 invoke 方法后作增強邏輯。
其次我們從第 2,3 點可以看到,不管是 Consumer 還是 Provider, 都對其全局 ID 作了相應處理。
這樣確保到達真正的業(yè)務層的時候保證有了此全局 Traceid,定義好 Instrumentation 和 Interceptor 后,最后一步就是在 skywalking.def 里指定定義的類:
- // skywalking-plugin.def 文件
- dubbo=org.apache.skywalking.apm.plugin.asf.dubbo.DubboInstrumentation
這樣打包出來的插件就會對 MonitorFilter 的 Invoke 方法進行增強,在 Invoke 方法執(zhí)行前對期 Attachment 作注入全局 TraceId 等操作,這一切都是靜默的,對代碼無侵入的。
總結
本文由淺入深地介紹了分布式追蹤系統(tǒng)的原理,相信大家對其作用及工作機制有了比較深的理解。
特別需要注意的是,引入某項技巧,一定要結合現(xiàn)有的技術架構作出最合理的選擇,就像 SkyWalking 有四個模塊,我司只采用其 Agent 采樣功能一樣,沒有最好的技術,只有最合適的技術。
通過此文,相信大家應該對 SkyWalking 的實現(xiàn)機制有了比較清晰的認識,文中只是介紹了一下 SkyWalking 的插件實現(xiàn)方式,不過其畢竟是工業(yè)級軟件,要了解其博大精深,還要多讀源碼哦。
作者:碼海
編輯:陶家龍
出處:轉載自公眾號碼海(ID:seaofcode)