抖音 ANR 自動(dòng)歸因平臺(tái)建設(shè)實(shí)踐

抖音作為一個(gè)超大型的應(yīng)用,我們?cè)? ANR 問(wèn)題治理上面臨著很大的挑戰(zhàn)。首先對(duì)于存量問(wèn)題的優(yōu)化,由于缺少有效的歸因手段,一些長(zhǎng)期的疑難問(wèn)題一直難以突破解決,例如長(zhǎng)期位于 Top 1 的 nativePollOnce 問(wèn)題。同時(shí)我們?cè)诜懒踊弦裁媾R很大的壓力,版本快速迭代引入的新增劣化,以及線上變更導(dǎo)致的激增劣化,都需要投入大量的人力去排查定位,無(wú)法在第一時(shí)間快速修復(fù)止損。
ANR 原理簡(jiǎn)介
既然我們要建設(shè)的是 ANR 歸因平臺(tái),首先需要了解下什么是 ANR ?它是 Android 系統(tǒng)定義的一種“應(yīng)用程序無(wú)響應(yīng)”的異常問(wèn)題,目的是為了監(jiān)控發(fā)現(xiàn)應(yīng)用程序是否存在交互響應(yīng)慢或卡死的問(wèn)題。從用戶的視角來(lái)看,發(fā)生 ANR 時(shí)設(shè)備上會(huì)出現(xiàn)提示應(yīng)用無(wú)響應(yīng)的彈窗,甚至在一些機(jī)型上可能就直接閃退了。所以 ANR 與其他崩潰問(wèn)題一樣,是一種會(huì)對(duì)用戶體驗(yàn)造成嚴(yán)重打斷的異常問(wèn)題。
接下來(lái)我們從系統(tǒng)的設(shè)計(jì)原理來(lái)看下為什么會(huì)發(fā)生 ANR ?以一種常見(jiàn)的廣播超時(shí)引起的 ANR 為例,首先系統(tǒng) AMS 服務(wù)會(huì)通過(guò) IPC 方式將一個(gè)有序廣播發(fā)送給應(yīng)用進(jìn)程,并在同時(shí)啟動(dòng)一個(gè)超時(shí)監(jiān)控。應(yīng)用進(jìn)程在 Binder 線程接收到廣播之后,會(huì)將其封裝成一個(gè)消息 Message 加入到主線程的消息隊(duì)列里等待執(zhí)行。正常情況下,廣播消息在應(yīng)用內(nèi)都會(huì)得到及時(shí)響應(yīng),然后通知系統(tǒng) AMS 服務(wù)取消超時(shí)監(jiān)控。但是在一些異常的情況下,如果在系統(tǒng)設(shè)置的超時(shí)到來(lái)之前,目標(biāo)消息還沒(méi)有調(diào)度執(zhí)行完成的話,系統(tǒng)就會(huì)判定響應(yīng)超時(shí)并觸發(fā) ANR。

歸因方案現(xiàn)狀
當(dāng)前業(yè)界針對(duì) ANR 問(wèn)題的歸因手段有哪些?第一種是傳統(tǒng)歸因方案:基于系統(tǒng)生成的 ANR Trace 和 ANR Info 來(lái)定位問(wèn)題原因。這里的 ANR Trace 是在問(wèn)題發(fā)生時(shí)系統(tǒng)通知應(yīng)用自身 dump 采集的各個(gè)線程的堆棧以及狀態(tài)信息,而 ANR Info 中則包括了 ANR 原因、系統(tǒng)以及應(yīng)用進(jìn)程的 CPU 使用率 和 IO 負(fù)載等信息。對(duì)于像下圖中這類(lèi)由于當(dāng)前消息嚴(yán)重耗時(shí)或卡死引起的 ANR,這種系統(tǒng)原生的方案可以幫助我們快速的定位到問(wèn)題堆棧。
但是它也存在一個(gè)明顯的問(wèn)題,從前面的原理分析可以知道,廣播等系統(tǒng)組件消息在加入到主線程之后,是按照它在消息隊(duì)列中的先后順序來(lái)執(zhí)行的,所以有可能是之前的歷史消息存在嚴(yán)重耗時(shí)從而引起的問(wèn)題。這種情況下在 ANR 實(shí)際發(fā)生時(shí)系統(tǒng)抓取的堆棧很有可能就已經(jīng)錯(cuò)過(guò)了問(wèn)題的現(xiàn)場(chǎng),基于這樣的數(shù)據(jù)進(jìn)行歸因得到的結(jié)果也是不準(zhǔn)確的。

