1. 程式人生 > >《手Q Android執行緒死鎖監控與自動化分析實踐》

《手Q Android執行緒死鎖監控與自動化分析實踐》

一、問題背景

手Q每個版本上線以後研發同學都會收到各種問題反饋。在跟進手Q內部使用者反饋的問題時,發現多例問題,其表象和原因如下:

1、問題表象:“未讀不消失”、“圖片不展示”、“菊花一直在轉” 。。。

2、問題原因:死鎖導致的功能不可用。

這類由死鎖造成的功能不可用的問題,具有表象簡單但影響非常嚴重的特點。一般使用者在遇到這類問題後,除了採取殺掉程序重啟的策略,沒有其他辦法繼續使用應用。由此可見,死鎖問題對產品的影響是巨大的,那麼有沒有有效的方法能夠監控Android應用的死鎖呢?

首先想到的是使用程式碼規範來避免死鎖的發生。手Q有250多個業務模組,400w+行程式碼,這麼多業務程式碼交叉呼叫,僅通過程式碼規範,很難避免死鎖發生。

然後想到的是CodeDog的程式碼工具掃描。與CodeDog同事溝通後,發現Converity靜態掃描無法識別巢狀呼叫使用鎖的情況,而鎖的巢狀呼叫是死鎖發生場景中一個比較常見的場景。顯然通過程式碼的靜態掃描沒法解決問題。

既然現成的程式碼掃描工具無法完全解決問題,只能硬著頭皮試著自己造輪子來監控Android執行緒的死鎖問題。

下面將詳細介紹這套Android執行緒卡死監控系統。(注意這裡用的詞是“卡死”而不是死鎖。死鎖只是執行緒卡死原因中比較重要的分類,除了死鎖還有許多其他問題造成執行緒的卡死。)

二、方案詳述

2.1整體方案概述

下面是手Q自建Android執行緒卡死監控的整體方案:

這裡寫圖片描述

Android執行緒監控整體方案分為兩部分:客戶端與後臺。

1、客戶端:由監控執行緒(WatchThread)與被監控執行緒(Thread)組成。

客戶端執行緒的監控的核心主要利用執行緒的Looper方法,監控執行緒監控從被監控執行緒訊息佇列取出的訊息的執行。當監控執行緒發現一個出佇列的訊息在一定時間(3分鐘,可以自己設定)沒有執行完成,則可認定被監控的執行緒發生卡死。這時,由監控執行緒獲取執行緒持有、等待的阻塞鎖資訊,將這些資訊上報到後臺,客戶端部分便完成了使命。

2、後臺:由自動化分析工具,分析執行緒卡死的原因。原因分析完成以後,就可以進行提單,然後再跟進每個卡死問題的解決。

整個方案的關鍵是上圖中兩個標紅的部分:客戶端上報執行緒卡死的關鍵資訊,後臺自動化分析卡死原因。下面將詳細介紹這兩部的實現。

2.2 客戶端上報

2.2.1卡死資訊

Android執行緒卡死監控中客戶端上報執行緒卡死的關鍵資訊,那麼哪些資訊是關鍵資訊呢?答案很明顯:執行緒資訊與執行緒持有、等待鎖的資訊。

在如何獲取這兩類資訊之前,先來分析一下Java中鎖的分類與特點。

Java中鎖的分類有自旋鎖、可重入鎖、阻塞鎖等等分類,其中能夠造成執行緒卡死的鎖,只有阻塞鎖。對於阻塞鎖有如下三種:

這裡寫圖片描述

根據這三種鎖是否有執行緒等待方和執行緒持有方,為了達到分析執行緒卡死的目標,需要獲取如下資訊:

1、對於sychronized鎖:需要獲取其持有執行緒和等待執行緒。

2、對於LockSupport鎖:需要獲取其持有執行緒和等待執行緒。

3、對於Object鎖:需要獲取其等待執行緒。

那麼接下來的重點就是如何獲取執行緒的資訊與鎖的資訊上報。

2.2.2 上報方案1:抓取java堆疊—不可行

首先想到的方案是:抓取java的堆疊進行上報。下面是抓取的java堆疊與其對應的程式碼:

這裡寫圖片描述

上圖中右的程式碼中121行已經獲取了sychornized鎖,但是左邊的java堆疊中並沒有展示對應鎖的資訊,故使用抓取java堆疊的方式不可行。

既然使用Java抓取堆疊資訊不可行,有沒有其他方案呢?答案:有。

