西瓜視頻穩定性治理體系建設一:Tailor 原理及實踐

摘要

Tailor [1]是西瓜視頻 Android 團隊開發的一款內存快照裁剪壓縮工具,廣泛用於字節跳動旗下各大 App 的 OOM 治理及異常排查,收益顯著,在西瓜視頻上更是取得 OOM 降低95%以上的好成績。Tailor 工具現已開源,本文將通過原理、方案和實踐來剖析 Tailor 的相關細節。

背景

穩定性治理一直是個老生常談的話題,過去我們調查穩定性問題只能依靠堆棧和源碼,但很多時候堆棧是遠遠不夠的,對於嚴重依賴的數據只能臨時增加埋點後再次上線蒐集,這期間還會遇到能不能蒐集到和怎麼蒐集的問題,使得我們治理穩定性問題時常常過於侷限和被動。探尋通用、高效、便捷的異常數據蒐集方案一直是我們在治理實踐中努力的方向。

西瓜視頻 Android 團隊基於Java 堆內存快照,搭建了一套相對完整的通用異常數據蒐集系統,能夠在異常發生時,嘗試 dump 出一個相對完整的內存快照文件,必要的時候藉助雲控系統實現快照回撈,最終通過內存快照輔助調查那些棘手的穩定性問題,以提升穩定性問題的治理效率。如何高效、安全、便捷的獲取內存快照,是整個通用異常數據蒐集系統裏關鍵的一環。

內存快照的作用

OOM 治理

我們知道內存快照是治理 OOM 問題及其他類型的內存問題的重要數據源,其重要性可以簡單理解爲:內存快照是解決常規堆內存 OOM 問題的充分條件。同時,內存快照中保存的對象信息和依賴關係也是靜態分析內存泄漏的關鍵,是所有內存泄漏檢測工具的基石。

Crash 治理

內存快照中保存的數據,很多時候也是調查其他類型異常的重要參考,比如 Activity、Fragment、View 狀態等、Framework 層及第三方對象的數據等,必要的時候都可以用來分析異常問題。作爲通用數據大大減少了定向埋點的煩惱,同時也覆蓋了很多無法滲透到的場景。

爲什麼要做裁剪

爲了能在需要的時候爲各類異常提供數據支持,必須要保證數據的穩定,這就需要解決快照在 dump、存儲、傳輸等環節可能存在的問題,不僅包括存儲空間和流量消耗問題,還包括隱私和安全性問題。

存儲

以 LargeHeap 應用爲例,其 OOM 時的內存快照大小通常在512M左右。不經過裁剪的話只能存儲在App的外部存儲空間或者 SDcard 上,這就會遇到空間不足或者 SDcard 的權限問題( Android 11對 App 的外部存儲空間也做了權限限制)。沒有足夠穩定的存儲空間,快照dump成功率將會大幅降低。

傳輸

傳輸過程對於數據的大小是非常敏感的,首當其衝的就是流量消耗問題,其次更小的快照傳輸耗時更少,回傳的成功率也會大幅提升。

隱私

內存快照是虛擬機堆內存數據的完整 copy,這其中可能包含有賬號、Token、聯繫人、密鑰以及其他可能存在隱私的圖片/字符串等,隱私數據是必須要裁剪掉的。

內存快照裁剪方案

目前已知的裁剪方案有種:一種是已開源的 Matrix 方案,另一種是本人在 2018 提出的 hprof 流裁剪方案。Matrix 方案分爲兩步:先通過 Debug.dumpHprofData 直接 dump 出一個完整的 hprof 文件;然後通過分析 hprof 文件分別裁剪掉數據相同的 Bitmap 對象和 String 對象。其裁剪方案存在以下問題:

  • 原生接口直接 dump 出的 hprof 文件過大,存儲問題不好解決;

  • 裁剪過程中涉及到大文件 I/O 和 hprof 分析,對 App 性能的影響不好控制;

  • 裁剪不徹底,快照中仍然存在大量無用數據和可能存在的隱私問題。

hprof 流裁剪則是基於 hprof 文件格式,在 hprof 文件寫入過程中進行裁剪壓縮,存儲空間問題大幅改善,也沒有大文件 I/O 和 hprof 分析過程帶來的性能問題。該方案源於實際的 OOM 治理需求,並參考了hprof 文件的格式定義,相關考慮如下:

治理需要

  • 對於 OOM 問題,只有對象大小和引用關係是必須的,其餘信息都是次要的;

  • OOM 時佔比最大的對象通常是 Bitmap/String,這些對象的數據主要消耗在 byte[] 、 char[];

  • Java 堆中的明文隱私信息通常以 Bitmap/String 的形式存在。

hprof格式