第二種是慢消息歸因方案:通過(guò)監(jiān)控主線程消息的執(zhí)行情況,并結(jié)合耗時(shí)消息的采樣抓棧來(lái)定位問(wèn)題原因。這個(gè)方案解決了前面?zhèn)鹘y(tǒng)歸因方案中存在的問(wèn)題,提供了一種更細(xì)粒度的監(jiān)控和歸因能力,可以同時(shí)發(fā)現(xiàn)當(dāng)前和歷史的耗時(shí)消息,以及其中可能存在的耗時(shí)問(wèn)題堆棧。
但這個(gè)方案也同樣存在的一些不足之處, 因?yàn)閷?duì)于像抖音這樣的大型應(yīng)用來(lái)說(shuō),ANR 通常是由于各種復(fù)雜的綜合性因素導(dǎo)致的,包括子線程 / 子進(jìn)程 CPU 搶占、應(yīng)用 / 系統(tǒng)內(nèi)存不足等也都會(huì)對(duì)主線程的執(zhí)行效率造成影響,間接導(dǎo)致主線程整體都變慢了。在這種情況下,主線程的慢消息或堆??赡懿⒉皇菃?wèn)題的根本原因,同樣以此得到的歸因結(jié)果也是不全面的。

所以總結(jié)一下目前的現(xiàn)狀,現(xiàn)有的 ANR 歸因方案存在以下幾個(gè)痛點(diǎn)問(wèn)題:首先是歸因不準(zhǔn)確,歸因結(jié)果難以消費(fèi),不能真正解決問(wèn)題。其次是歸因能力少,對(duì)于復(fù)雜問(wèn)題難以定位根本原因。最后是歸因效率低,人工排查周期長(zhǎng)。
建設(shè)思路
接下來(lái)重點(diǎn)介紹下 ANR 歸因平臺(tái)的建設(shè)思路,平臺(tái)歸因體系主要圍繞以下三個(gè)方向進(jìn)行建設(shè):
- 單點(diǎn)問(wèn)題歸因:首先需要對(duì)單個(gè) ANR 問(wèn)題實(shí)現(xiàn)精準(zhǔn)的歸因,這也是我們整個(gè)歸因體系建設(shè)的重點(diǎn)和基礎(chǔ)。
- 聚合問(wèn)題歸因:其次是線上大數(shù)據(jù)的聚合問(wèn)題歸因,幫助我們聚焦 Top 重點(diǎn)問(wèn)題。
- 劣化問(wèn)題歸因:最后線上灰度以及全量版本劣化問(wèn)題的自動(dòng)歸因,提升新增 / 激增問(wèn)題的解決效率,目前正在建設(shè)中,本次分享就不展開(kāi)介紹了。
單點(diǎn)歸因思路
單點(diǎn) ANR 問(wèn)題的歸因可以分為三個(gè)步驟,首先需要從原理出發(fā)明確 ANR 問(wèn)題區(qū)間;接下來(lái)對(duì)問(wèn)題進(jìn)行粗歸因,也就是一種定性的分析,比如說(shuō)是主線程阻塞卡死、還是 CPU 搶占或是內(nèi)存異常導(dǎo)致的問(wèn)題;最后就是進(jìn)一步進(jìn)行細(xì)歸因,也就是需要定位到具體的問(wèn)題代碼,能實(shí)際指導(dǎo)我們消費(fèi)并解決問(wèn)題。
問(wèn)題區(qū)間
首先從 ANR 問(wèn)題的原理出發(fā),來(lái)分析一下如何大致確定 ANR 問(wèn)題區(qū)間。我們同樣還是以上面的廣播消息超時(shí)引起的 ANR 為例,問(wèn)題產(chǎn)生的時(shí)間順序?yàn)椋簭南到y(tǒng) AMS 服務(wù)發(fā)送有序廣播并啟動(dòng)超時(shí)監(jiān)控開(kāi)始,到應(yīng)用進(jìn)程將該廣播消息加入到主線程消息隊(duì)列,并按照隊(duì)列中的先后順序等待調(diào)度執(zhí)行。當(dāng)系統(tǒng)進(jìn)程的超時(shí)時(shí)間結(jié)束前,對(duì)應(yīng)的廣播消息還沒(méi)有執(zhí)行完成并通知系統(tǒng),系統(tǒng)就會(huì)判定響應(yīng)超時(shí)并觸發(fā) ANR。這里我們可以以 ANR 實(shí)際發(fā)生時(shí)間為結(jié)束點(diǎn),往前回溯對(duì)應(yīng)的超時(shí)時(shí)間(這里不同的 ANR 原因會(huì)對(duì)應(yīng)不同的超時(shí)時(shí)間設(shè)置),也就是后續(xù)需要診斷分析 ANR 問(wèn)題區(qū)間范圍。