2.2.3上報方案2:抓取系統Traces.txt——可行

既然抓取java的堆疊行不通,只能尋求其他解決方案。突然想到,Android在發生ANR時有一套系統機制:

1、Android應用發生ANR時,系統會發出SIGQUIT訊號給發生ANR程序。

2、系統訊號捕捉執行緒觸發輸出/data/anr/traces.txt檔案,記錄問題產生虛擬機器、執行緒堆疊相關資訊。

3、這個trace檔案中包含了執行緒資訊和鎖的資訊,藉助這個trace檔案可以分析卡死的原因。

由此,如果利用這個系統原有的機制,自己線上程卡死時候觸發traces檔案的形成進行上報,便可以把執行緒卡死的關鍵進行進行上報。本監控方案便是利用系統機制進行卡死資訊的抓取:

1、當監控執行緒發現被監控執行緒卡死時,主動向系統傳送SIGQUIT訊號。

2、等待/data/anr/traces.txt檔案生成。

3、檔案生成以後進行上報。

4、當然,這裡也有少數Trace檔案生成失敗的情況,但是,對於手Q大盤監控可忽略。

既然方案可行,就需要分析利用系統機制抓取的資訊(所有執行緒資訊、執行緒堆疊中鎖的資訊)是否滿足需求。下面是一個利用系統機制繼續抓取的例子:
這裡寫圖片描述

右圖程式碼中的synchonized鎖資訊已經在左邊系統dump的堆疊中,由此可見,可以利用這個堆疊進行卡死分析。那是否利用這些資訊就足夠進行執行緒卡死原因的分析了呢?

天下永遠沒有這麼便宜的晚餐。

2.2.4上報難點:Traces中沒有LockSupport鎖的持有者資訊
通過分析上報的Traces檔案與抓取鎖資訊對比,sychornized、object鎖的資訊得到解決,但是LockSupport鎖的資訊竟然缺少了持有執行緒。利用系統機制抓取的堆疊,可以獲取鎖的資訊如下表所示:
這裡寫圖片描述

下面是LockSupport鎖無法獲取持有執行緒資訊的一個例子:
這裡寫圖片描述

右圖的程式碼在執行lock.lock()之後,執行緒已經獲取了LockSupport鎖,但是從左邊的系統堆疊中卻沒有這個鎖的資訊。

這將是後續進行自動化分析的一個難點問題。那麼有沒有什麼解決方案?通過深入分析手Q的程式碼,找到了答案。

2.2.5 解決方案:主動記錄LockSupport鎖的執行緒資訊
解決問題的思路,先人工分析所有上報trace檔案,需找關鍵特徵:

1、找到發生死鎖時系統dump堆疊的關鍵特徵。

2、人工對卡死問題聚類分析,發現卡死堆疊中locksupport鎖的問題全部為資料庫問題。

3、再分析手Q的資料庫相關程式碼,發現數據庫事務LockSupport鎖的入口統一,能夠記錄獲取、等待獲取的執行緒資訊。

在上面的分析結論中,為了解決LockSupport沒有持有執行緒資訊這個難點,利用發現問題統一、程式程式碼入口統一的特點,採取下面這一招:

在系統dump的Trace檔案中人工記錄LockSupport鎖資訊。

解決方案:在資料庫相關的程式碼中新增如下記錄程式碼。
這裡寫圖片描述

上述程式碼中,將等待獲取LockSupport鎖執行緒記錄到等待列表中,獲取LockSupport鎖以後從等待列表中移除,並記錄當前執行緒(記錄當前執行緒id、name資訊)為LockSupport鎖的持有執行緒,當前執行緒釋放LockSupport鎖以後再將記錄清空。

有了上述記錄的LockSupport鎖執行緒資訊,只要在卡死形成的traces檔案最後新增這些資訊,然後再進行上報,這樣就解決了沒有LockSupport鎖持有執行緒資訊的問題。

在traces檔案最後一行新增的具體如下圖所示:
這裡寫圖片描述

當然,為了方便後續的問題分析,在trace檔案最後一行還添加了其他一些資訊,如:被卡死執行緒的名稱、系統版本號、發生時間等等。

目前,客戶端已經解決了執行緒卡死以後上報資訊不完整的問題,那麼,接下來的重點就是要識別這些卡死的原因,下面章節將詳細道來。

