記一次Android週期性控制程式碼洩漏的排查

陶然陶然發表於2023-11-13

  滴滴國際化外賣 Android 商戶端正常迭代版本過程中,新版本釋出並且線上穩定一段時間後,突然觸發線上 Crash 報警。  

  第一次排查發現是在依賴的底層平臺 so 庫中崩潰,經過溝通了解到其之前也存在過崩潰問題,所以升級相關底層 so 版本。重新發版後短期沒有出現 Crash 大面積上報情況,只有零星上報,但不久後又發生了第二次大面積 Crash 上報。具體資訊如下圖所示:  

  在定位分析問題的過程中收穫很多,透過這篇文章分享該 Crash 的排查過程、問題根因以及一些經驗總結,希望能為讀者在遇到同型別問題時提供一些參考。

  排查流程

  Crash 描述

  Crash 的量級,從兩次高峰期的峰值來看,集中爆發的峰值為每日50次左右,第二次爆發的峰值最高為173次。同時,Crash 和影響使用者數量是一比一。  

  兩次大面積爆發的問題裝置集中在華為的三款機型,執行時間都在14天左右,記憶體情況正常,執行緒情況正常,此時還沒有相關線索指向控制程式碼,所以這個時候沒有關注控制程式碼,如下圖:  

  

  定位分析

  從整體的 Crash 描述以及 Crash 統計平臺的相關資料來分析,每隔14天就大面積爆發一次,可以確定是週期性問題,這種問題的排查難度較高。

  根據上報的錯誤日誌,明確其崩潰位置是在底層的 libpush.so 庫,同時和其維護的同學溝通後發現依賴的 so 版本出現過問題,所以我們在第一次大面積上報之後,升級了底層 so 庫版本。雖然當時增發版本後沒有明顯的 Crash 上報,但還是存在零星的 Crash 上報,這讓我們放鬆了警惕,未對問題的根因進行定位,才導致了後面更嚴重的第二次爆發。

  第二次爆發後,透過分析 Crash 統計平臺上的173次 Crash 日誌資訊發現,Crash 程式碼地址都是“000000000007ce08”,如下圖,由於靜態庫中的程式碼地址都是固定的,所以對底層 so 庫進行了程式碼定位。  

  我分析底層 so 庫程式碼習慣是使用 IDA,透過 IDA 定位到的問題程式碼如下圖:  

  找到對應問題程式碼塊,定位到直接原因是 fopen 檔案返回空,fwrite 寫入資料之前未做空判斷。由於 fopen 是呼叫系統 API,系統 API 出現問題機率極小,所以一定是業務某些異常場景導致開啟檔案失敗。

  業務進行了哪種非法呼叫引發的異常場景?我們需要定位到問題場景的程式碼執行環境和具體的使用者操作路徑。此時只有完整復現這個問題,才能找到導致 fopen 失敗的根因。

  上面我們推測是週期性問題,與業務運營側同學確認,沒有周期性的活動釋出,排除客觀因素。

  從 Crash 相關資料分析,除了定位到 libpush.so 的直接程式碼位置,沒有太好的進展,所以根據對應上報高峰的時間段排查 top5 中的其它新增 Crash,發現其中一個 Crash 從上報時間、執行時間、機型幾個緯度與直接 Crash 資訊高度一致,大機率是同一個問題導致,檢視對應的堆疊資訊。  

  綜合定位到的底層 so 庫的問題程式碼,分析原因是控制程式碼超限後 fopen 開啟失敗導致為空,綜合 App 長時間執行分析,控制程式碼洩漏問題有14天(左右)的週期性共性條件,同時輸出了佔比top3問題機型的控制程式碼上限都是1024。我們知道目前國內大多機型的控制程式碼上限是10000+,不過由於我們自己的業務形態是基於定製裝置的,定製裝置更新換代較慢,機型較老,所以控制程式碼上限較低,最終導致了問題主要集中在業務採購的定製系統裝置,非定製的使用者裝置控制程式碼雖然也會異常增加,但是在一個版本週期內是遠遠達不到控制程式碼上限的,也就不會出現崩潰問題。

  從以上資訊推測崩潰問題是控制程式碼洩漏導致超過系統上限,剩下的就是如何復現使用者的操作路徑和正向程式碼根因定位了。

  問題復現

  由於是控制程式碼洩漏問題,輸出開啟的本地 fd 後,無法直接定位到具體 so。又因為是新版本新增問題,所以透過反向排除法,進行版本 diff,排查更新的程式碼。而業務程式碼未涉及控制程式碼操作,故逐個進行依賴 SDK 還原,裝置定時輸出 fd 數量進行分析。

  測試版本為線上有問題版本:

  第一次測試記錄:測試耗時:12h+,fd: 227 → 272

  第二次測試記錄:測試耗時:15h+,fd: 227 → 296

  第三次測試記錄:測試耗時:24h,fd: 227 → 313  

  控制程式碼數量明顯增加。

  測試版本為還原更新的SDK版本:

  第一次測試記錄: 測試耗時:15h,fd: 193 → 198

  第二次測試記錄:測試耗時:21h,fd: 193 → 204,切換過賬號一次

  第三次測試記錄:測試耗時:39h,fd: 193 → 210

  控制程式碼數量無明顯增加。

  對比 SDK 還原前後兩個版本執行的資料得出結論,控制程式碼異常增加的根因線上上版本所依賴的3個 SDK。這時候只需再依次對比3個依賴 SDK 的資料,定位到具體的問題 SDK 是時間的問題。同時我們也將排查定位進展同步給各自相關基礎 so 庫維護同學,發現之前其中一個 so 庫的歷史版本存在過控制程式碼洩漏問題,和相應同學溝通了解相關資訊。透過梳理底層 so 的控制程式碼洩漏呼叫邏輯,增加呼叫日誌,並對裝置控制程式碼數量持續觀察,最終發現 so 側存在一個邏輯:6小時輪詢開啟19個控制程式碼,但是開啟後沒有正常關閉釋放。

  此時問題的根因已大概定位,為了加快復現,我們把6小時輪詢時間縮短為2分鐘,執行一段時間後程式控制程式碼數量達到1024上限發生崩潰,崩潰日誌與線上崩潰日誌完全相同,正向從使用者角度復現了該問題。同時透過有無問題的兩個版本 so 跑資料,對比後也證明問題的產生是由當前 so 庫導致。至此,問題直接原因以及根本原因都已定位,問題修復後發版上線,線上驗證透過。

  為什麼問題會集中爆發在 fwrite 方法的呼叫上?原因是業務其中一個場景需要30s輪詢呼叫某個操作控制程式碼的 API,高頻呼叫 fwrite。它不能定位根本原因,不過暴露了直接原因,這也說明記憶體洩漏和控制程式碼洩漏可能會報在任何程式碼位置。這裡也讓我們初期排查問題時,偏離了方向。不過這個就是這篇文章最想強調的內容,也是最想解決的問題,當出現這類問題,作為RD,我們要重點關注些什麼?來快速糾正方向,快速定位問題。

  下面我們簡單介紹一下控制程式碼洩漏是什麼?如何處理?如何預防?Android中都有哪些常見的控制程式碼?有助於我們後期快速排查定位控制程式碼相關問題。

  什麼是控制程式碼洩漏

  控制程式碼洩漏,就是當開啟的資源未被正常釋放,導致資源不能關閉回收。因為系統會為每個程式規定最大檔案描述符上限數,一般 Linux 系統的程式最大控制程式碼上限為1024,不過現在比較新的 Android 系統,上限升到32768,我們可以透過 adb shell ulimit -n 來檢視:  

  截圖為oppo findx2的裝置檔案描述符上限數

  程式存在控制程式碼洩漏問題時,對應的資源控制程式碼不會被釋放,當達到上限時,程式崩潰,報出異常資訊。一般的異常資訊有

  Could not allocate dup blob fd

  java.lang.RuntimeException: Could not read input channel file descriptors from parcel.

  abort message 'could not create instance too many filesœ

  java.io.IOException: Cannot run program "logcat": error=24, Too many open files

  "Could not allocate JNI Env: %s", error_msg.c_str()

  "Could not open input channel pair"

  如果上報的日誌資訊包含這種,那大概就是控制程式碼洩漏導致了。

  注意:控制程式碼洩漏和記憶體洩漏這類問題不屬於業務邏輯問題,是程式分配的資源耗盡,導致再次分配時無足夠的資源進行分配,所以當程式執行時,達到對應的資源上限後,就算普通的程式碼依然會直接報錯,這個時候,上報的日誌就是對應的程式碼位置,這點容易誤導我們排查線上問題。

  如何解決控制程式碼洩漏

  上面也提過,控制程式碼洩漏和記憶體洩漏是一類問題,這類問題崩潰後,Crash 的位置可能不會明確的標出是哪裡出現問題,最終的 Crash 日誌也可能是普通程式碼。

  問題使用者操作路徑存在共性的情況(復現難度較低)

  確定問題使用者操作路徑的共性

  根據使用者操作路徑,反覆操作進行控制程式碼數量監控並本地儲存

  輸出控制程式碼異常增長情況,檢視異常增長的控制程式碼

  排查業務上,涉及控制程式碼分配的程式碼

  問題使用者操作路徑無共性的情況(復現難度較高)

  透過diff問題版本前後的程式碼,確定涉及控制程式碼分配的程式碼改動

  透過排除法進行逐一回退對比,進行控制程式碼數量監控和分析

  透過工具(so 庫可以藉助 IDA)進行問題程式碼定位,輔助分析問題

  確定問題程式碼或者問題依賴,再深入定位具體程式碼

  以上的結論是建立在沒有其它輔助手段基礎上,實際排查過程中,我們還可以透過 Crash 平臺上的輔助資訊、梳理底層 so 庫程式碼邏輯、積極與 so 側同學溝通等角度進行輔助定位,也可以加速定位到問題程式碼。

  定位控制程式碼洩露相關命令和程式碼

  1.檢視裝置控制程式碼上限:adb shell ulimit -n

  2.輸出程式控制程式碼:

  private void listFd() {String tag = "FD_TAG";File fdFile = new File("/proc/" + android.os.Process.myPid() + "/fd");File[] files = fdFile.listFiles();int length = files.length;MerchantLogUtils.logFd(tag, "fd length: " + length);StringBuilder sb = new StringBuilder();for (int i = 0; i < length; i++) {File f = files[i];String strFile = Os.readlink(f.getAbsolutePath());sb.append(strFile + "\n");}}

  控制程式碼洩漏常見問題以及分類

  1.Andorid 常見的控制程式碼洩漏問題

  HandlerThread 的使用,要記得 release

  IO 流操作開啟後要在 finally 中 close

  SQLite 資料庫操作要把 cursor 例項 close

  InputChannel相關,即 WindowManager.addView反覆呼叫時,記得 removeView

  Bitmap 進行 IPC

  這是常見的 Android 控制程式碼洩漏的點,具體的原理分析資料很多。

  2.部分 Android 的檔案描述符的具體分類,可以縮小排查範圍  

  如何預防和監控控制程式碼洩漏

  上面都是如何解決,其實更希望問題線上下或者灰度期間就暴露並且解決掉。

  預防

  程式碼開發過程中,涉及控制程式碼操作需“慎之又慎”,要經過充分的自測

  程式碼 CR 的 CheckList 增加“控制程式碼相關程式碼的重點CR”,如上面介紹的 Android 常見的控制程式碼洩露的場景

  版本需求改動涉及控制程式碼建立時,QA 需重複多次操作對應路徑,檢視控制程式碼情況

  測試粒度覆蓋控制程式碼,測試包定時輸出控制程式碼指標,達到對應的閾值後報警,端上同學介入排查控制程式碼增長原因

  自動化測試中增加長時間執行時控制程式碼數量的觀測

  觀測

  線上定時獲取裝置控制程式碼數量並且上報,建立控制程式碼均值觀測

  根據線上穩定期間的控制程式碼數量均值,設定合理的報警閾值,及時感知到該類線上問題

  總結

  從 Crash 統計平臺的相關資料可以看到,所有的 Crash 都在底層 so 庫裡,這類問題我們除了分析 Crash 統計平臺提供的相關資訊,是否還有其它可以輔助定位問題的手段呢?

  在全面定位過程中,也需要關注相同時間段的其他新增 Crash 資訊,確認是否有相關性,如同為新增、機型、時間、地區、使用者操作場景等等之間的相關性。

  透過對應的工具進行三方 so 的程式碼邏輯梳理,綜合已有資料進行分析,如本次排查過程中使用 IDA 分析底層 so 庫程式碼,定位到直接原因為 fwrite 之前未做空判斷。由於系統 API 出現問題機率極小,如果之前處理過此類問題,綜合 Crash 統計平臺上的機型、執行時間等,從這就可以初步定位是控制程式碼洩漏問題。

  積極同步進展以及所有可能的分析到底層 so 側同學,我們定位到控制程式碼洩漏,反向排除法定位問題過程中也同步到底層so同學,發現之前另一個底層so 解決過控制程式碼洩漏問題,提前定位到問題 so,減少了問題定位的人力浪費。

  在與相關同學溝通時,要帶有自己的分析和判斷,有著重點的討論,這樣會極大地提升排查效率。

  綜上,當問題發生在平臺底層庫時,同時又無明顯的使用者操作路徑,透過程式碼定位工具先嚐試定位直接原因,再透過 Crash 平臺上各方面的資訊對比分析其相關性,配合測試資料,可以加快定位這類週期性問題。

來自 “ 滴滴技術 ”, 原文作者:趙楠;原文連結:https://server.it168.com/a2023/1109/6828/000006828725.shtml,如有侵權,請聯絡管理員刪除。

相關文章