粗歸因
在明確 ANR 問(wèn)題的時(shí)間范圍后,我們需要從技術(shù)角度來(lái)拆解下,如何進(jìn)行粗歸因的定性分析。從上面的原理分析可以推導(dǎo)出,引起 ANR 的根本原因就是系統(tǒng)組件消息(包括 input 事件)在應(yīng)用側(cè)沒(méi)有得到及時(shí)的執(zhí)行。而我們知道這些關(guān)鍵目標(biāo)消息都是在主線程中進(jìn)行消費(fèi)處理的,所以這里的關(guān)鍵點(diǎn)就是 ANR 區(qū)間內(nèi)之前的這些消息為什么執(zhí)行耗時(shí) ?從系統(tǒng)的角度來(lái)看,所有代碼邏輯的執(zhí)行可以分為 On-CPU 和 Off-CPU 兩種情況:
- On-CPU:對(duì)應(yīng) Running 狀態(tài),即當(dāng)前任務(wù)正在占用 CPU 資源進(jìn)行計(jì)算處理。已知 計(jì)算耗時(shí) = 計(jì)算量 / 計(jì)算速度,這里計(jì)算速度會(huì)受到 CPU 硬件本身的限制,比如 CPU 核心頻率以及當(dāng)前運(yùn)行在大核或小核上。另一個(gè)跟應(yīng)用自身關(guān)系比較緊密的就是計(jì)算量,例如主線程在執(zhí)行 CPU 密集型的操作,比如 JSON 序列化 / 反序列化,或是在處理大量的高頻業(yè)務(wù)消息。
- Off-CPU:包括 Runnable 和 Sleep 兩種狀態(tài)。Runnable 代表任務(wù)所需的資源已就緒,正在 CPU 運(yùn)行隊(duì)列上等待調(diào)度執(zhí)行,這里除了受到系統(tǒng)本身的調(diào)度策略的影響之外,也跟當(dāng)前同樣已就緒并等待調(diào)度的任務(wù)數(shù)量有關(guān)。如果子線程或子進(jìn)程有很多 CPU 耗時(shí)任務(wù)在等待執(zhí)行的話,因?yàn)榭偟挠?jì)算資源是有限的,互相之間頻繁的搶占也會(huì)影響主線程的執(zhí)行效率。Sleep 代表任務(wù)在阻塞等待資源,比如等待 Lock、IO、同步 Binder 以及內(nèi)存 Block GC 等。通常情況下我們應(yīng)該避免在主線程發(fā)生這類(lèi) Block 阻塞問(wèn)題,同時(shí)這也是比較常見(jiàn)的一類(lèi)解決卡頓或 ANR 的優(yōu)化手段。

下面我們來(lái)看下第一種主線程異常消息直接引起的 ANR,它可能是由于當(dāng)前消息嚴(yán)重耗時(shí)或卡死導(dǎo)致的,也可能是由于之前的一個(gè)或多個(gè)歷史耗時(shí)消息引起的 ANR。


第二種就是后臺(tái)任務(wù) CPU 資源搶占引起的ANR,它會(huì)間接影響主線程的執(zhí)行效率。從下圖中可以看到在子線程 CPU 負(fù)載變高之后,主線程的整體性能開(kāi)始下降變慢,這種情況下就會(huì)更容易發(fā)生 ANR。最典型的就是在冷啟動(dòng)的場(chǎng)景下,通過(guò)降級(jí)或打散 CPU 耗時(shí)的后臺(tái)任務(wù),我們已經(jīng)驗(yàn)證可以有效的降低 ANR 率以及縮短啟動(dòng)首刷耗時(shí)。

