天天看點

位元組跳動 iOS Heimdallr 卡死卡頓監控方案與優化之路

位元組跳動 iOS Heimdallr 卡死卡頓監控方案與優化之路

本文主要介紹Heimdallr對卡死、卡頓異常的監控原理,并結合長時間的業務沉澱發現的問題進行不斷疊代和優化,逐漸實作全面、穩定、可靠的曆程。

位元組跳動 iOS Heimdallr 卡死卡頓監控方案與優化之路

👉 點這裡申請

位元組跳動 iOS Heimdallr 卡死卡頓監控方案與優化之路
本文主要介紹

Heimdallr

對卡死、卡頓異常的監控原理,并結合長時間的業務沉澱發現的問題進行不斷疊代和優化,逐漸實作全面、穩定、可靠的曆程。

作者:位元組跳動終端技術——白昆侖

前言

卡死、卡頓作為目前iOS App的重要性能名額,不僅影響着使用者體驗,更關系到使用者留存、DAU等重要産品資料。本文主要介紹

Heimdallr

一、什麼是卡死/卡頓?

卡頓,顧名思義就是在使用過程中出現了一段時間的阻塞,使得使用者在這一段時間内無法進行操作,螢幕上的内容也沒有任何的變化。

Heimdallr

在監控名額上,根據阻塞時間的長短進行了3個等級的劃分。

1、流暢性與丢幀:動畫、滑動清單不流暢,一般為十幾至幾十毫秒的級别

2、卡頓:短時間操作無反應,恢複後能繼續使用,從幾百毫秒至幾秒

3、卡死:長時間無反應,直至被系統殺死,通過線上收集資料,最少為5s

可以看到,根據嚴重性由小至大可将卡頓問題劃分為流暢性與丢幀、卡頓、卡死三個不同的等級。卡死的嚴重程度與Crash是相當的,甚至更為嚴重。因為卡死不僅僅造成了類似于崩潰的閃退,更使得使用者被迫等待了相當長的一段時間,更加損害使用者的體驗。由于監控方案上的差異,本文主要面向的是後兩者卡頓和卡死的監控。

二、卡死/卡頓的原因

iOS開發中,由于UIKit是非線程安全的,是以一切與UI相關的操作都必須放在主線程執行,系統會每16ms(1/60幀)将UI的變化重新繪制,渲染至螢幕上。如果UI重新整理的間隔能小于16ms,那麼使用者是不會感到卡頓的。但是如果在主線程進行了一些耗時的操作,阻礙了UI的重新整理,那麼就會産生卡頓,甚至是卡死。主線程對于任務的處理是基于

Runloop

機制,如下圖所示。Runloop支援外部注冊通知回調,提供了

1、

RunloopEntry

2、

RunloopBeforeTimers

3、

RunloopBeforeSources

4、

RunloopBeforeWaiting

5、

RunloopAfterWaiting

6、

RunloopExit

6個時機的事件回調,其流轉關系如下圖所示。

Runloop

在沒有任務需要處理的時候就會進入至休眠狀态,直至有信号将其喚醒,其又會去處理新的任務。

位元組跳動 iOS Heimdallr 卡死卡頓監控方案與優化之路

在日常編碼中,

UIEvent

事件、

Timer

dispatch

主線程任務都是在

Runloop

的循環機制的驅動下完成的。一旦我們在主線程中的任何一個環節進行了一個耗時的操作,或者因為鎖的使用不當造成了與其它線程的死鎖,主線程就會因為無法執行

Core - Animation

的回調而造成界面無法重新整理。而使用者的互動又依賴于

UIEvent

的傳遞和響應,該流程也必須在主線程中完成。是以說主線程的阻塞會導緻UI和互動的雙雙阻塞,這也是導緻卡死、卡頓的根本原因。

三、監控方案

既然問題的根本在于主線程

Runloop

的阻塞,那麼我們就要通過技術手段監測主線程

Runloop

的運作狀态。為了能夠實時擷取主線程

Runloop

的狀态,首先對主線程注冊上面提到的幾個事件回調,在觸發事件回調時,利用

signal

機制将其運作狀态傳遞給另一個正在監聽的子線程(後面稱之為監聽線程)。監聽線程對于信号的處理可以是多樣的,它可以設定等待

signal

的逾時時間,如果超過了設定的門檻值,這說明主線程可能正在經曆阻塞。通過監聽線程,我們可以完整地了解到主線程

Runloop

