手把手教你高效監控ANR
ANR監控是一個非常有年代感的話題了,但是市面上的ANR監控工具,或者並非真正意義上的ANR的監控(而是5秒卡頓監控);或者並不完善,監控不到到所有的ANR。而想要得到一個完善的ANR監控工具,必須要先了解系統整個ANR的流程。本文分析了ANR的主要流程,給出了一個完善的ANR監控方案。該方案已經在Android微信客戶端上經過全量驗證,穩定地運行了一年多的時間。
我們知道ANR流程基本都是在system_server系統程序完成的,系統程序的行為我們很難監控和改變,想要監控ANR就必須找到系統程序跟我們自己的應用程序是否有互動,如果有,兩者互動的邊界在哪裡,邊界上應用一端的行為,才是我們比較容易能監控到的,想要要找到這個邊界,我們就必須要了解ANR的流程。
一、ANR流程
無論ANR的來源是哪裡,最終都會走到ProcessRecord中的appNotResponding,這個方法包括了ANR的主要流程,所以也比較長,我們找出一些關鍵的邏輯來分析:frameworks/base/services/core/java/com/android/server/am/ProcessRecord.java:
void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,
先是一長串if else,給出了幾種比較極端的情況,會直接return,而不會產生一個ANR,這些情況包括:程序正在處於正在關閉的狀態,正在crash的狀態,被kill的狀態,或者相同程序已經處在ANR的流程中。
另外很重要的一個邏輯就是判斷當前ANR是否是一個SilentAnr,所謂“沉默的ANR”,其實就是後臺ANR,後臺ANR跟前臺ANR會有不同的表現:前臺ANR會彈無響應的Dialog,後臺ANR會直接殺死程序。前後臺ANR的判斷的原則是:如果發生ANR的程序對使用者來說是有感知的,就會被認為是前臺ANR,否則是後臺ANR。另外,如果在開發者選項中勾選了“顯示後臺ANR”,那麼全部ANR都會被認為是前臺ANR。
我們繼續分析這個方法:
if (!isSilentAnr && !onlyDumpSelf) {
發生ANR後,為了能讓開發者知道ANR的原因,方便定位問題,會dump很多資訊到ANR Trace檔案裡,上面的邏輯就是選擇需要dump的程序。ANR Trace檔案是包含許多程序的Trace資訊的,因為產生ANR的原因有可能是其他的程序搶佔了太多資源,或者IPC到其他程序(尤其是系統程序)的時候卡住導致的。
選擇需要dump的程序是一段挺有意思邏輯,我們稍微分析下:需要被dump的程序被分為了firstPids、nativePids以及extraPids三類:
-
firstPIds:firstPids是需要首先dump的重要程序,發生ANR的程序無論如何是一定要被dump的,也是首先被dump的,所以第一個被加到firstPids中。如果是SilentAnr(即後臺ANR),不用再加入任何其他的程序。如果不是,需要進一步新增其他的程序:如果發生ANR的程序不是system_server程序的話,需要新增system_server程序;接下來輪詢AMS維護的一個LRU的程序List,如果最近訪問的程序包含了persistent的程序,或者帶有BIND_TREAT_LIKE_ACTVITY標籤的程序,都新增到firstPids中。
-
extraPids:LRU程序List中的其他程序,都會首先新增到lastPids中,然後lastPids會進一步被選出最近CPU使用率高的程序,進一步組成extraPids;
-
nativePids:nativePids最為簡單,是一些固定的native的系統程序,定義在WatchDog.java中。
拿到需要dump的所有程序的pid後,AMS開始按照firstPids、nativePids、extraPids的順序dump這些程序的堆疊:
File tracesFile = ActivityManagerService.dumpStackTraces(firstPids,
這裡也是我們需要重點分析的地方,我們繼續看這裡做了什麼,跟到AMS裡面,
frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java:
public static Pair<Long, Long> dumpStackTraces(String tracesFile, ArrayList<Integer> firstPids,
我們首先關注到remainingTime,這是一個重要的變數,規定了我們dump所有程序的最長時間,因為dump程序所有執行緒的堆疊,本身就是一個重操作,何況是要dump許多程序,所以規定了發生ANR之後,dump全部程序的總時間不能超過20秒,如果超過了,馬上返回,確保ANR彈窗可以及時的彈出(或者被kill掉)。我們繼續跟到dumpJavaTracesTombstoned
private static long dumpJavaTracesTombstoned(int pid, String fileName, long timeoutMs) {
再一路追到native層負責dump堆疊的system/core/debuggerd/client/debuggerd_client.cpp:
bool debuggerd_trigger_dump(pid_t tid, DebuggerdDumpType dump_type, unsigned int timeout_ms, unique_fd output_fd) {
來了來了!之前說的互動邊界終於找到了!這裡會通過sigqueue向需要dump堆疊的程序傳送SIGQUIT訊號,也就是signal 3訊號,而發生ANR的程序是一定會被dump的,也是第一個被dump的。這就意味著,只要我們能監控到系統傳送的SIGQUIT訊號,也許就能夠監控到發生了ANR。
每一個應用程序都會有一個SignalCatcher執行緒,專門處理SIGQUIT,來到art/runtime/signal_catcher.cc:
void* SignalCatcher::Run(void* arg) {
WaitForSignal方法呼叫了sigwait方法,這是一個阻塞方法。這裡的死迴圈,就會一直不斷的等待監聽SIGQUIT和SIGUSR1這兩個訊號的到來。
整理一下ANR的過程:當應用發生ANR之後,系統會收集許多程序,來dump堆疊,從而生成ANR Trace檔案,收集的第一個,也是一定會被收集到的程序,就是發生ANR的程序,接著系統開始向這些應用程序傳送SIGQUIT訊號,應用程序收到SIGQUIT後開始dump堆疊。來簡單畫個示意圖:
所以,事實上程序發生ANR的整個流程,也只有dump堆疊的行為會在發生ANR的程序中執行。這個過程從收到SIGQUIT開始(圈1),到使用socket寫Trace(圈2)結束,然後再繼續回到server程序完成剩餘的ANR流程。我們就在這兩個邊界上做做文章。
首先我們肯定會想到,我們能否監聽到syste_server傳送給我們的SIGQUIT訊號呢?如果可以,我們就成功了一半。
二、監控SIGQUIT訊號
Linux系統提供了兩種監聽訊號的方法,一種是SignalCatcher執行緒使用的sigwait方法進行同步、阻塞地監聽,另一種是使用sigaction方法註冊signal handler進行非同步監聽,我們都來試試。
2.1. sigwait
我們首先嚐試前一種方法,模仿SignalCatcher執行緒,做一模一樣的事情,通過一個死迴圈sigwait,一直監聽SIGQUIT:
static void *mySigQuitCatcher(void* args) {
這個時候就有了兩個不同的執行緒sigwait同一個SIGQUIT,具體會走到哪個呢,我們在sigwait的文件中找到了這樣的描述(sigwait方法是由sigwaitinfo方法實現的):
原來當有兩個執行緒通過sigwait方法監聽同一個訊號時,具體是哪一個執行緒收到訊號時不能確定的**。不確定可不行,當然不滿足我們的需求。
3.2. Signal Handler
那我們再試下另一種方法是否可行,我們通過可以sigaction方法,建立一個Signal Handler:
void signalHandler(int sig, siginfo_t* info, void* uc) {
建立了Signal Handler之後,我們發現在同時有sigwait和signal handler的情況下,訊號沒有走到我們的signal handler而是依然被系統的Signal Catcher執行緒捕獲到了,這是什麼原因呢?
原來是Android預設把SIGQUIT設定成了BLOCKED,所以只會響應sigwait而不會進入到我們設定的handler方法中。我們通過pthread_sigmask或者sigprocmask把SIGQUIT設定為UNBLOCK,那麼再次收到SIGQUIT時,就一定會進入到我們的handler方法中。需要這樣設定:
sigset_t sigSet;
最後需要注意,我們通過Signal Handler搶到了SIGQUIT後,原本的Signal Catcher執行緒中的sigwait就不再能收到SIGQUIT了,原本的dump堆疊的邏輯就無法完成了,我們為了ANR的整個邏輯和流程跟原來完全一致,需要在Signal Handler裡面重新向Signal Catcher執行緒傳送一個SIGQUIT:
int tid = getSignalCatcherThreadId(); //遍歷/proc/[pid]目錄,找到SignalCatcher執行緒的tid
(如果缺少了重新向SignalCatcher傳送SIGQUIT的步驟,AMS就一直等不到ANR程序寫堆疊,直到20秒超時後,才會被迫中斷,而繼續之後的流程。直接的表現就是ANR彈窗非常慢(20秒超時時間),並且/data/anr目錄下無法正常生成完整的 ANR Trace檔案。)
以上就得到了一個不改變系統行為的前提下,比較完善的監控SIGQUIT訊號的機制,這也是我們監控ANR的基礎。
三、完善的ANR監控方案
監控到SIGQUIT訊號並不等於就監控到了ANR。
3.1. 誤報
充分非必要條件1:發生ANR的程序一定會收到SIGQUIT訊號;但是收到SIGQUIT訊號的程序並不一定發生了ANR。
考慮下面兩種情況:
-
其他程序的ANR:上面提到過,發生ANR之後,發生ANR的程序並不是唯一需要dump堆疊的程序,系統會收集許多其他的程序進行dump,也就是說當一個應用發生ANR的時候,其他的應用也有可能收到SIGQUIT訊號。進一步,我們監控到SIGQUIT時,可能是監聽到了其他程序產生的ANR****,從而產生誤報。
-
非ANR傳送SIGQUIT:傳送SIGQUIT訊號其實是很容易的一件事情,開發者和廠商都可以很容易的傳送一個SIGQUIT(java層呼叫android.os.Process.sendSignal方法;Native層呼叫kill或者tgkill方法),所以我們可能會收到非ANR流程傳送的SIGQUIT訊號,從而產生誤報。
怎麼解決這些誤報的問題呢,我重新回到ANR流程開始的地方:
void appNotResponding(String activityShortComponentName, ApplicationInfo aInfo,
在ANR彈窗前,會執行到makeAppNotRespondingLocked方法中,在這裡會給發生ANR程序標記一個NOT_RESPONDING的flag。而這個flag我們可以通過ActivityManager來獲取:
private static boolean checkErrorState() {
監控到SIGQUIT後,我們在20秒內(20秒是ANR dump的timeout時間)不斷輪詢自己是否有NOT_RESPONDING對flag,一旦發現有這個flag,那麼馬上就可以認定發生了一次ANR。
(你可能會想,有這麼方便的方法,監控SIGQUIT訊號不是多餘的嗎?直接一個死迴圈,不斷輪訓這個flag不就完事了?是的,理論上確實能這麼做,但是這麼做過於的低效、耗電和不環保外,更關鍵的是,下面漏報的問題依然無法解決)
另外,Signal Handler回撥的第二個引數siginfo_t,也包含了一些有用的資訊,該結構體的第三個欄位si_code表示該訊號被髮送的方法,SI_USER表示訊號是通過kill傳送的,SI_QUEUE表示訊號是通過sigqueue傳送的。但在Android的ANR流程中,高版本使用的是sigqueue傳送的訊號,某些低版本使用的是kill傳送的訊號,並不統一。
而第五個欄位(極少數機型上是第四個欄位)si_pid表示的是傳送該訊號的程序的pid,這裡適用幾乎所有Android版本和機型的一個條件是:如果傳送訊號的程序是自己的程序,那麼一定不是一個ANR。可以通過這個條件排除自己傳送SIGQUIT,而導致誤報的情況。
3.2. 漏報
充分非必要條件2:程序處於NOT_RESPONDING的狀態可以確認該程序發生了ANR。但是發生ANR的程序並不一定會被設定為NOT_RESPONDING狀態。
考慮下面兩種情況:
-
後臺ANR(SilentAnr):之前分析ANR流程我們可以知道,如果ANR被標記為了後臺ANR(即SilentAnr),那麼殺死程序後就會直接return,並不會走到產生程序錯誤狀態的邏輯。這就意味著,後臺ANR沒辦法捕捉到,而後臺ANR的量同樣非常大,並且後臺ANR會直接殺死程序,對使用者的體驗也是非常負面的,這麼大一部分ANR監控不到,當然是無法接受的。
-
閃退ANR:除此之外,我們還發現相當一部分機型(例如OPPO、VIVO兩家的高Android版本的機型)修改了ANR的流程,即使是發生在前臺的ANR,也並不會彈窗,而是直接殺死程序,即閃退。這部分的機型覆蓋的使用者量也非常大。並且,確定兩家今後的新裝置會一直維持這個機制。
所以我們需要一種方法,在收到SIGQUIT訊號後,能夠非常快速的偵查出自己是不是已處於ANR的狀態,進行快速的dump和上報。很容易想到,我們可以通過主執行緒是否處於卡頓狀態來判斷。那麼怎麼最快速的知道主執行緒是不是卡住了呢?上一篇文章中,分析Sync Barrier洩漏問題時,我們反射過主執行緒Looper的mMessage物件,該物件的when變數,表示的就是當前正在處理的訊息入隊的時間,我們可以通過when變數減去當前時間,得到的就是等待時間,如果等待時間過長,就說明主執行緒是處於卡住的狀態,這時候收到SIGQUIT訊號基本上就可以認為的確發生了一次ANR:
private static boolean isMainThreadStuck(){
我們通過上面幾種機制來綜合判斷收到SIGQUIT訊號後,是否真的發生了一次ANR,最大程度地減少誤報和漏報,才是一個比較完善的監控方案。
3.3. 額外收穫:獲取ANR Trace
回到之前畫的ANR流程示意圖,Signal Catcher執行緒寫Trace(圈2)也是一個邊界,並且是通過socket的write方法來寫Trace的,如果我們能夠hook到這裡的write,我們甚至就可以拿到系統dump的ANR Trace內容。這個內容非常全面,包括了所有執行緒的各種狀態、鎖和堆疊(包括native堆疊),對於我們排查問題十分有用,尤其是一些native問題和死鎖等問題。Native Hook我們採用PLT Hook 方案,這種方案在微信上已經被驗證了其穩定性是可控的。
int (*original_connect)(int __fd, const struct sockaddr* __addr, socklen_t __addr_length);
其中有幾點需要注意:
-
只Hook ANR流程:有些情況下,基礎庫中的connect/open/write方法可能呼叫的比較頻繁,我們需要把hook的影響降到最低。所以我們只會在接收到SIGQUIT訊號後(重新發送SIGQUIT訊號給Signal Catcher前)進行hook,ANR流程結束後再unhook。
-
只處理Signal Catcher執行緒open/connect後的第一次write:除了Signal Catcher執行緒中的dump trace的流程,其他地方呼叫的write方法我們並不關心,並不需要處理。例如,dump trace的流程會在在write方法前,系統會先使用connet方法連結一個path為“/dev/socket/tombstoned_java_trace”的socket,我們可以hook connect方法,拿到這個socket的name,我們只處理connect這個socket後,相同執行緒(即Signal Catcher執行緒)的第一次write,這次write的內容才是我們唯一關心的。
-
Hook點因API Level而不同:需要hook的write方法在不同的Android版本中,所在的so也不盡相同,不同API Level需要分別處理,hook不同的so和方法。目前這個方案在API 18以上都測試過可行。
這個Hook Trace的方案,不僅僅可以用來查ANR問題,任何時候我們都可以手動向自己傳送一個SIGQUIT訊號,從而hook到當時的Trace。Trace的內容對於我們排查執行緒死鎖,執行緒異常,耗電等問題都非常有幫助。
這樣我們就得到了一個完善的ANR監控方案,這套方案在微信上平穩運行了很長一段時間,給我們評估和優化微信Android客戶端的質量提供了非常重要根據和方向。
關注我,每天分享知識乾貨