最后一種內(nèi)存等資源異常問(wèn)題,例如虛擬機(jī) Java 內(nèi)存不足時(shí),GC 線程就會(huì)開(kāi)始變得活躍并進(jìn)行頻繁 GC,這同樣也會(huì)搶占主線程 CPU 資源或 Block GC 等待,從而導(dǎo)致 ANR 問(wèn)題的發(fā)生。對(duì)于抖音這樣的視頻類(lèi)大型應(yīng)用,線上內(nèi)存問(wèn)題導(dǎo)致的卡頓或 ANR 問(wèn)題的占比較高,目前也在專(zhuān)項(xiàng)治理優(yōu)化中。

基于以上的演繹推理,總結(jié)一下我們的歸因思路主要包括以下幾類(lèi):第一,主線程本身的異常問(wèn)題,例如存在嚴(yán)重的阻塞等待或者 CPU 繁忙等問(wèn)題。第二,后臺(tái)任務(wù)搶占 CPU 資源導(dǎo)致的異常問(wèn)題。最后,內(nèi)存 / IO 等系統(tǒng)資源不足導(dǎo)致的異常問(wèn)題,目前正在探索中,本次分享就不展開(kāi)介紹了。
細(xì)歸因
主線程消息異常
首先來(lái)看主線程消息異常的歸因思路:我們需要先對(duì)主線程消息進(jìn)行監(jiān)控,這里包括三種情況,已經(jīng)執(zhí)行完成和正在執(zhí)行中的消息以及消息隊(duì)列中待執(zhí)行的消息。對(duì)于已經(jīng)或正在執(zhí)行的消息,我們主要關(guān)注它們的耗時(shí)情況,通過(guò)分析系統(tǒng)源碼我們可以知道,主線程消息隊(duì)列里處理的消息一共包括三種:Java 消息、Native 消息和 Idle Handler。而對(duì)于待執(zhí)行的消息,我們主要關(guān)注其中的數(shù)量,從而判斷是否存在大量消息堆積的異常問(wèn)題。

對(duì)于主線程異常消息的問(wèn)題類(lèi)型,第一類(lèi)就是耗時(shí)消息,即在問(wèn)題區(qū)間內(nèi)存在一個(gè)或多個(gè)耗時(shí)大于閾值的慢消息。另一種是高頻消息,也就是存在出現(xiàn)次數(shù)以及累計(jì)耗時(shí)超過(guò)一定閾值的高密度消息。這里通過(guò)對(duì)業(yè)務(wù)消息的 target、callback 和 what 等信息進(jìn)行聚合分析,有時(shí)也可以協(xié)助定位到導(dǎo)致問(wèn)題的業(yè)務(wù)方。


但是僅僅找到異常消息并不能直接幫助我們解決問(wèn)題,還需要對(duì)消息的耗時(shí)原因做進(jìn)一步的歸因分析,找到其中引起消息耗時(shí)的問(wèn)題函數(shù)。接下來(lái)介紹一下線上 Trace 數(shù)據(jù)采集方案,這里我們采取類(lèi)似 Matrix 方案,通過(guò) ASM 字節(jié)碼工具,在編譯時(shí)對(duì)應(yīng)用內(nèi)的業(yè)務(wù)代碼進(jìn)行插樁,也就是在函數(shù)的入口和出口插入一行統(tǒng)計(jì)代碼,來(lái)記錄當(dāng)前方法的運(yùn)行耗時(shí)。為了降低采集時(shí)的性能損耗,將方法 begin / end 狀態(tài)標(biāo)識(shí)、當(dāng)前方法 ID 以及時(shí)間戳的 Diff 相對(duì)值,合并使用一個(gè) 64 位的變量來(lái)記錄。在 ANR 等異常問(wèn)題發(fā)生時(shí),再將 Ring Buffer 中記錄的數(shù)據(jù)進(jìn)行上報(bào),在后端數(shù)據(jù)鏈路處理生成對(duì)應(yīng)的 Trace 堆棧,并提供給后續(xù)的診斷算法進(jìn)行自動(dòng)化分析,以及 Perfetto 人工可視化分析的需求。