2.3 服務端識別
2.3.1識別方案:關鍵資訊上報,自動化分析
服務端識別方案可概述為:關鍵資訊上報,自動化分析。

關鍵資訊上報這是自動化分析的前提。在詳述自動化分析方案之前,首先回顧一下到目前為止已經獲取到的資訊。

1、Traces.txt:所有執行緒資訊、系統dump鎖資訊(synchonized、Object、LockSupport)。

2、主動記錄的LockSupport鎖資訊(持有執行緒)。

3、主動識別到被卡死執行緒。

有了這三個關鍵的資訊,接下來就是進行執行緒卡死的自動化分析,下面是自動化卡死分析的完整方案:
這裡寫圖片描述

上圖的整體識別方案,詳細步驟說明如下:

1、客戶端將主動記錄的LockSupport鎖資訊、被卡死執行緒資訊等新增到系統dump的trace檔案最後一行進行上報。

2、服務端進行自動化分析時,首先進行預處理:LockSupport鎖等待執行緒資訊預處理、執行緒堆疊proguard還原。

3、從trace檔案中提取所有執行緒持有、等待的鎖資訊,記錄到每個執行緒中。

4、提取trace檔案最後一行記錄的LockSupport鎖持有執行緒,從第3步分析的所有執行緒中找到該執行緒,並在執行緒持有鎖中加入LockSupport鎖。

5、接下來從trace檔案最後一行提取出被卡死執行緒,從被卡死的執行緒開始分析。

6、被卡死執行緒是否有等待鎖,如果無,則判定為非死鎖,進入第12步進行卡死原因分析。

7、如果有等待鎖,找到該等待鎖的持有執行緒。

8、該持有執行緒是否有等待鎖,如果無,則判定為非死鎖,進入第12步進行卡死原因分析。

9、如果有等待鎖,判斷該執行緒是否已經在遍歷列表中。

10、如果已經中遍歷列表中,判斷是否存在鎖列表迴圈,如果是,則判定為死鎖。

11、如果沒有中遍歷列表中,將該執行緒加入遍歷列表中,進入第7步進行迴圈。

12、非死鎖原因分析。目前分為:網路、檔案IO、HashMap、系統IPC、抓棧、GC、資料庫、ProcessManager、PB、後續擴充套件。

完成上述自動化分析以後,輸出執行緒卡死問題列表。對於什麼是死鎖,下面將舉例說明。

2.3.2 死鎖舉例:兩個執行緒互相等待對方已獲取的鎖導致死鎖
首先,看發生死鎖的兩個執行緒堆疊:

1、 MSF-Receiver執行緒堆疊:
這裡寫圖片描述

MSF-Receiver執行緒已經獲取資料庫LockSupport鎖,等待獲取0x18087549鎖。

2、QQ_DB執行緒堆疊:
這裡寫圖片描述

QQ_DB執行緒已經獲取0x18087549鎖,等待獲取資料庫LockSupport鎖。

將兩個執行緒獲取與等待獲取的鎖做成一個列表,如下表所示:
這裡寫圖片描述

從上表可看出:MSF-Receiver執行緒與QQ_DB執行緒互相等待對方已獲取的鎖,他們之間存在鎖列表環,判定為死鎖。

從這個死鎖的列子發現,要做自動化分析死鎖,只要能夠找到執行緒之間存在鎖列表迴圈就可以判定為死鎖。那麼自動化分析是否如想象中那麼容易呢?其實不然,在自動化分析過程中,遇到了幾個難點問題。

2.3.3 識別難點1:LockSupport等待鎖的阻塞地址不同
在自動化分析過程中,發現如下問題:

1、系統dump的執行緒堆疊中有等待資料庫事務的LockSupport鎖物件資訊。

2、但是同一個LockSupport鎖,不同執行緒阻塞時的物件地址不同。

上面兩點是什麼意思?還是按照慣例,先看兩個執行緒堆疊:
這裡寫圖片描述

手Q程式碼中所有資料庫操作,都由同一個LockSupport鎖來控制。但是,從上面的系統堆疊來看,Recent_Handler執行緒阻塞的LockSupport鎖物件地址為0x43810d48,而QQ_SUB執行緒阻塞的LockSupport鎖物件地址為0x41fcb538。

由此可見,對於同一個LockSupport鎖,不同執行緒阻塞時的物件地址不同。要進行自動化分析,如果對於同一個鎖必須要識別為同一個物件,可是上述堆疊表現卻不能完成這個任務。怎麼解決?