hprof [2]文件有明確定義,其數據組織形式比較簡單,整體可分爲 Header和 Record 數組兩部分,相關數據組織定義如下:

  • Header: "JAVA PROFILE 1.0.2" + indetifiers + timestamp (13byte + 4byte + 8byte)

  • Record:tag + time + length + body(1byte + 4byte + 4byte + byte[$length])

Android 上 dump 出的 hprof 文件雖然也遵循 hprof 格式,但也有所不同,典型的是其一級TAG只有:STRING、LOAD_CLASS、HPROF_TAG_STACK_TRACE、HEAP_DUMP_SEGMENT、HEAP_DUMP_END。HEAP_DUMP_SEGMENT 又分了很多二級 TAG ,這些二級 TAG 中既有標準 hprof 定義的,也有 Android 自定義的 TAG。跟裁剪關係比較緊密的二級 TAG 是 PRIMITIVE_ARRAY_DUMP,存放的是諸如 byte[] 、char[] 、int[]等類型的數據,其格式如圖所示:

通過 hprof 格式定義可以發現,直接裁剪掉所有的 byte[]和 char[]就可以實現對 Bitmap/String 對象的裁剪。同時其數據格式定義中還存在大量的無用數據,比如 timestamp、class-serial-number、stack-serial-number、reserved 數據等等,4byte 的 length/number 等也可以壓縮成 3byte 或者 2byte 等等。

Tailor 裁剪壓縮實現

如果只爲了治理 OOM,可以進行最大化裁剪(如byte[]、char[]、boolean[]、short[]、float[]、int[]、double[]、long[]、hprof格式裁剪),甚至可以只保留 app-heap。但作爲通用異常數據,西瓜視頻也會在必要的時候,通過回撈快照來分析非 OOM 類的異常,甚至是 native 異常。隨着穩定性治理的深入,快照更多是用來分析非 OOM 異常。對於非 OOM 異常,快照的完整性尤爲重要,同時非 OOM 的 crash 堆內存通常較小,最大化裁剪沒有必要,綜合考慮之後 Tailor 只保留了 byte[]、char[] 和 hprof 格式裁剪。

快照 dump 的過程大致可以分爲 5 步,Tailor 只關注 open 和 write 環節。通過 xHook [3](針對 Android 平臺 ELF 的 PLT hook庫)在 native 層 hook dump 過程必然會調用到的 open/write 函數,以此實現對hprof 文件寫入流的代理,進而進行 hprof 流裁剪。爲了進一步降低寫入後的文件體積,Tailor 會在裁剪之後直接進行 zlib 流壓縮。流程大致如下:

  • 調用 Tailor.dumpHprofData() 時,會依次調用 nOpen()、Debug.dumpHprofData()、nClose();

  • nOpen 在 native 層開啓對 int open(const char* __path, int __flags, ...)和 ssize_t write_proxy(int fd, const char*buffer, size_t count) 的 hook 代理;

  • Debug.dumpHprofData 執行中會先調到 open 函數,hook 代理邏輯會過濾出目標文件的 fd;調到 write 函數時 hook 代理邏輯通過 fd 過濾出目標文件的寫入數據進行裁剪壓縮;

  • nClose 邏輯清除之前的 hook 代理。

// isGzip 是否在裁剪之後進行zip壓縮
public static synchronized void dumpHprofData(String fileName, boolean isGzip) throws IOException {
       nOpen(fileName, isGzip);
       Debug.dumpHprofData(fileName);
       nClose();
}

Tailor 裁剪壓縮效果

實際的裁剪效果取決於具體現場,OOM 現場的快照通常比較大(LargeHeap/非 LargeHeap 的差異也很大),非 OOM 的則要小很多,根據西瓜視頻(LargeHeap)的實踐經驗可以得出以下數據:

  • 體積

    • OOM:約 50%可以裁剪壓縮到 10M 以內。

    • 非 OOM:約 60%可以裁剪壓縮到 5M 以內,約 90%可以裁剪壓縮到 10M 以內。

  • 耗時

    • 同原生 dump 耗時相差很小:dump 過程的耗時主要集中在兩次 ProcessHeap 調用和文件寫入上。

  • 穩定性

    • 基本沒有穩定性問題:此開源版本已運行半年以上,未發現有 Tailor 相關的 crash。

西瓜視頻治理實踐

西瓜視頻彙集了短、中、長各類視頻資源,人均使用時長超過 100 分鐘,同時啓動次數又相對較少,導致內存問題會被放大,進而導致治理難度加大。以西瓜視頻 Android v4.0.0 爲例,這期間 Java crash 約爲 6.5‱ 左右(影響用戶的 DAU 佔比),而其中 OOM 就高達 3.4‱,佔比過半 。