但對(duì)于抖音這樣的大型應(yīng)用來(lái)說(shuō),插樁方案也會(huì)有一些弊端,當(dāng)插樁的函數(shù)過(guò)多時(shí),會(huì)對(duì)包體積以及性能產(chǎn)生負(fù)面的影響。為了盡可能降低監(jiān)控工具對(duì)線上用戶體驗(yàn)的影響,我們提出了精準(zhǔn)插樁的方案!因?yàn)榻Y(jié)合對(duì)于線上問(wèn)題的分析訴求來(lái)看,我們重點(diǎn)關(guān)注的是上層執(zhí)行的業(yè)務(wù)函數(shù),比如頁(yè)面生命周期、業(yè)務(wù)消息等入口方法,以及底層那些可能耗時(shí)的業(yè)務(wù)函數(shù)。所以我們基于靜態(tài)代碼分析的基礎(chǔ)能力,分析提取出帶有耗時(shí)特征的函數(shù)來(lái)進(jìn)行插樁,例如下面表格中帶有鎖關(guān)鍵字的函數(shù)、存在 Native / IO 等調(diào)用的函數(shù)以及特別復(fù)雜的大方法等。在這個(gè)精準(zhǔn)插樁的優(yōu)化策略之下,大幅減少了約 90% 的插樁數(shù)量!

在大幅精簡(jiǎn)插樁數(shù)量之后,我們?cè)谙M(fèi)線上數(shù)據(jù)時(shí)又面臨到一個(gè)問(wèn)題,就是僅有插樁的堆棧信息太少,有時(shí)難以幫助我們實(shí)際定位到發(fā)生問(wèn)題的代碼。為了解決這一痛點(diǎn)問(wèn)題,我們又設(shè)計(jì)了插樁和抓棧數(shù)據(jù)擬合的優(yōu)化方案,其原理就是通過(guò)對(duì)耗時(shí)超過(guò)一定閾值的慢函數(shù)進(jìn)行抓棧上報(bào),然后在服務(wù)端再將插樁與抓棧數(shù)據(jù)根據(jù)時(shí)間點(diǎn)進(jìn)行擬合,補(bǔ)齊其中缺失的業(yè)務(wù)和系統(tǒng)堆棧信息。如下圖中所示,當(dāng)插樁堆棧中連續(xù)兩個(gè)節(jié)點(diǎn)能在抓棧數(shù)據(jù)中找到最小間距的數(shù)據(jù)并對(duì)齊時(shí),抓棧數(shù)據(jù)中對(duì)應(yīng)層之間的數(shù)據(jù)將會(huì)被補(bǔ)全到到插樁數(shù)據(jù)中,生成新的擬合堆棧數(shù)據(jù),圖中的 D 就是被補(bǔ)全的數(shù)據(jù)。


在獲取到線上問(wèn)題發(fā)生時(shí)的詳細(xì) Trace 數(shù)據(jù)后,我們需要進(jìn)一步找到其中引起耗時(shí)的問(wèn)題函數(shù),常見(jiàn)的問(wèn)題函數(shù)類(lèi)型包括以下兩種:
- 慢函數(shù):是指函數(shù)的執(zhí)行耗時(shí)超過(guò)一定的閾值;并且從實(shí)際可消費(fèi)性的角度來(lái)看,我們預(yù)期是要能找到更靠近葉子節(jié)點(diǎn)的業(yè)務(wù)慢函數(shù)。所以需要根據(jù)實(shí)際調(diào)用堆棧的情況,進(jìn)一步剔除底層的基礎(chǔ)庫(kù)或工具類(lèi)方法,以及存在相同調(diào)用鏈路的親緣父子節(jié)點(diǎn),來(lái)找到最合適的慢函數(shù)問(wèn)題。
- 高頻函數(shù):對(duì)應(yīng)就是單個(gè)雖然并不耗時(shí),但是由于次數(shù)很多,累計(jì)執(zhí)行耗時(shí)超過(guò)閾值的函數(shù);根據(jù)我們以往的經(jīng)驗(yàn)來(lái)看,對(duì)于高頻函數(shù)的優(yōu)化通常也能帶來(lái)不錯(cuò)的收益。


當(dāng)然僅僅知道函數(shù)慢也是不夠的!我們結(jié)合一個(gè)線上實(shí)際的示例來(lái)看,通過(guò) Trace 可以發(fā)現(xiàn)標(biāo)記紅框的這里有一個(gè)業(yè)務(wù)函數(shù)執(zhí)行比較耗時(shí),但是這里為什么會(huì)耗時(shí)呢?我們要如何進(jìn)行優(yōu)化呢?目前僅有的堆棧信息并不能滿足我們的歸因需求。之后通過(guò)分析業(yè)務(wù)代碼并補(bǔ)齊了缺失的“關(guān)鍵”信息之后,我們就可以明確知道這里耗時(shí)的原因是由于鎖競(jìng)爭(zhēng)導(dǎo)致的,并且我們還進(jìn)一步補(bǔ)充了當(dāng)前持有鎖的線程以及堆棧等重要信息。