2.3.4 解決方案: 提取特徵,判定為同一個鎖
1、LockSupport鎖提供排程執行緒阻塞與喚醒功能。

2、不同執行緒都與各自LockSupport鎖的許可關聯,所以造成了不同執行緒阻塞物件地址不同。

既然這個問題是由於LockSupport鎖的實現原理決定,那麼是否有解決方案呢?答案是:有。

如果有辦法能夠讓這些不同的地址都指向同一個地址,只要能夠做到這件事情,那麼問題就迎刃而解。具體的解決方案的分析思路如下:

1、既然阻塞的LockSupport鎖物件地址是不同的,那麼是否可以尋找系統堆疊中LockSupport鎖物件之前的有沒有什麼共同特徵呢?

2、分析所有dump的堆疊後發現,在系統堆疊的LockSupport鎖物件之前,有相同的函式呼叫,可以提取這些關鍵的字串,將其統一特徵進行歸類。

3、在進行自動化分析時,只要發現系統堆疊中有這個字串特徵,便在當前分析執行緒鎖列表中,加入一個人為構造的地址相同的LockSupport鎖。

具體構造如下:
這裡寫圖片描述

上圖中提取了字串“SQLiteConnectionPool.waitForConnection”為等待LockSupport鎖的共同特徵,線上程的等待鎖中加入同一個“dbconnection”鎖。

這樣就解決了阻塞在同一個LockSupport鎖,不同執行緒阻塞時的物件地址不同的難點。

2.3.5 識別難點2:非死鎖問題
對於非死鎖問題,人工分析所有上報後,發現卡死原因分類較多,大致可分為:

1、網路、檔案IO、HashMap、系統IPC、抓棧、GC、資料庫、ProcessManager、PB

2、後續仍然可擴充套件分類。

對於非死鎖問題的判定,這裡提出基於堆疊關鍵詞匹配來判定問題分類的方案。

首先,預設堆疊關鍵字與問題分類對應關係,如:Scoket等。

其次,對未識別的類別的堆疊聚類,人工分析top問題找出關鍵字串進行擴充套件。

再次,完善堆疊關鍵字與問題類別對應關係。

下表示展示了目前已歸納總結出的卡死問題與對應判定關鍵字。
這裡寫圖片描述

重點案例: HashMap卡死

在分析系統dump時,發現多執行緒訪問HashMap會造成卡死,具體卡死原因如下:

1、HashMap在插入元素過多需要進行Resize步驟

2、Resize步驟包括擴容與ReHash

3、ReHash在併發的情況下可能會形成連結串列環

4、訪問HashMap連結串列環這個位置時造成卡死

解決方案:

1、保證同一時間只有單個執行緒訪問HashMap

2、多執行緒場景使用ConcurrentHashMap

三、卡死監控與自動化效果
卡死自動化分析後,會輸出卡死問題概覽列表。以11月7號為例,手Q自動化分析的執行緒卡死以後,輸出以下列卡死問題概覽:
這裡寫圖片描述

通過以上問題概覽,可以看到每類卡死問題的個數與佔比,方便問題總結。在這些問題中:

1、死鎖問題佔比最高,達到35.6%,已經全部解決。

2、其他非死鎖問題:

已經解決:IO、HashMap、網路呼叫。

暫未解決:IPC、ProcessManager、PB、抓棧、GC、檔案IO等。

通過幾個版本的監控與問題的解決,取得了良好的效果,如下所示:

這裡寫圖片描述

1、MSF執行緒卡死率0.3%降低到0.1%,公用執行緒卡死率0.51%降低到0.18%。

2、730一灰APM使用公用執行緒造成卡死問題嚴重,及時發現並推進解決。

總的來說,通過執行緒卡死的監控與自動化分析,以及對發現卡死問題的及時解決,手Q執行緒卡死率逐版本下降,卡死問題得到有效控制。

四、後續規劃
目前執行緒卡死監控任然有一些需要完善的地方:

1、目前還是需要中自動化分析問題後進行人工提單。後續需要做到自動化提單。

2、未完全記錄LockSupport鎖持有資訊,目前只是記錄資料庫,對於其他使用LockSupport鎖沒有監控。後續採用Hook LockSupport鎖的方案,記錄完整資訊,完善死鎖監控問題定位能力。

如果您覺得我們的內容還不錯,就請轉發到朋友圈,和小夥伴一起分享吧~

這裡寫圖片描述