前言 相信接觸過併發系統的小伙伴們基本都使用過線程池,或多或少調整過對應的參數。以 Java 中的經典模型來說,能夠配置核心線程數、最大線程數、隊列容量等等參數。 public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, lon ...
前言
相信接觸過併發系統的小伙伴們基本都使用過線程池,或多或少調整過對應的參數。以 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,直譯就是黑魔法,源碼參見 https://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 這款工具以任務執行時的系統指標數據為基礎,計算出比較合理的線程池參數,給我們進行後續的壓測調參提供了相對比較合理的參考,值得推薦。
『註:本文來自博客園“小溪的博客”,若非聲明均為原創內容,請勿用於商業用途,轉載請註明出處http://www.cnblogs.com/xiaoxi666/』