為了更好的對(duì)慢函數(shù)的耗時(shí)進(jìn)行歸因,除了前面采集的 Trace 堆棧數(shù)據(jù)之外,我們還需要補(bǔ)充一些關(guān)鍵的上下文信息,這里就統(tǒng)稱(chēng)為精細(xì)化數(shù)據(jù)。比如上面提到的鎖,還有函數(shù)的 CPU-Time、Binder 調(diào)用的名稱(chēng)、IO 讀寫(xiě)的文件路徑和大小、繪制渲染相關(guān)的 RenderNode,以及內(nèi)存 Block GC 等相關(guān)信息。

所以回顧總結(jié)一下主線程消息異常的歸因流程,首先需要明確當(dāng)前 ANR 的問(wèn)題區(qū)間,然后找到其中的異常消息(耗時(shí)消息或高頻消息),進(jìn)一步下鉆找到其中引起耗時(shí)的問(wèn)題函數(shù)(慢函數(shù)或高頻函數(shù)),最后再結(jié)合精細(xì)化數(shù)據(jù)對(duì)其耗時(shí)原因進(jìn)行歸因。

后臺(tái)任務(wù)異常
接下來(lái)再看下后臺(tái)任務(wù) CPU 異常的歸因思路:首先我們需要明確是否存在后臺(tái)任務(wù)對(duì)主線程 CPU 資源產(chǎn)生搶占的問(wèn)題,這里可以結(jié)合主線程的非自愿上下文切換以及調(diào)度狀態(tài)的信息,來(lái)觀測(cè)主線程是否有較多的時(shí)間都花費(fèi)在等待系統(tǒng)調(diào)度上。如果存在明顯的異常情況,再結(jié)合系統(tǒng)和應(yīng)用的 CPU 使用率信息,可以進(jìn)一步先定位到是應(yīng)用的子線程 / 子進(jìn)程,或是關(guān)鍵系統(tǒng)進(jìn)程(如 dex2oat 進(jìn)程等),還是其他應(yīng)用進(jìn)程造成的 CPU 資源搶占。
對(duì)于應(yīng)用自身造成的 CPU 搶占問(wèn)題,我們需要進(jìn)一步定位到具體的問(wèn)題代碼。所以我們?cè)谥暗?Trace 采集方案的基礎(chǔ)上,進(jìn)行了重大的升級(jí)改造,擴(kuò)展支持了全線程的 Trace 數(shù)據(jù)采集。

單線程 Trace | 多線程 Trace | 說(shuō)明 | |
Flag 狀態(tài)位 | 2 bit | 2 bit | 代表函數(shù)開(kāi)始/結(jié)束:3(二進(jìn)制0b11)= catch, // 預(yù)留 2(二進(jìn)制0b10)= throw,// 預(yù)留 1(二進(jìn)制0b01)= begin, 0(二進(jìn)制0b00)= end |
Method ID | 20 bit | 20 bit | 代表插樁的方法 ID,最大支持 1048575 個(gè)函數(shù) |
Thread ID | - | 15 bit | 代表當(dāng)前線程的 TID |
Timestamp | 42 bit | 27 bit | 代表當(dāng)前函數(shù)執(zhí)行時(shí)與基準(zhǔn)時(shí)間的相對(duì)時(shí)間,多線程模式下最大支持 134,217,727 ms = 約 1.5 天 |
由于后臺(tái)任務(wù)我們重點(diǎn)關(guān)注的是 CPU-Time 耗時(shí),所以在采集函數(shù)的 Wall-Time 執(zhí)行耗時(shí)之外,同時(shí)也支持函數(shù)粒度的 CPU-Time 耗時(shí)采集,并在后端進(jìn)行處理關(guān)聯(lián)。這里出于性能損耗上的考慮,我們會(huì)進(jìn)一步精簡(jiǎn)控制同時(shí)需要采集 CPU 時(shí)間的插樁函數(shù)數(shù)量,例如僅對(duì)系統(tǒng)的生命周期方法,以及子線程的 Runnable、Callable 以及二方 / 三方的任務(wù)框架入口方法,以及少量的關(guān)鍵特征方法才開(kāi)啟,并且會(huì)設(shè)置最小的采樣間隔時(shí)間。
對(duì)于
CPU-Time 的獲取一般有兩種方式:一種是通過(guò)定期讀取 proc
文件系統(tǒng)下的文件來(lái)解析獲取,這種方式如果想要精確到方法級(jí)別需要相當(dāng)高的讀取頻率,這種高頻率讀取文件并解析的性能損耗很高,不適合在線上方法級(jí)別的
CPU-Time 采集;另一種則是通過(guò) Android 提供的 SystemClock.currentThreadTimeMillis() 方法或者 Native 層的 clock_gettime(CLOCK_THREAD_CPUTIME_ID) 方法獲取當(dāng)前線程的 CPU-Time,這種方式比較適合采集方法級(jí)別的 CPU 耗時(shí),在方法開(kāi)始和結(jié)束時(shí)分別調(diào)用前述方法再計(jì)算差值即可,因此我們線上采集選擇的也是這個(gè)方案。
同樣回顧總結(jié)一下后臺(tái)任務(wù)異常的歸因流程,在 ANR 的問(wèn)題區(qū)間內(nèi),首先需要明確是否存在對(duì)主線程執(zhí)行效率產(chǎn)生明顯影響的 CPU 資源搶占,如果是應(yīng)用自身的問(wèn)題,先找到應(yīng)用內(nèi) CPU 負(fù)載較高的線程或進(jìn)程,進(jìn)一步定位到對(duì)應(yīng)異常階段里的后臺(tái)任務(wù)代碼,最后再結(jié)合精細(xì)化數(shù)據(jù)對(duì)其 CPU 耗時(shí)原因進(jìn)行歸因。

