線程池優化與監控
作者:明義、光線
部門:業務技術
一、前言
隨着應用不斷的迭代更新,零售工程內異步任務逐漸增多,包括網絡請求、本地 DB 操作、輪詢等任務使用的都是同一個線程池,線程池極易打滿(比如網絡一慢就容易阻塞線程池),導致任務堆積,造成「操作卡頓」的現象。用戶看到操作沒反應,可能會更頻繁的嘗試操作,再加上不斷輪詢的任務,應用的線程池隊列會快速積壓更多的任務,「卡頓」現象加劇,單一線的線程池已經無法支撐業務需求:
-
任務沒有隔離,異步任務相互影響主要是長耗時影響短耗時任務
-
輪迴任務開銷大:沒有統一的輪詢處理方式,業務方需要自己創建線程或線程池(有些乾脆就在默認的 I/O 線程池進行輪詢),應用中存在大量的自建線程池,增加無謂的資源消耗
-
缺少監控:缺乏線程池的監控和日誌,線程池的運行狀況和健康度無法衡量,一旦出現「卡頓」問題,排查非常困難,完全不知道是哪些任務造成的問題,傷害用戶體驗的同時,也極大的消耗開發人員的精力
圖注:短時間任務暴漲的情況,在幾毫秒內觸發多次任務
二、整體設計
目標:
-
任務隔離,避免耗時任務影響交互
-
統一輪詢,減少資源開銷
-
任務監控,防止業務方使用錯誤
-
信息採集與監控,快速定位排查問題
優化的核心在於分離和監控
分離:
對原有的 I/O 線程池進行拆分,分離出不適合放在這個線程池的任務,保證 I/O 線程池能對大量、快速的本地任務給予更好的支持,而分離的關鍵就在於分離出「慢」和「多」的任務:
-
避免耗時短的任務等待耗時長的任務
-
避免頻繁、大量執行的任務佔據大量的線程池資源
具體的策略包括:
-
將網絡請求的線程分離出來,放入單獨的線程池中,避免因網絡任務慢阻塞本地的快速任務
-
將輪詢任務分離出來,當輪詢的任務耗時過長或者線程池打滿的場景下,會極速加劇線程池的惡化,最終讓主流程完全癱瘓,輪詢任務不適合放在通用的線程池中,通過將它放入通用的輪詢任務線程池,統一化的對輪詢任務進行管理
監控:
除了已知的網絡任務 & 輪詢任務需要分離出 I/O 線程池之外,還需要增加對線程池的監控,進一步分離出不適合放在 I/O 線程池的任務(同樣是「慢」和「多」)
-
通過監控每個任務執行的時長,分離出長耗時(即「慢」)的任務,分析出是因爲邏輯 bug 還是本身的複雜度正常佔用,如果是正常佔用就考慮是否合適繼續放在默認的 I/O 線程池,最終目標是達成對「慢」的優化
-
另外零售工程中還存在短時間大量重複任務堆積的現象,當線程池接近或達到負荷狀態時,監控線程池中的任務,找到批量的任務,然後進行優化,最終目標是達成對「多」的優化
-
通過上面的方式將「慢」的任務分離出去之後,並不代表這樣就完事了,這些任務可能會消耗大量的系統資源,所以同樣需要統一對這個線程池進行監控(主要是網絡任務的監控),把慢網絡請求找出來,達到對「慢」的進一步優化
-
通過 API 監控業務方使用合理性,比如:使用多線程輪詢的合理性
三、技術實現
2.1 線程管理庫
目標:
-
管理工程內所有線程池與線程創建
-
子線程任務監控
-
線程池隔離任務分開執行,使用不同線程池執行不同任務
-
輪詢任務的統一與異常任務的過濾
-
線程池的自動最優設置
線程庫結構圖
** 線程池管理,目前會提供三種線程池:**
-
網絡線程池:針對網絡任務。更改方式:直接在網絡庫替換業務方無感知
-
IO 線程池:針對本地異步任務。更改方式:App 啓動時 Hook RxJava 線程池替換業務方無感知
-
輪詢線程池:針對輪詢任務,需要業務方的接入
線程池 API 定義
3.2 輪詢任務統一
-
禁止時間間隔小於 1 秒輪詢任務,防止高頻輪詢消耗資源
-
所有輪詢任務巡檢間隔 1 秒檢測一次
-
輪詢檢測是獨立的線程,只做輪詢檢測,有任務時死循環間隔檢測,沒有任務時則睡眠等待下次註冊執行,根據結果輪詢執行或睡眠
-
線程任務模式有兩種:
單線程回調: 表示最多佔用一個線程。例:1 秒輪詢回調 1 次,再回調前會判斷上個任務是執行完成,如果沒有執行完成則回調 pollTaskExceptionCallback(),如果已經執行完成則回調 pollTaskCallback()
多線程回調:表示最多佔用 30 個線程。例:1 秒輪詢回調 1 次,再回調前會判斷當前任務佔用線程總量,如果沒有執行完成則回 pollTaskExceptionCallback(),如果已經執行完成則回調。pollTaskCallback() 默認是單線程回調 -
業務方可以指定輪詢次數,或設置無限輪詢
-
輪詢線程池動態擴容:初始線程爲 30 個,如果使用線程等於核心線程則動態擴容 2 倍,最大限制 120 個核心線程
輪詢任務流程圖
輪詢 API 定義
任務過濾:
輪詢正常回調 pollTaskCallback() 方法,如果異常這時將回調到 pollTaskExceptionCallback() 方法。
異常任務過濾的實現:
-
記錄所有任務。
-
每次執行任務時會進行校驗,校驗監聽者是否已經把自己申請的線程全部使用,如果已經全部佔用將回調異常方法,直接到任務有一個釋放後再繼續回調。
-
任務執行完後重設記錄數據。
輪詢異常回調有三種觸發場景:
-
單線程任務沒有釋放超時。
-
多線程任務佔用線程超過最大數量。
-
輪詢線程池被全部佔滿超時。
任務過濾 - 流程圖
四、工程更改
工程更改主要分爲 5 塊:
-
網關請求線程池的替換,在構建 RxJava observable 替換成自定義網絡線程池。
-
RxJava 默認線程池的替換,App 啓動時把 RxJava Hook 設置成自定義 IO 線程池。
-
輪詢業務接入統一輪詢。
-
網線線程池使用錯誤攔截,目的防止線程使用錯誤,禁止 IO 線程網絡請求,通過攔截器方式實現。
-
線程任務監聽設置給 APM ,通過 APM 分析上報。
備註:只有輪詢需要業務方逐個替代其它都正常使用不感知。
五、信息採集與監控
-
任務提交之後記錄每個任務的調用棧信息以及任務提交的時間,之後在任務真正開始執行時會記錄任務開始的時間,任務執行完成後即可算出一個任務完整的執行時間、任務等待時間等,這樣就可以抓取出「慢」的任務
-
在任務執行完成時,如果線程池已滿並且任務的等待時間超過閾值,則會拉出線程池任務棧的信息,用於查找出異常的任務
線程池監聽 API
任務監聽實現流程
改善效果:
卡頓定義:包含主線程超過 300ms 慢方法、ANR、本地子線程操作超過 1s、線程池阻塞、頁面渲染時間超過 200ms。
卡頓次數趨勢圖
圖注:卡頓次數下降 76%
六、未來規劃
-
支持更多維度的任務監控,並增加自動報警能力
-
針對不同的機型進行最優線程池配置,最大化複用系統資源
-
逐漸完善 pthread、線程任務數量等監控能力
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/gTiffvjwUulT2nDbt68j6Q