OOM 問題常見的治理思路,基本都是通過內存泄漏檢測工具實現的,這類工具的侷限性在於其輸出的是孤立的內存泄漏 case,缺少對整體堆內存影響的評估,無法從泄漏中看出 OOM 的直接原因,還存在比較嚴重的誤報行爲。雖然後續很多新的工具在性能上有所提升,但本質仍屬於 LeakCanary 這一體系,並未從根本上解決工具在治理 OOM 時所面臨的問題。

針對這種情況,西瓜視頻 Android 完全拋棄了線上內存泄漏檢測機制,開發完善了 Tailor 內存快照裁剪壓縮工具,並以此爲核心制定了線上線下同步治理的長效策略:

  • 線下開發、迴歸、Monkey、壓測等環節自動集成 LeakCanary 檢測內存泄漏;

  • 線上 OOM 時通過 Tailor 主動 dump 內存快照,通過回撈快照精準分析 OOM 問題。

該策略將治理防範的重點放到了線下,在建設完善內存問題前置發現能力的同時,也避免線上分析帶來的性能影響和問題規模爆炸。同時,通過 Tailor 內存快照裁剪壓縮工具和回撈機制,使得整個內存優化治理形成閉環,以線下防範爲主,線上精準治理爲輔,線上反哺線下,既可以精準高效地治理線上所有的堆內存 OOM 問題,又補充完善了線下監控體系。

經過一段時間的治理,西瓜視頻 Android 新版本的 Java 堆內存 OOM 問題從 3.5‱ 降低到了 0.03‱,直接降低了兩個數量級,並能長期以極低的人力投入保持下去。與此同時,我們也通過內存快照解決了大量迭代過程中遇到的其他類型的棘手的異常,不僅拓展了穩定性治理的思路,也沉澱出了新的穩定性治理的方法論。

在實際治理過程中,很多時候對於堆棧無法直接定位的問題,我們只能通過分析業務代碼、分析增量代碼、AB 實驗等方法來定位。當第二次遇到時,即便知道原因,仍然需要重複之前繁瑣的調查,治理經驗太過主觀,很難傳承。而通過內存快照則不會有此類問題,快照的分析過程是客觀可重複的,每解決一類問題,後續再遇到是完全可以複製之前的分析過程的。

堆內存 OOM 治理

事實上由於泄漏直接導致的 OOM 問題相對較少,能直接導致 OOM 或者內存水位比較高的,更多的是業務邏輯、緩存邏輯等,這些很多是現有檢測工具覆蓋不到的。事實上對於大多數 App 而言,實際能夠導致 OOM 的原因十分有限,通過快照可以很直觀的發現問題。

上圖所示的是一個 OOM 現場,通過內存泄漏檢測工具,的確可以找出多處泄漏,但都不是導致 OOM 的根本原因。即便修復了這些泄漏,顯然也無法解決此類 OOM 問題(Android 硬件加速邏輯的漏洞,導致大量 byte[] 被 JNI Global 持有而泄漏)。

其他Crash治理

內存快照也是及其重要的數據現場,對於調查數據狀態相關穩定性問題,是極爲重要的數據補充。如果我們在非 OOM 類的 crash 時,也能獲取內存快照,那麼就獲取了crash 時完整的內存狀態。對於堆棧無法定位的問題,可以結合源碼和快照數據來輔助調查問題,以下是三個典型的案例:

案例1

上圖是一個比較常見的 Java crash 堆棧,堆棧中沒有業務相關的信息,對於業務比較複雜的 App,傳統手段很難快速定位。通過快照來調查此問題,就變得非常簡單了:MAT 裏先篩選出 mRecycled 爲 true 的 Bitmap 對象,再通過「Path to GC Roots」即可定位。

案例2

上圖同樣是沒有任何業務信息的 crash 堆棧,通過源碼判斷是在 mListener.onSurfaceTextureAvailable 回調裏間接將 mLayer 置空導致的。由於置空代碼位於 Framework 層,無法通過打點拿到相關 trace。

最後我們通過快照過濾出 crash時的 TextureLayer 實例,發現其 mAttachInfo 爲 null,斷定是在回調裏執行 removeView 而最終導致 mLayer 被置空的,再通過這個 TextureLayer 實例逐層向上找到 mParent 爲 null 的節點,最終找定位到被 remove 的上層節點,進而定位到了問題場景。

案例3

西瓜視頻裏經常遇到 OutOfMemoryError: pthread_create (1040KB stack) failed 類型的 native OOM,有一類明確因爲播放器實例過多,導致 native 層緩存佔用過大而 OOM 的。究竟是播放器自身的問題,還是業務層的問題很難判斷。如果通過針對性的埋點來蒐集數據太被動,而通過快照裏 Java 層 player 對象的狀態、引用關係來判斷則非常簡單,此類問題前後有三類:業務層未正確釋放 player、player 的異步 release 被 block、standard 的 Activity 過多導致 player 實例過多等。