循環的周期,目前處于哪個階段,耗時了多久等等。根據這些必要的資訊,就可以采取對應的政策進行異常的捕獲和處理,後面會單獨就卡頓、卡死分别進行說明。

目前大多數APM工具都是采用監聽

Runloop

的方式進行卡頓的捕獲,這也是性能、準确性表現最好的一種方案。由于

RunloopBeforeTimers

的和

RunloopBeforeSources

是緊鄰的兩個事件回調,

Heimdallr

為了降低

Runloop

頻繁事件回調造成的性能損失,去除了對

RunloopBeforeTimers

的監聽。

1. 卡頓(ANR)

卡頓監控的特點在于主線程的阻塞是暫時的、能夠恢複的,是以我們要擷取卡頓持續的時間,用來評估卡頓問題的嚴重性。我們預先設定一個卡頓時間的門檻值T,當主線程阻塞的時間超過該門檻值,則會觸發全線程的抓棧,擷取卡頓場景的堆棧資訊。此後監聽線程繼續等待主線程直至主線程恢複,并計算卡頓的總時間,整合之前擷取的堆棧資訊,上報卡頓異常。

需要說明的是,如果在抓棧之後主線程無法恢複,那麼該異常不是卡頓,應交由卡死子產品處理。

2. 卡死(WatchDog)

與卡頓不同,卡死的阻塞是更長的,而且是無法恢複的。iOS系統會對App的主線程進行類似的監控,一旦發現了阻塞的情況,持續時間大于目前系統内允許的門檻值(不同iOS版本和機型不同),就會強制殺死目前App程序,這個操作是沒有任何通知的。是以我們需要做的就是在系統發現卡死并強殺之前,擷取堆棧,并盡可能的評估出卡死持續的時間。

預先設定一個卡死的門檻值T(預設是8s),這個門檻值可以是相對保守的,并不是說超過了這個門檻值就一定會被判定為卡死。在超過卡死門檻值T的時候,擷取全線程的堆棧,并儲存至本地檔案中。之後每隔一段時間(采樣間隔,預設是1s),會進行一次采樣。采樣的目的不是為了擷取新的堆棧,而是為了更新卡死持續的時間,将該資訊儲存至本地檔案中。是以,采樣的間隔越小逼近真實卡死時間按越精确。直至到某一個時間節點,系統把App殺死。當App下一次啟動時,卡死子產品會根據上一次啟動中保留的本地檔案資訊,還原出卡死的堆棧、持續時間等資訊,并上報卡死異常。

需要說明的是,很多人認為卡死一定是因為死鎖、死循環這樣的場景,導緻程式永遠也無法完成導緻的。其實不然,在很多場景下,一個或多個耗時的操作,隻要其耗時超過了系統的允許門檻值,都會觸發夾死。當應用啟動過程中,沒有在限定時間内完成初始化工作也會被系統殺死。是以,某些卡死可能是多個場景的不合理一起導緻的,這也給卡死的問題定位提出了更高的要求。

四、問題與優化

理想是豐滿的,現實是骨感的。看似”無懈可擊“的監控方案,線上上卻暴露出不同程度的問題。

1. 卡頓監控優化

在卡頓監控中,我們認為超過了卡頓門檻值時擷取的堆棧一定是一個卡頓的場景,其實不然。在一些時候,擷取的堆棧可能是他人的”背鍋俠“。我們來看下面這個case。導緻主線程卡頓的是4這個耗時操作,但是當我們設定門檻值逾時時,擷取的堆棧卻是沒有任何性能問題的5。是以如果使用這種方式來進行卡頓的監控,一定會存在誤報。而根據機率來講,雖然上報的5是一個誤報,但就線上的上報量來講,4的數量一定是要大于5的。是以上報量級大的堆棧才應該是真正的耗時操作,是需要我們專注去解決的,而那些量級較小的堆棧則可能是誤報。 

那麼是否能夠通過一些技術手段,在控制性能開銷的情況下,對卡頓場景捕捉的更加準确呢?一個比較好的思路就是采樣政策。如下圖,我們在原有的”正常模式“的基礎上增加了”采樣模式“。需要額外定義采樣間隔、采樣門檻值。我們把卡頓門檻值的等待過程,劃分為以采樣間隔為機關的粒度更細的時間節點。在每個時間節點進行主線程采樣,對主線程進行堆棧的提取。由于僅對主線程進行堆棧提取,是以耗時較全線程抓棧要小很多。

位元組跳動 iOS Heimdallr 卡死卡頓監控方案與優化之路