聚合歸因思路
基于以上對(duì) ANR 單點(diǎn)問(wèn)題進(jìn)行診斷分析后產(chǎn)出的歸因結(jié)論,我們可以進(jìn)一步結(jié)合線上大數(shù)據(jù)進(jìn)行聚合歸因,從而幫助我們更好的聚焦到 Top 重點(diǎn)問(wèn)題的優(yōu)化上。
歸因標(biāo)簽
首先是對(duì)歸因標(biāo)簽的聚合分析,主要包括以下幾類(lèi):
- 粗歸因標(biāo)簽:針對(duì) ANR 問(wèn)題定性的歸因分類(lèi)標(biāo)簽,包括主線程阻塞、高頻消息、CPU 搶占、堆內(nèi)存不足等。
- 細(xì)歸因標(biāo)簽:針對(duì)細(xì)歸因定位到的問(wèn)題代碼的精細(xì)化歸因標(biāo)簽,包括主線程鎖、IO、Binder 或者 Block GC 阻塞耗時(shí)等。
- 業(yè)務(wù)歸因特征:發(fā)生 ANR 時(shí)用戶所在場(chǎng)景頁(yè)面等業(yè)務(wù)維度的特征標(biāo)簽,有時(shí)也可以輔助快速定位到問(wèn)題相關(guān)的業(yè)務(wù)方。
如下圖所示,通過(guò)對(duì)以上不同歸因標(biāo)簽的多維聚合分析,可以幫助我們對(duì)線上 ANR 問(wèn)題的特征分布有一個(gè)全局的了解和認(rèn)知,同時(shí)也能指導(dǎo)我們?cè)跉w因能力上下一步需要重點(diǎn)攻堅(jiān)的方向。

異常問(wèn)題
其次是對(duì)細(xì)歸因產(chǎn)出的異常問(wèn)題進(jìn)行聚合分析, 目前主要包括主線程異常函數(shù)、后臺(tái)任務(wù)以及內(nèi)存這三個(gè)維度。聚合后的問(wèn)題列表支持滲透率、耗時(shí)均值、PCT 50 / 90 耗時(shí)以及場(chǎng)景等維度的統(tǒng)計(jì)數(shù)據(jù),可以幫助我們識(shí)別出線上整體占比較高或耗時(shí)特別嚴(yán)重的這類(lèi)問(wèn)題。

在進(jìn)入異常函數(shù)的歸因詳情頁(yè)之后,可以查看當(dāng)前問(wèn)題函數(shù)在線上大數(shù)據(jù)聚合后的火焰圖,其中 Caller 堆棧代表上層不同業(yè)務(wù)方的調(diào)用次數(shù)分布情況,而 Callee 堆棧則是所有子函數(shù)的耗時(shí)分布情況。


