推薦一款工具,輔助估算執行緒池參數

  • 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);
}

一般情況下,我們設置參數步驟是:

  1. 確定業務屬性,比如IO密集型、CPU密集型、混合型等。

  2. 參考理想化的執行緒計算模型算出理論值。如《Java並發編程實戰》一書中的理想化模型:

  1. 輔之以壓測等手段對參數進行逐步調優。

  2. 再高級點,我們也可以對執行緒池進行監控,並實時對參數進行調整,也即參數動態化方案。可參考: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()

 

使用方法

該工具的使用方法也很簡單:

  1. 把你的業務程式碼封裝為一個函數,放到 createTask 函數中。

  2. 設定 CPU使用率的期望值、隊列佔用記憶體的期望值。

  3. 執行,等待結果輸出。

 

下面分別展示一個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 這款工具以任務執行時的系統指標數據為基礎,計算出比較合理的執行緒池參數,給我們進行後續的壓測調參提供了相對比較合理的參考,值得推薦。