線程池
web 應用中的線程池大小決定了在指定時間內能夠處理的併發請求數。如果一個 web 應用接收到的請求數高於線程池大小,多出來的請求將進入隊列等待,或被拒絕。請注意併發和並行不是一個概念。併發請求指的是正在處理中的請求數量,在某個時間點,只有其中的一小部分能夠得到 CPU 執行。而並行請求指的是正在處理的請求數量,在某個時間點,所有請求都在被 CPU 執行。
在非阻塞型 IO 應用中,比如 NodeJS,單個線程(進程)能夠同時處理多個請求。多核 CPU 處理器下,通過增加線程或進程數能夠處理並行請求。
在阻塞型 IO 應用中,比如 SpringMVC,單個線程只能同時處理一個請求。要同時處理多個併發請求的話,我們必須增加線程數量。
計算密集型應用
在計算密集型應用中,線程池的大小應該等同於主機中 CPU 的數量。再添加更多線程將會打斷請求的處理,因爲線程的上下文切換也會延遲響應時間。非阻塞型 IO 應用將會是 CPU 密集型的,因爲在請求得到處理的時候沒有線程等待時間。
IO 等待應用
決定 IO 等待應用的線程池大小會由於依賴於下游系統的響應時間而變得更加複雜,因爲一個線程在其他系統響應之前始終是阻塞的。我們不得不像《應答者模式:I/O 阻塞型應用》中討論的那樣去增加線程的數量以提高 CPU 利用率。利特爾法則
利特爾法則應用於非技術領域,比如銀行,以估算處理進入銀行客戶所需要的銀行出納櫃檯的數量。利特爾法則:在一個穩定的系統中,長時間觀察到的平均顧客數量 L,等於長時間觀察到的有效到達速率,λ,與平均每個顧客在系統中花費的時間之乘積:L = λW。
適用於 web 應用的利特爾法則:一個系統中線程的平均數量(Threads),等於 web 請求的到達速率(WebRequests per sec),與平均每個處理的響應時間(ResponseTime)的乘積。
Threads = 線程的數量
WebRequests per sec = 一秒內能夠處理的 web 請求數
ResponseTime = 處理一次 web 請求所需要的時間
Threads = (WebRequests/sec) X ResponseTime
儘管上邊這個公式提供了處理進入請求的線程個數,它並沒有提供線程數和 CPU 核心數之間的比率信息,比如一個 x 個 CPU 的主機需要分配多少個線程。
測試決定線程池大小
要找出合適的線程池大小,需要在吞吐量和響應時間之間進行權衡。先以一個最小值開始測試:一個 CPU 一個線程(也就是線程池大小 = CPU 個數),應用線程池大小與下游系統平均響應時間成正比增長,直到 CPU 使用率飽和或者響應時間開始退化爲止。下圖指出了請求數、CPU 以及響應時間等指標之間的關聯關係。
CPU Vs 請求數演示了在增加 web 應用負載時的 CPU 利用率。
響應時間 Vs 請求數圖演示了增加 web 應用負載對響應時間的影響。
綠點指出了最佳吞吐量和響應時間。
線程池大小 = CPU 個數
上圖描述的是 IO 等待型應用在線程數等於 CPU 數時的情況。應用的線程在等待下游系統響應時發生了阻塞。由於線程都阻塞住了,系統響應時間因請求進入等待隊列而被拉長。由於所有線程都處於阻塞狀態,應用開始拒絕請求,儘管 CPU 使用率還很低。
線程池很大
上圖描述的是 IO 等待型應用在 web 應用中創建了很多線程的情況。由於有很多數量的線程,線程的上下文切換將會很頻繁。由於不必要的線程上下文切換,儘管吞吐量還沒升上去的時候應用的 CPU 使用率就已經很高了。響應時間由於被請求的處理被線程的上下文切換所打斷而被拉長。
最佳線程池大小
上圖描述的是 IO 等待型應用在 web 應用中創建了合理數量的線程的情況。CPU 得到了有效利用,具備良好的吞吐量和較少的線程上下文切換。我們可以看到由於更少的打斷(上下文切換),請求處理更加有效,應用有一個良好的響應時間。
線程池隔離
對於大多數 web 應用而言,只有少數幾種類型的 web 請求會花費比較長的處理時間。這些慢的請求處理可能會拖累所有線程,並降低整個應用的性能。處理這種問題的兩個方案是:
- 爲慢處理的 web 請求設置在一臺獨立的主機;
- 在同一個應用中爲慢處理的 web 請求分配一個獨立的線程池;