最后,基于以上的歸因標(biāo)簽、異常問(wèn)題以及業(yè)務(wù)歸因信息,平臺(tái)會(huì)產(chǎn)出一個(gè)對(duì) ANR 問(wèn)題最終的歸因結(jié)論以及對(duì)應(yīng)的綜合置信度評(píng)分。

落地效果
接下來(lái)再介紹一下平臺(tái)目前的落地效果:首先這個(gè)案例是一個(gè)啟動(dòng)階段的 ANR 問(wèn)題,我們從主線程 Trace 中可以分析定位到主線程的耗時(shí)函數(shù),并且通過(guò)細(xì)歸因標(biāo)簽的結(jié)果,可以明確知道是一個(gè)鎖耗時(shí)的問(wèn)題。進(jìn)一步結(jié)合鎖的詳情信息進(jìn)行下鉆分析,通過(guò)當(dāng)前子線程持有鎖的聚合堆棧,發(fā)現(xiàn)是由于某個(gè)后臺(tái)任務(wù)的執(zhí)行時(shí)機(jī)變更提前了,從而與主線程某個(gè)任務(wù)產(chǎn)生了鎖競(jìng)爭(zhēng)沖突,導(dǎo)致主線程長(zhǎng)時(shí)間的阻塞等待引起了 ANR。



第二個(gè)案例是一個(gè)主線程高頻消息問(wèn)題,從定位到的問(wèn)題函數(shù)可以發(fā)現(xiàn)其調(diào)用的非常高頻,平均在一次 ANR 里會(huì)出現(xiàn)了上千次!通過(guò)進(jìn)一步分析發(fā)現(xiàn)是由于某個(gè)業(yè)務(wù)的邏輯 Bug,導(dǎo)致在特定場(chǎng)景下會(huì)發(fā)送大量的重復(fù)消息,導(dǎo)致主線程消息隊(duì)列堵塞引起的 ANR 劣化。


第三個(gè)案例是一個(gè)子線程高頻任務(wù)問(wèn)題,通過(guò)定位到的后臺(tái)任務(wù)可以發(fā)現(xiàn)其在多個(gè)子線程的出現(xiàn)次數(shù)都非常高頻,并且累計(jì)的 CPU 耗時(shí)也比較高。進(jìn)一步分析發(fā)現(xiàn)也是某個(gè)業(yè)務(wù)的 Bug 問(wèn)題,在特定場(chǎng)景下會(huì)向子線程發(fā)送大量的重復(fù)任務(wù),并且由于這些異步任務(wù)內(nèi)部還會(huì)給主線程 Handler 發(fā)送或刪除消息,所以除了會(huì)搶占 CPU 資源之外,還會(huì)間接導(dǎo)致主線程消息隊(duì)列在遍歷取消息時(shí)會(huì)發(fā)生高頻的鎖競(jìng)爭(zhēng)耗時(shí),兩個(gè)因素疊加之下引起的 ANR。


最后總結(jié)下平臺(tái)過(guò)去一年的階段性成果,總共累計(jì)發(fā)現(xiàn)了有效問(wèn)題 88 個(gè),修復(fù)并優(yōu)化其中 56 個(gè),同時(shí)協(xié)助抖音 / 抖極的大盤(pán) ANR 率分別下降了 -13.06% 和 -8.70% ,并取得了不錯(cuò)的業(yè)務(wù)收益。
總結(jié)展望
抖音 ANR 自動(dòng)歸因平臺(tái)未來(lái)的規(guī)劃主要包括以下三個(gè)方面:
- 歸因體系:持續(xù)打磨監(jiān)控能力和歸因算法,包括探索完善 Java / Native 內(nèi)存、繪制渲染以及 Native Trace 等方向上的精細(xì)化歸因能力。
- 防劣化體系:持續(xù)優(yōu)化線上劣化歸因和消費(fèi)流程,提升線上自動(dòng)歸因準(zhǔn)確率,以及劣化問(wèn)題的消費(fèi)解決效率。
- 專(zhuān)家系統(tǒng):沉淀專(zhuān)家經(jīng)驗(yàn),并嘗試結(jié)合大模型等新技術(shù),通過(guò)對(duì)技術(shù)特征和業(yè)務(wù)特征進(jìn)行精細(xì)化聚合分析,進(jìn)一步提升問(wèn)題發(fā)現(xiàn)和解決效率。



































