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