推薦一款工具,輔助估算執行緒池參數
- 2022 年 10 月 5 日
- 筆記
前言
相信接觸過並發系統的小夥伴們基本都使用過執行緒池,或多或少調整過對應的參數。以 Java 中的經典模型來說,能夠配置核心執行緒數、最大執行緒數、隊列容量等等參數。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
一般情況下,我們設置參數步驟是:
-
確定業務屬性,比如IO密集型、CPU密集型、混合型等。
-
參考理想化的執行緒計算模型算出理論值。如《Java並發編程實戰》一書中的理想化模型:
-
輔之以壓測等手段對參數進行逐步調優。
-
再高級點,我們也可以對執行緒池進行監控,並實時對參數進行調整,也即參數動態化方案。可參考:Java執行緒池實現原理及其在美團業務中的實踐
工具推薦
本文則推薦一款工具,它不關心任務內部是如何實現的,而是通過計算運行時的各種系統指標(包括 CPU計算時間、IO等待時間、記憶體佔用等)來直接計算執行緒池參數的。我們可以直接在這些參數的基礎上,再配合壓測進行調優,避免盲目調參。
這個工具叫做 dark_magic,直譯就是黑魔法,源碼參見 //github.com/sunshanpeng/dark_magic。裡面的備註已經很詳細,本文不再贅述。只提一下系統指標的計算方式。
指標的計算方式
CPU計算時間 和 IO等待時間 的計算:
-
先執行兩遍任務,進行預熱。
-
獲取當前執行緒的 CPU計算時間,記為 C1
-
再執行一遍任務
-
獲取當前執行緒的 CPU計算時間,記為 C2
-
計算當前任務執行需要的 CPU計算時間:C2 – C1
-
計算當前任務執行中的 IO等待 時間:總耗時 – CPU計算時間
其中,計算當前執行緒的 CPU計算時間使用 rt.jar 包中的方法:
ManagementFactory.getThreadMXBean().getCurrentThreadCpuTime()
記憶體佔用的計算:
-
生成1000個(可配置)任務加入到阻塞隊列中
-
循環調用 15次(可配置) System.gc() 函數,觸發gc
-
記錄目前的記憶體使用情況,記為 M0
-
再次生成1000個(可配置)任務加入到阻塞隊列中
-
循環調用 15次(可配置) System.gc() 函數,觸發gc
-
記錄目前的記憶體使用情況,記為 M1
-
計算當前任務執行需要的記憶體:M1 – M0
其中,計算記憶體使用 rt.jar 包中方法:
Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
使用方法
該工具的使用方法也很簡單:
-
把你的業務程式碼封裝為一個函數,放到 createTask 函數中。
-
設定 CPU使用率的期望值、隊列佔用記憶體的期望值。
-
執行,等待結果輸出。
下面分別展示一個CPU密集型和IO密集型的輸出(我們設置的 CPU 使用率期望值為 60%,隊列佔用記憶體的期望值為 10MB ):
# CPU密集型
Target queue memory usage (bytes): 10240
createTask() produced threadpool.AsyncCPUTask which took 40 bytes in a queue
Formula: 10240 / 40
* Recommended queue capacity (bytes): 256
Number of CPU: 8
Target utilization: 0.59999999999999997779553950749686919152736663818359375
Elapsed time (nanos): 3000000000
Compute time (nanos): 2949786000
Wait time (nanos): 50214000
Formula: 8 * 0.59999999999999997779553950749686919152736663818359375 * (1 + 50214000 / 2949786000)
* Optimal thread count: 4.79999999999999982236431605997495353221893310546875000
# IO密集型
Target queue memory usage (bytes): 10240
createTask() produced threadpool.AsyncIOTask which took 40 bytes in a queue
Formula: 10240 / 40
* Recommended queue capacity (bytes): 256
Number of CPU: 8
Target utilization: 0.59999999999999997779553950749686919152736663818359375
Elapsed time (nanos): 3000000000
Compute time (nanos): 55528000
Wait time (nanos): 2944472000
Formula: 8 * 0.59999999999999997779553950749686919152736663818359375 * (1 + 2944472000 / 55528000)
* Optimal thread count: 259.19999999999999040767306723864749073982238769531250000
針對執行緒數的計算而言:
-
對於 CPU 密集型任務,IO等待時間(Wait time) 遠遠小於 CPU計算時間(Compute time)。計算出來的推薦核心執行緒數為 4.8。
-
對於 IO 密集型任務,IO等待時間(Wait time) 遠遠大於 CPU計算時間(Compute time)。計算出來的推薦核心執行緒數為 259。
而隊列大小與任務中使用的對象大小有關,這裡的記憶體使用是通過計算 gc 執行前後的記憶體大小差異得到的(本文中的例子均為 40 B)。由於該演算法內部使用 System.gc() 觸發 gc。但由於 gc 不一定真的會立刻執行,所以拿到的隊列結果可能不一定準確,只能作為粗略參考。
總結
總的來說,dark_magic 這款工具以任務執行時的系統指標數據為基礎,計算出比較合理的執行緒池參數,給我們進行後續的壓測調參提供了相對比較合理的參考,值得推薦。