SLS:基於 OTel 的移動端全鏈路 Trace 建設思考和實踐
作者:高玉龍(元泊)
首先,我們瞭解一下移動端全鏈路 Trace 的背景:
從移動端的視角來看,一個 App 產品從概念產生,到最終的成熟穩定,產品研發過程中涉及到的研發人員、工程中的程式碼行數、工程架構規模、產品釋出頻率、線上業務問題修復時間等等都會發生比較大的變化。這些變化,給我們在排查問題方面帶來不小的困難和挑戰,業務問題會往往難以復現和排查定位。比如,在產品初期的時候,工程規模往往比較小,業務流程也比較簡單,線上問題往往能很快定位。而等到工程規模比較大的時候,業務流程往往涉及到的模組會比較多,這個時候有些線上問題就會比較難以復現和定位排查。
本文是筆者在 2022 D2 終端技術大會上的分享,希望能給大家帶來一些思考和啟發。
端側問題為什麼很難復現和定位?
線上業務問題為什麼很難復現和排查定位?經過我們的分析,主要是由 4 個原因導致:
- 移動端 & 服務端日誌採集不統一,沒有統一的標準規範來約束資料的採集和處理。
- 端側往往涉及的模組非常多,研發框架也各不相同,程式碼相互隔離,裝置碎片化,網路環境複雜,會導致端側資料採集比較難。
- 從端視角出發,不同框架、系統之間的資料在分析問題時往往獲取比較難,而且資料之間缺少上下文關聯資訊,資料關聯分析不容易。
- 業務鏈路涉及到的業務域往往也會比較多,從端的視角去復現和排查問題,往往需要對應域的同學參與排查,人肉運維成本比較高。
這些問題如何來解決? 我們的思路是四步走:
- 建立統一標準,使用 標準協議 來約束資料的採集和處理。
- 針對不同的平臺和框架,統一資料採集能力。
- 對多系統、多模組產生的資料進行自動上下文關聯分析和處理。
- 我們也基於機器學習,在自動化經驗分析方面做了一些探索。
統一資料採集標準
如何統一標準? 目前行業內也有各種各樣的解決方案,但存在的問題也很明顯:
- 不同方案之間,協議/資料型別不統一;
- 不同方案之間,也比較難以相容/互通。
標準這裡,我們選擇了 OTel,OTel 是 OpenTelemetry 的簡稱,主要原因有兩點:
- OTel 是由雲原生計算基金會(CNCF)主導,它是由 OpenTracing 和 OpenCensus 合併而來,是目前可觀測性領域的準標準協議;
- OTel 對不同語言和資料模型進行了統一,可以同時相容 OpenTracing 和 OpenCensus,它還提供了一個廠商無關的 Collectors,用於接收、處理和匯出可觀測資料。
在我們的解決方案中,所有端的資料採集規範都基於 OTel,資料儲存、處理、分析是基於 SLS 提供的 LogHub 能力進行構建。
端側資料採集的難點
只統一資料協議還不夠,還要解決端側在資料採集方面存在的一些問題。總的來說,端側採集當前面臨 3 個主要的難點:
- 資料串聯難
- 效能保障難
- 不丟資料難
端側研發過程中涉及到的框架、模組往往比較多,業務也有一定的複雜性,存線上程、協程多種非同步呼叫 API,在資料採集過程中,如何解決資料之間的自動串聯問題?移動端裝置碎片化嚴重,系統版本分佈比較散,機型眾多,如何保障多端一致的採集效能?App 使用場景的不確定性也比較大,如何確保採集到的資料不會丟失?
端側資料串聯的難點
我們先來分析一下端側資料自動串聯所面臨的主要問題。
- 在端側資料採集過程中,不僅會採集業務鏈路資料,還會採集各種效能&穩定性監控資料,可觀測資料來源比較多;
- 如果用到其他的研發框架,如 OkHttp、Fresco 等,可能還會採集三方框架的關鍵資料用於網路請求,圖片載入等問題的分析和定位。對於業務研發同學來說,我們往往不會過多的關注這類三方框架技術能力,涉及到這類框架問題的排查時,過程往往比較困難;
- 除此之外,端側幾乎完全非同步呼叫,而且非同步呼叫 API 比較多,如執行緒、協程等,鏈路打通也存在一定的挑戰。
這裡會有幾個共性問題:
- 三方框架的資料如何採集?如何串聯?
- 不同可觀測資料來源之間如何串聯?
- 分佈在不同執行緒、協程之間的資料如何自動串聯?
端側資料自動串聯方案
我們先看下端側資料自動串聯的方案。
在 OTel 協議標準中,是通過 trace 協議來約束不同資料之間的串聯關係。OTel 定義了 trace 資料鏈路中每條資料必須要包含的必要欄位,我們需要確保同一條鏈路中資料的一致性。比如,同一條 trace 鏈路中,trace_id 需要相同;其次,如果資料之間有父子關係,子資料的 parent_id 也需要與父資料的 span_id 相同。
我們知道,不管是 Android 平臺,還是 iOS 平臺,執行緒都是作業系統能夠排程的最小單元。也就是說,我們所有的程式碼,最終都會線上程中被執行。在程式碼被執行過程中,如果我們能把上下文資訊和當前執行緒進行關聯,在程式碼執行時,就能自動獲取當前上下文資訊,這樣就可以解決同一個執行緒內的 trace 資料自動關聯問題。
在 Android 中,可以基於執行緒變數 ThreadLocal 來儲存當前執行緒棧的上下文資訊,這樣可以確保在同一執行緒中採集到的業務資料進行自動關聯。如果是在協程中使用,基於執行緒變數的方案就會存在問題。因為在協程中,協程真實執行的執行緒是不確定的,可能會在協程執行的生命週期內進行執行緒切換,我們需要利用協程排程器和協程 Context 來保持當前上下文的正確性。在協程恢復時,讓關聯的上下文資訊在當前執行緒生效,在協程掛起時,再讓上下文資訊在當前執行緒失效。
在 iOS 中,主要基於 activity tracing 機制來保持上下文資訊的有效性。通過 activity tracing 機制,在一個業務鏈路開始時,會自動建立一個 activity,我們把上下文資訊與 activity 進行關聯。在當前 activity 作用域範圍內,所有產生的資料都會與當前上下文自動關聯。
基於這兩種方案,在產生 Trace 資料時,SDK 會按照 OTel 協議的標準,自動把上下文資訊關聯到當前資料中。最終產生的資料,會以一棵樹的形式進行邏輯關聯,樹的根節點就是 Trace 鏈路的起點。這種方式,不僅支援協程/執行緒內的資料自動關聯,還支援多層級巢狀。
三方框架的資料採集和串聯
針對三方框架的資料採集,我們先看看業內通行的做法,目前主要有兩類:
- 如果三方庫支援攔截器或代理的配置,一般會通過在對應攔截器增加埋點程式碼的方式來實現;
- 如果三方庫對外暴露的介面比較少,一般會通過 Hook 或其他方式增加埋點程式碼,或者不支援對應框架的埋點。
這種做法會存在兩個主要的問題:
- 埋點不完全,拿 OkHttp 來舉例說明,三方 SDK 內部也可能存在對 OkHttp 的依賴,通過攔截器的方式,可能只支援當前業務程式碼的埋點採集,三方 SDK 的網路請求資訊無法被採集到,會導致埋點資訊不完全;
- 可能需要侵入業務程式碼,為了實現對應框架的埋點,需要有一個切入時機,這個切入時機往往需要在對應框架初始化時增加程式碼配置項來實現。
如何解這兩個問題?
我們使用的方案是實現一個 Gradle Plugin,在 Plugin 中對位元組碼進行插樁處理。 我們知道,Android App 在打包的過程中,有個流程會把 .class 檔案轉為 .dex 檔案,在這個過程中,可以通過 transform api 對 class 檔案進行處理。我們是藉助 ASM 的方式來實現 class 檔案的插樁處理。在對位元組碼處理的過程中,需要先找到合適的插樁點,然後注入合適的指令。
這裡拿 OkHttp 的位元組插樁進行舉例:插樁的目標是在 OkHttpClient 呼叫 newCall 方法時,把當前執行緒的上下文資訊關聯到 OkHttp 的 Request 中。在 Transform 過程中,我們先根據 OkHttpClient 的類名過濾出目標 class 檔案,然後再根據 newCall 這個方法名過濾要插樁的方法。接下來,需要在 newCall 方法開始的地方把上下文資訊插入到 request 的 tags 物件中。經過我們的分析,需要在 newCall 方法呼叫開始的時候,插入目的碼。為了方便實現和除錯,我們在擴充套件庫中實現了一個 OkHttp 的輔助工具,在目標位置插入呼叫這個工具的位元組碼,傳入 request 物件就可以了。
插入後的位元組碼會和擴充套件庫進行關聯。這樣就能解決三方框架資料採集和上下文自動關聯的問題。
相對於傳統做法,使用位元組碼插樁的方案,業務程式碼侵入性會更低,埋點對業務程式碼和三方框架都能生效,同時結合擴充套件庫也能完成上下文的自動關聯。
如何確保效能
在可觀測資料採集過程中,會有大量的資料產生,對記憶體、CPU 佔用、I/O 負載都有一定的效能要求。
我們基於 C 對核心部分進行實現,確保多平臺的效能一致性,並從三個方面對效能做了優化:
首先,是對協議化處理過程進行優化。資料協議方面選擇使用 Protocal Buffer 協議,Protocal Buffer 相對 JSON 來說,不僅速度更快,而且更省記憶體空間。在協議的序列化上,我們採用了手動封裝協議的實現,在序列化的過程中,避免了很多臨時記憶體空間的開闢、複製以及無關函式的呼叫。
其次,在記憶體管理方面,我們直接對 SDK 的最大使用記憶體做了可配置的大小限制。記憶體的使用,可以根據業務情況按需配置,避免 SDK 記憶體佔用過大對 App 的穩定性造成影響;其次,還引入了動態記憶體管理機制,記憶體空間的使用按需增加,不會一直佔用 App 的記憶體空間,避免記憶體空間的浪費。同時還提升了字串的處理效能。在字元的處理上,引入了動態字串機制,它可以記錄字串自身的長度,獲取字元長度時,操作複雜度低,而且可以避免緩衝區溢位,同時也可以減少修改字串時帶來的記憶體重分配次數。
最後,在檔案快取管理方面,我們也限制了檔案大小的上限,避免對端裝置儲存空間的浪費。在快取檔案的落盤處理上,我們引入了 Ring File 機制,把快取資料儲存在多個檔案上面,以日誌檔案組的形式對多個檔案進行組裝。整個日誌檔案組以環形陣列的形式,從頭開始寫,寫到末尾再回到頭重新迴圈寫。通過這種方式寫資料,可以減少寫檔案時的隨機 Seek,而且 Ring File 的機制,可以確保單個日誌檔案不會過大,從而儘可能的降低系統 I/O 的負載。除了 Ring File 的機制外,還把斷點儲存、快取清理的邏輯放到了一起聚合執行,減少隨機 Seek。checkpoint 的檔案大小也做了限制,在超出指定大小後會對 checkpoint 檔案進行清理,避免 checkpoint 檔案過大影響檔案讀寫效率。
經過上面的這些優化措施之後,最終 SDK 採集資料的吞吐量提升了 2 倍,記憶體和 CPU 佔用都有明顯的降低。每秒鐘最高可支援 400+條資料的採集。
如何確保日誌不丟失?
效能滿足要求還不夠,還需要確保採集到的資料不能丟失。在 App 的使用過程中,app 經常可能會出現異常崩潰,手機裝置異常重啟,以及網路質量差,網路延時、抖動大的情況。在這類異常場景下,如何確保採集到資料不會丟失?
在採集資料時,我們使用了預寫日誌(WAL)機制,並結合自建網路加速通道來優化這個問題。
- 引入預寫日誌機制的目的是確保寫入到 SDK 的資料,在傳送到伺服器之前,不會因為異常原因而丟失。這個過程的核心是,在資料成功傳送到伺服器之前,先把資料快取在移動裝置的磁碟上,資料傳送成功之後,再移除磁碟上的快取資料。如果因為 App 異常原因,或者裝置重啟導致資料傳送失敗,因為快取的資料還在,SDK 會根據記錄的斷點資訊對資料傳送進度進行恢復。同時預寫日誌機制可以確保資料的寫入和傳送併發執行,不會互相阻塞;
- 在資料傳送之前,還會對多條資料做聚合處理,並通過 lz4 演算法進行壓縮處理,這種做法可以降低資料傳送時的請求次數和網路傳輸流量的消耗。如果資料傳送失敗,還會有重試策略,確保資料至少能成功傳送一次;
- 在資料傳送時,SDK 支援就近接入加速邊緣節點,並通過邊緣節點與 SLS 之間的內部網路加速通道傳輸資料。
經過這三種主要的方式優化之後,資料包的平均大小降低了 2.1 倍,整體的 QPS 平均提升 13 倍,資料整體的傳送成功率達到了 99.3%,網路延時平均下降了 50%。
多系統資料關聯處理
解決了端側資料的串聯和採集效能問題之後,還需要處理多系統之間的資料儲存和關聯分析問題。
資料儲存方面,我們直接基於 SLS LogHub 能力,把相關的資料統一儲存,基於 SLS,日均可以承載 PB 級別的流量,這個吞吐量可以支援移動端可觀測資料的全量採集。
解決了資料的統一儲存問題之後,還需要處理兩個主要的問題。
**第一個問題,**不同系統可觀測資料之間的上下文關聯如何處理?
根據 OTel 協議的約束,我們可以基於 parent_id 和 span_id 來處理根節點、父節點、子節點之間的對映關係。首先,在查詢 Trace 資料鏈路時,會先從 SLS 拉取一定時間段內的所有 Trace 資料。然後按照 OTel 協議的約束,對每條資料進行節點型別的判定。由於多系統的資料可能存在延時,在查詢 Trace 資料鏈路時,有些資料可能還沒有到達。我們還需要對暫時不存在的父節點進行虛擬化處理,確保 Trace 鏈路的準確性。接下來,還需要對節點進行規整處理,把屬於同一個 parent_id 的節點進行聚合,然後再按照每個節點的開始時間進行排序,最終就可以得到一條 trace 鏈路資訊,基於這個鏈路資訊,我們可以還原出系統的呼叫鏈路。
第二個問題, 在進行 Trace 分析時,我們往往還需要從系統視角出發,對不同維度的資料進一步分析。比如,如果想從裝置 ID、App 版本、服務呼叫等不同維度,對 Trace 資料進一步分析,該怎麼做?我們來看一下怎麼解決這個問題。
多系統資料拓撲生成
當我們從系統整體視角對問題進行分析時,所需要的 Trace 資料規模往往會比較大,每分鐘可能有數千萬條資料,而且對資料的時效性要求也比較高。傳統的流處理方式在這種場景下很容易遇到效能瓶頸問題。我們採用的方案是,把流處理問題轉換為批處理問題,把傳統的鏈路處理視角轉換為系統處理視角。經過視角轉化之後,從系統視角來看,解決這個問題最主要的核心,就是如何確定兩個節點之間的關係。
我們看一下具體的處理過程。在批處理上,我們使用了 MapReduce 框架。首先,在資料來源處理階段,我們基於 SLS 的定時分析(ScheduledSQL)能力,對資料進行聚合處理,按照分鐘級從 Trace 資料來源中撈取資料。在 Map 階段,先按照 traceID 進行分組,對分組之後的資料再按照 spanID、parentID 維度對資料進行聚合。然後計算出相關的統計資料,如成功率、失敗率、延時指標等基礎統計資料。在實際的業務使用中,往往還會採集一些和具體業務屬性相關的資料,這部分資料往往會根據業務的不同,有比較大的差異。針對這部分型別的資料,在聚合處理的過程中,支援按照其他維度對結果進行分組。此時會得到兩種中間產物:
- 包含兩個節點關係的聚合資料,我們把這種型別的資料,叫做邊資訊
- 以及未匹配到的原始資料
這兩種中間產物,在 Combine 階段還會再進行聚合處理,最終會得到包含基礎統計指標,以及其他維度的結果資料。
最終產物會包含幾個主要的資訊:
- 邊資訊,可以體現呼叫關係。
- 依賴資訊,可以體現服務依賴關係。
- 還有指標資訊,以及其他資源資訊等。其中,業務屬性相關的資料會體現在資源資訊中。
基於這些產物,我們可以通過對資源、服務等資訊的多個維度篩選,來統計出對應維度的問題分佈和影響鏈路。
自動化問題根因定位探索
接下來向大家介紹下,我們在自動化問題根因定位方向的一些探索。
我們知道,隨著 App 版本的迭代,每次 App 的發版可能會涉及到多個業務的程式碼變更。這些變更,有的經過充分測試,也有的未經過充分測試,或者常規測試方法沒有覆蓋到,對線上業務可能會產生一定的潛在影響,導致部分業務不可用。App 規模越大,業務模式越多,對應的業務資料量,請求鏈路,不確定性就越大。出了問題之後,往往需要多人跨域參與排查,人肉運維成本比較高。
如何在端側問題排查定位方向,通過技術手段進行研發效能的提速? 我們基於機器學習技術做了一些探索。
我們目前的方法是,先對 Trace 源資料進行特徵處理;然後再對特徵進行聚類分析,去找到異常 Trace;最後再基於圖演算法等,對異常 Trace 進行分析,找到異常的起始點。
首先,實時特徵處理階段會讀取 Trace 源資料,對每個 Trace 鏈路按照由底向上找 5 個節點的方式生成一個特徵,並對特徵進行編碼。然後對編碼之後的特徵通過 HDBSCAN 演算法進行層次聚類分析,此時相似的異常會分到同一個組裡面,接下來再從每組異常 Trace 中找出一條典型的異常 Trace。最後,通過圖演算法找到這條異常 Trace 的起點,從而確定當前異常 Trace 可能存在的問題根因。通過這種方式,只要是遵循 OTel 標準協議的資料來源都能夠進行處理。
案例:多端鏈路追蹤
經過對資料處理之後,我們來看下最終的效果。
這裡有一個模擬 Android、iOS、服務端,端到端鏈路追蹤的場景。
我們使用 iOS App 來作為指令的傳送端,Android App 來作為指令的響應端,用來模擬遠端開啟汽車空調的操作。我們從圖上可以看到,iOS 端“開啟車機空調”這個操作觸發後,依次經過了“使用者許可權校驗”、“傳送指令”、“呼叫網路請求”等環節。Android 端收到指令後,依次執行“遠端啟動空調”、“狀態檢查”等環節。從這個呼叫圖可以看得到,Android、iOS、服務端,多端鏈路被串聯到了一起。我們可以從 Android、iOS、服務端的任何一個視角,對呼叫鏈路進行分析。每個操作的耗時,對應服務的請求數,錯誤率,以及服務依賴都能體現出來。
整體架構
接下來,我們來看下整套解決方案的架構:
- 最底層是資料來源,遵循 OTel 協議,各個端對應的 SDK 按照協議規範統一實現;
- 資料儲存層,是直接依託於 SLS LogHub,所有系統採集到的資料統一儲存;
- 再往上是資料處理層,對關鍵指標、Trace 鏈路、依賴關係、拓撲結構、還有特徵等進行了預處理。
最後是上層應用,提供鏈路分析、拓撲查詢、指標查詢、原始日誌查詢,以及根因定位等能力
後續規劃
最後總結下我們後續的規劃:
- 在採集層,會繼續完善外掛、註解等方式的支援,降低業務程式碼的侵入性,提升接入效率
- 在資料側,會豐富可觀測資料來源,後續會支援網路質量、效能等相關資料的採集
- 在應用側,會提供使用者訪問監測、效能分析等能力
最後,我們會把核心技術能力開源,共享社群。
- 為iframe正名,你可能並不需要微前端
- SLS:基於 OTel 的移動端全鏈路 Trace 建設思考和實踐
- 為iframe正名,你可能並不需要微前端
- 釘釘 ANR 治理最佳實踐 | 定位 ANR 不再霧裡看花
- 釘釘 ANR 治理最佳實踐 | 定位 ANR 不再霧裡看花
- 低程式碼多分支協同開發的建設與實踐
- Flutter for Web 首次首屏優化——JS 分片優化
- 全新的 React 元件設計理念 Headless UI
- 我們是如何追逐元宇宙、XR等“概念股”浪潮的?
- 盒馬 iOS Live Activity &“靈動島”配送場景實踐
- ECMAScript 雙月報告:Hashbang Grammer 提案成功進入到 Stage 4
- 如何根治 Script Error.
- 語雀桌面端技術架構實踐
- 語雀桌面端技術架構實踐
- Clang Module 內部實現原理及原始碼分析
- 基於 LowCodeEngine 的除錯能力建設與實踐
- 基於 LowCodeEngine 的除錯能力建設與實踐
- Android Target 31 升級全攻略 —— 記阿里首個超級 App 的坎坷升級之路