根據西瓜視頻團隊的實踐,大量無法通過堆棧來定位的問題,通過快照則可以很輕鬆的定位到原因。那些即便不能直接定位到問題原因的 case,內存快照也可以提供必要的數據支持。以下是西瓜視頻團隊實踐中總結出的典型的可以通過內存快照來輔助調查的問題分類:

  1. Framework:完整的 Activity、Fragment、View 狀態,完整的 Framework 層數據&狀態。

  2. 插件類問題:有完整的插件&狀態信息、Class、Classloader 及 dex 信息等等。

  3. 務層問題

  4. 第三方問

內存快照裁剪後續

作爲一個立足於提升穩定性治理效率的基礎工具,能在必要的時候爲任何可能的異常提供完整通用的數據現場,是其當仁不讓的職責。能否提供完整的數據現場,核心集中在 dump、存儲、傳輸三個環節,因而 dump 速度、體積、完整性也就成爲了核心優化方向。基於目前的成果,對比 Android 原生的快照 dump 邏輯,內存快照裁剪壓縮工具在以下方面還有進一步的優化提升空間:

裁剪壓縮

在保證快照數據儘可能完整的前提下,怎樣進一步裁剪體積是個矛盾的問題,基於 hprof 格式裁剪仍有很大空間。同時,也可以探索其他高效的裁剪方案,以裁剪掉最終分析時用不到的數據。

裁剪壓縮速度

目前 Tailor 的裁剪壓縮耗時跟原生 dump 耗時比較接近,這是因爲裁剪壓縮的過程耗時有限,主要時間消耗在兩次調用 ProcessHeap 和文件寫入上,直接幹掉第一次調用將會大幅減少整個 dump 耗時。

Dump內存消耗

Android 快照 dump 是在 native 層完成的,dump 過程中每個 Record 都是通過 std::vector<uint8_t> 先緩存之後,再寫入到文件裏的,實際 dump 過程中 Record 可能會非常大,這時就需要額外申請內存。而當我們是在 native 內存不足的 crash 現場,dump Java 堆內存快照時會大概率失敗(大多數 native 內存不足都是由於 Java 層的業務邏輯導致的,必要的時候可以通過 Java 堆現場來定位問題)。如何保證在 native 內存不足時,也能成功 dump 內存快照,是值得思考的。

通過分析相關源碼可以發現,實際只需要 hook 下列接口,就可以代理 Record 的緩存過程,直接對攔截到的數據進行裁剪壓縮,就不會有 Record 緩存空間的問題,也可以提升快照 dump 的速度。

總結

Android 穩定性治理髮展至今,相關的監控工具和方法論並不完善。基於內存快照的治理思路和分析方法,將會是傳統穩定性治理體系的重要補充,其分析過程更客觀、直接、高效,有效減少數據埋點的同時也淨化了代碼邏輯,將內存快照作爲通用異常數據進行蒐集可以一勞永逸。

內存快照裁剪壓縮是通用異常數據蒐集系統裏至關重要的一環,是關係到整個技術路線是否通用的核心和關鍵。Tailor 只是邁開了其中的一小步,方案還有很大的優化空間。開源不是終點,我們希望集思廣益、共同探索完善,在 Android 穩定性治理上走的更快更遠。

接下來我們會逐步開源並詳細介紹西瓜 Android 性能穩定性團隊的其他核心監控體系建設,這其中主要有:Raphael(Native 內存泄漏監控工具)和 Sliver(高性能 Trace 工具)等,覆蓋 Native 內存泄漏檢測、ANR 治理、卡頓治理、基礎性能優化等方向,敬請關注!

相關資料

  1. Tailor 開源地址:

    https://github.com/bytedance/tailor

  2. HPROF 協議:

    http://hg.openjdk.java.net/jdk6/jdk6/jdk/raw-file/tip/src/share/demo/jvmti/hprof/manual.html#mozTocId848088

  3. xHook 鏈接:

    https://github.com/iqiyi/xHook

  4. Android Camera內存問題剖析 (基於 Tailor 和內存快照的實戰案例)

更多分享

基於有限狀態機與消息隊列的三方支付系統補單實踐

UME - 豐富的Flutter調試工具

一例 Go 編譯器代碼優化 bug 定位和修復解析

字節跳動破局聯邦學習:開源Fedlearner框架,廣告投放增效209%


歡迎關注「 字節跳動技術團隊 」

簡歷投遞聯繫郵箱「 [email protected] 」

 點擊閱讀原文,快來加入我們吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章