擷取了主線程堆棧後,通過提取頂層第一個自身調用來進行堆棧的聚合。如果某一個相同堆棧持續的時間超過了設定的采樣門檻值,例如圖中的4,重複了3次,那麼就會判定該場景一定是一個卡頓場景。那麼此時就會進行全線程抓棧,而後面的卡頓門檻值觸發時則不再抓棧。

結合主線程采樣,我們可以更加精準的以函數級别監控卡頓場景,但是也需要付出采樣帶來的額外性能開銷。為了将采樣的開銷降至最低,避免線上對低端裝置造成二次性能劣化,卡頓監控支援采樣功能的退火政策。當某一個卡頓場景被多次捕獲時,為了避免再次将其捕獲,造成不必要的性能浪費,會逐漸增加采樣間隔,直至将”采樣模式“退化成”正常模式“。

在Slardar平台配置并開啟采樣功能後,可以通過

sample_flag

來過濾通過采樣逾時擷取的卡頓異常。通過此方式擷取的堆棧,大機率為卡頓場景,可以更加有針對性的去分析和解決。

2. 卡死監控優化

相比卡頓,卡死的誤報大多發生在背景(目前

Heimdallr

提供背景卡死過濾,如果對背景卡死不關心的業務方可以自行打開)。因為背景場景的限制,目前App的線程優先級更低,而且随時存在被系統挂起的可能,這給我們進行卡死時間的判定帶來了很多問題。

上面的Case描述的是一個卡死的誤報場景,因為在背景的原因線程的優先級較低,是以1、2、3任務執行的時間要比前台更久,更加容易超過我們的卡死門檻值。而後,因為iOS系統的政策問題,背景應用被挂起(suspend),直至某一個時間點因為記憶體緊張,将整個應用殺死。但請注意,這個流程屬于App正常的生命周期範疇,并不是

WatchDog

。而按照我們之前的政策,這将會被判定為卡死。由于我們無法監聽到suspend事件,是以這種場景目前還無法排除誤報。

還有一種誤觸發夾死的case是,suspend發生在8s門檻值前,在長時間的挂起後,應用被resume,此時8s的逾時被觸發。但是實際上,我們的App隻有在8s中的很少一部分時間在running,大部分時間都是被挂起,是以不應該觸發夾死判定。歸根結底是卡死計時的準确性問題。

為了解決上面的問題,對計時政策進行了改進。相比于直接進行8s的等待,我們将時間細分為8個1s。如果在這段時間内App被挂起,等到恢複時也不會直接超過8s的門檻值,而僅僅會造成最多1s的誤差。

此外,上面也提到過,卡死有的時候可能是多個耗時場景累計導緻的。為了能夠跟蹤主線程的變化,在抓棧之後的采樣階段,對主線程進行堆棧采樣,并将其一起上報。結合采樣中擷取的主線程堆棧,我們可以得到一個主線程堆棧變化的時間線,能夠更加準确的幫助定位問題所在。(時間線功能在Heimdallr 0.7.15之後支援)

最後,我們發現部分卡死場景是由于

OC Runtime Lock

導緻的(大機率是

dyld

OC Runtime Lock

造成的死鎖)。一旦發生這種類型的卡死,其它所有線程的OC代碼都會是以而阻塞,當然也包括監聽線程,卡死監控此時就無法捕獲這個異常。為了能夠覆寫所有場景,我們把卡死、卡頓子產品的所有邏輯進行了C/C++重構,解除了對OC調用的依賴,并且性能相比與OC實作進一步得到提升。

結語

Heimdallr

ANR

WatchDog

子產品經過一段時間的疊代與優化,達到了一個全面、穩定、可靠的狀态。這期間的一些優化思路借鑒了一些開源的APM架構,并結合使用方的實際需求進行不斷改進。感謝所有使用方的回報,幫助我們不斷完善我們的功能與體驗。後續我們會繼續針對Watchdog場景增加防卡死功能,幫助接入方能夠在無侵入式的情況下,解決通用場景的卡死問題。

🔥 火山引擎 APMPlus 應用性能監控是火山引擎應用開發套件 MARS 下的性能監控産品。我們通過先進的資料采集與監控技術,為企業提供全鍊路的應用性能監控服務,助力企業提升異常問題排查與解決的效率。

目前我們面向中小企業特别推出「APMPlus 應用性能監控企業助力行動」,為中小企業提供應用性能監控免費資源包。現在申請,有機會獲得60天免費性能監控服務,最高可享6000萬條事件量。

👉 點選這裡,立即申請