由于Web應用程式跑在Tomcat作業執行緒,因此Web應用對請求的處理時間也直接影響Tomcat性能,而Tomcat和Web應用在運行程序中所用到的資源都來自os,因此調優需要將服務端看作是一個整體來考慮,
- I/O調優指選擇NIO、NIO.2還是APR
- 執行緒池調優指的是給Tomcat的執行緒池設定合適的引數,使得Tomcat能夠又快又好地處理請求
I/O模型
I/O調優實際上是連接器型別的選擇,一般情況下默認都是NIO,在絕大多數情況下都是夠用的,
APR
除非你的Web應用用到了TLS加密傳輸,而且對性能要求極高,這個時候可以考慮APR,因為APR通過OpenSSL來處理TLS握手和加/解密,OpenSSL本身用C語言實作,它還對TLS通信做了優化,所以性能比Java高,
NIO.2
若你的Tomcat跑在Windows,且HTTP請求的資料量較大,可考慮NIO.2,因為Windows從os實作了真正的異步I/O,若傳輸資料量較大,異步I/O效果就能顯露出來,
若Tomcat在Linux,建議NIO,Linux內核沒有完善支持異步I/O,因此JVM也沒有采用原生的Linux異步I/O,而是在應用層面通過epoll模擬異步I/O模型,只是Java NIO的使用者感覺不到,
因此在Linux,Java NIO和Java NIO.2底層其實都是通過epoll實作,但Java NIO更簡單高效,
執行緒池調優
跟I/O模型緊密相關的是執行緒池,執行緒池的調優就是設定合理的執行緒池引數,
- Tomcat執行緒池的關鍵引數:

如何確定maxThreads:
- 若該引數設定小了
Tomcat會發生執行緒饑餓,并且請求的處理會在佇列中排隊等待,導致回應時間變長 - 若過大
因為服務器的CPU的核數有限,執行緒數太多會導致執行緒在CPU上來回切換,耗費大量的切換開銷,
maxThreads多少合適呢?
利特爾法則
系統中的請求數 = 請求的到達速率 × 每個請求處理時間
去超市結賬排隊,如何估算一個佇列有多長呢?
- 佇列中如果每個人都買很多東西,那么結賬的時間就越長,佇列也會越長
- 短時間一下有很多人來收銀臺結賬,佇列也會變長
因此佇列的長度等于 新人加入佇列的頻率 乘以 平均每個人處理的時間,
計算出了佇列的長度,就創建相應數量執行緒處理請求,這樣既能以最快速度處理完所有請求,同時又沒有額外的執行緒資源閑置和浪費,
假設一個單核服務器在接收請求:
- 如果每秒10個請求到達,平均處理一個請求需要1秒,那么服務器任何時候都有10個請求在處理,即需要10個執行緒
- 如果每秒10個請求到達,平均處理一個請求需要2秒,那么服務器在每個時刻都有20個請求在處理,因此需要20個執行緒
- 如果每秒10000個請求到達,平均處理一個請求需要1秒,那么服務器在每個時刻都有10000個請求在處理,因此需要10000個執行緒,
因此可以總結出一個公式:
執行緒池大小 = 每秒請求數 × 平均請求處理時間
理想情況,執行緒一直在忙著干活,沒有被阻塞在I/O等待,
實際上任務在執行中,執行緒不可避免會發生阻塞,比如阻塞在I/O等待上,等待DB或下游服務回應,雖然通過非阻塞I/O模型可減少執行緒的等待,但是資料在用戶空間和內核空間拷貝程序中,執行緒還是阻塞,
執行緒一阻塞就會讓出CPU,執行緒閑置下來,就好像作業人員不可能24h處理請求,解決辦法就是增加作業人員數量,一個人去休息另一個人頂上,即增加執行緒數,因此I/O密集型應用需要設定更多的執行緒,
執行緒I/O時間與CPU時間
至此我們又得到一個執行緒池個數的計算公式,假設服務器是單核:
執行緒池大小 = (執行緒I/O阻塞時間 + 執行緒CPU時間 )/ 執行緒CPU時間
執行緒I/O阻塞時間 + 執行緒CPU時間 = 平均請求處理時間
平均請求處理時間在兩公式都有,這說明請求時間越長,必然需要更多的執行緒,
不同的是:
- 第一個公式用每秒請求數 乘 請求處理時間
- 第二個公式用 請求處理時間 除以 執行緒CPU時間,CPU時間<請求處理時間
雖然這兩個公式是從不同的角度來看待問題的,但都是理想情況,有前提條件:
- 請求處理時間越長,需要的執行緒數越多,但前提是CPU核數要足夠,如果一個CPU來支撐10000 TPS并發,創建10000個執行緒,顯然不合理,會造成大量執行緒背景關系切換
- 請求處理程序中,I/O等待時間越長,需要的執行緒數越多,前提是CUP時間和I/O時間的比率要計算的足夠準確
- 請求進來的速率越快,需要的執行緒數越多,前提CPU核數跟得上
實際場景下如何確定執行緒數
先用上面兩公式估算出理想執行緒數,再壓測調整,達到最優,
一般若系統TPS要求足夠大,用第一個公式算出來的執行緒數往往會比公式二算出來的要大,我建議選取這兩個值中間更靠近公式二的值,
即先設定一個較小的執行緒數,然后進行壓測,當達到系統極限時(錯誤數增加,或者回應時間大幅增加),再逐步加大執行緒數,當增加到某個值,再增加執行緒數也無濟于事,甚至TPS反而下降,那這個值可以認為是最佳執行緒數,
執行緒池中其他引數,最好就默認值,能不改就不改,除非在壓測的程序發現瓶頸,
如果發現了問題就需要調整,比如maxQueueSize,如果大量任務來不及處理都堆積在maxQueueSize中,會導致記憶體耗盡,這個時候就需要給maxQueueSize設一個限制,當然,這是一個比較極端的情況了,
再比如minSpareThreads引數,默認25個執行緒,如果發現系統在閑的時候用不到25個執行緒,就可以調小一點;如果系統在大部分時間都比較忙,執行緒池中的執行緒總是遠遠多于25個,這個時候你就可以把這個引數調大一點,這樣執行緒池就不需反復創建和銷毀執行緒,
調優很多時候是在找系統瓶頸
假如有個狀況:系統回應比較慢,但CPU的用率不高,記憶體有所增加,通過分析Heap Dump發現大量請求堆積在執行緒池的佇列中,請問這種情況下應該怎么辦呢?
應該懷疑大量執行緒被阻塞了,應該看看web應用是不是在訪問外部資料庫或者外部服務遇到了延遲,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/291691.html
標籤:其他
上一篇:? 就這?TypeScript其實并不難!(建議收藏)?
下一篇:cgb2106-day06
