一次tomcat壓測調優記錄

1. 前言

該tomcat web應用承擔集團登錄註冊頁面功能,對性能有一定要求,由於先前沒有太多相關經驗(只壓測過一個dubbo服務),這次調得比較艱辛,便做個記錄。

2. 調優過程

起初沒有給運維任何tomcat配置要求,同時也沒留意去確認tomcat配置,這個導致了後續壓測過程各種詭異的問題。

a.在壓測初期,持續請求10分鐘左右出現無請求進來,netstat查看的tomcat所在服務器存在大量CLOSE_WAIT的連接。 
CLOSE_WAIT的連接一般是自己程序中缺少關閉連接等引起,但是查看程序也沒發現哪裏沒有關閉,而且大多CLOSE_WAIT是與瀏覽器端的http協議下的tcp連接。後經運維排查是centos自身的BUG引起,升級到centos-release-6-6.el6.centos.12.2.x86_64後解決。

其中對於CLOSE_WAIT和TIME_WAIT的TCP連接起初一直不太理解是怎麼出現,怎麼解決,後詳細查看TCP四次揮手斷開連接瞭解了整個過程。 (圖片來自網絡)
TCP四次揮手 
比如客戶端應用程序發起CLOSE信息,服務端接收到後進入CLOSE_WAIT狀態並做ACK,之後服務端程序發起CLOSE信息,客戶端接收到之後進入TIME_WAIT,服務端收到客戶端的ACK之後進入CLOSED狀態,客戶端TIME_WAIT需要等待到超時才進入CLOSED狀態。

基於此,服務器端出現大量CLOSE_WAIT不是一個正常的狀態,首先需要確認CLOSE_WAIT狀態對方的IP,再查看這個IP對應的代碼是否缺少關閉連接。 
但是如果出現大量TIME_WAIT,不是太要緊,只要不佔滿句柄就行,如果真的佔滿了可以嘗試修改內核TCP超時時間和TCP的TIME_WAIT重用。

b.然後壓測500個併發出現connection timeout和read timeout,這種情況基本是在請求數超過了配置的最大值,一開始找運維排除nginx和vm的限流,然後再查看tomcat的限制,發現tomcat未配置最大線程數,默認情況最大線程數是200,最大等待隊列100,然後修改tomcat的server.xml配置

<Connector port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" connectionTimeout="30000"
        enableLookups="false" maxThreads="2048" minSpareThreads="100" acceptCount="512" URIEncoding="UTF-8"/>12

調整protocol使用nio的,調整最大線程(maxThreads)爲2048用於壓測,最小空閒線程數(minSpareThreads)100,接收請求隊列最大(acceptCount)爲512 
,到這裏壓1000或者2000併發都不會出現拒絕請求的情況。

這裏再細化下connection timeout和read timeout,connection timeout處於連接還沒建立就超時的狀態,如果不是網絡問題,多半是maxThreads+acceptCount不夠處理併發請求,可以嘗試增加這兩個值;read timeout是建立連接後,一種是等待在隊列太久沒處理導致超時、一種是程序自身執行時間太久,可通過access log記錄請求執行時長排查。

c.再壓一段時間後,出現請求響應慢,請求進不來,此時大多是connection refused(由於先前調整的線程池,一直還在排查線程池問題),後來查看gc日誌發現一直在做full gc並且老年代內存無法釋放,由於沒有指定過gc方式,當時以爲是gc引起,所以先調整了gc配置爲cms 
記錄gc日誌和服務掛掉時dump內存配置

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/vol/logs/heap.bin -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:/vol/logs/gc.log1

修改爲cms的gc方式的配置

-XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=10 -XX:+CMSClassUnloadingEnabled -XX:+CMSParallelRemarkEnabled -XX:MaxTenuringThreshold=15 -XX:CMSInitiatingOccupancyFraction=70 1

對CMS的GC理解不深,大致知道默認的gc方式是阻塞應用,CMS採用併發標記清理的方式,後續再翻書學習下。TODO

d.通過修改gc方式得到了一定的緩解,在壓測不到30分鐘左右時發現又出現吞吐量降低,響應非常慢的問題; 
大致知道是有很多對象沒釋放,通過jmap查看前20個佔用最大的對象(./jmap -histo pid | head -n 20 ),好像都還好,最大的是session不過也才300多M(估計是當時剛好不在壓力期間已經被釋放完了); 
然後在出現慢的時候dump了全部內存下來,但是實在太大了(5G),下載太久; 
之後把堆內存調整爲1G並壓到出現慢,查看dump也是session最大,佔百分之八十左右,這時候還沒意識到是session的問題,以爲1G太小看不出根本問題; 
再把堆內存調整爲2G並壓到出現慢,查看dump發現session佔了1.5G,突然靈光一閃,我這邊request session是封裝放到redis的,本地的tomcat session佔這麼大是不對的。。。(閃得也太遲了點吧),然後把tomcat的session超時時間設置成一分鐘果然不會再出現頻繁FULL GC的問題。再查看代碼,原來是封裝的session的地方腦抽的寫了個super.getSession(true)。默認配置下每次請求來都會生成新的session,並且超時時間是30分鐘,在內存中佔30分鐘才被銷燬!!

看起來上面的問題都非常明顯。。。但是在一個壓測環境下去找這些問題源頭是什麼真是麻煩死了。。

e.好了,最後一步,光明就在不遠的前方。壓測結果不算太理想,tps在4800左右,不過最起碼長時間壓下來算是穩定了。之後通過jprofile工具查看cpu的消耗在哪來分析單次請求耗時點。 
這裏寫圖片描述 
jprofile去連接遠程機器時需要在遠程機器開啓agent,執行bin/jpenable後選擇是給gui訪問然後填寫端口即可。

f.這裏再記錄下穩定壓測後想做一些優化遇到的問題: 
這邊在嘗試提高TPS的時候,發現查看所有的壓測指標資源貌似都未被佔滿,但是吞吐量始終上不去(應該是哪個未留意到的資源被佔滿了),tomcat線程池一半不到,cpu百分之70左右浮動,堆內存1G不到,磁盤IO在百分之八左右,帶寬佔用很低(速度在80k/s)tcp連接數在1W+ 
排查這個問題時首先懷疑到可能是使用到的一個後端http服務跟不上,通過mock掉該服務後無明顯提升 
後懷疑是tomcat沒配置正確,然後空壓一個test.jsp,tps在2W+ 
再懷疑是redis沒使用正確,在空test.jsp前使用一個redis session filter,相當於只壓redis,tps也在1W8到2W+ 
這個時候已排除了所有的外圍服務,確認了是自身程序的問題,通過jprofile去查看耗時較大的一個函數,發現ua解析較慢,通過將解析結果放到threadlocal,tps得到小幅增長,從4800到5000+ 
然後發現log4j的block較多,通過將log4j的級別調整到ERROR後,tps一下能到7000+,但是還是不能理解,後有同事說可能是磁盤IO次數的限制,這個當時沒有關注到的點,但是環境已被撤除。

3. tips

3.1 壓測開始前準備 
設定壓測業務場景:比如用戶從加載登錄頁面到使用賬號密碼登錄完成 
準備壓測環境:與線上同等配置的服務器以及數據量,資源無共享的純淨環境 
根據線上需求制定壓測目標:比如TPS>1000,平均響應時間<100ms,錯誤率<0.01%,穩定運行,CPU<80%,無內存溢出,Full GC間隔時長>30分鐘,gc時長<150ms 
配置資源數:各線程池、連接池大小,內存分配大小,tomcat連接數

3.2 制定壓測目標 
TPS >= (80% * 單日訪問量) / (40% * 24 * 60 * 60),線上80%的請求量幾種在40%的時間,根據線上情況進行調整

3.3 如何配置資源數 
根據平均響應時間和TPS計算tomcat線程池大小,TPS/(1/RT),比如TPS>=5000,平均響應時間<=100ms,線程池大小配置5000/10,再往上調整CPU數目的整數比例(這種計算方式不一定好,主要響應時間這個不太好定,上下會相差較大,) 
數據庫連接池或其他線程池大小可先設定爲tomcat線程大小的一半,根據壓測結果再進行調優

3.4 壓測指標及對應查看工具 
CPU,top/visualvm 
內存,free查看系統內存使用情況/jmap可查看最大使用的內存對象以及dump全部堆/visualvm/mat查看dump分析內存泄露對象及其引用 
磁盤IO,iostat 
磁盤空間,df -hl 
網絡帶寬,iptraf 
連接數,netstat 
線程,top -Hp pid/jstack pid/visualvm/tda查看線程dump/jprofile監控實時線程block情況 
FULL GC,-Xloggc:/path/gc.log開啓gclog後查看gclog、jstat -gc 
響應時長,jprofile查看耗時較大的函數

3.5 定位瓶頸分三步走 
檢查壓測端瓶頸,可通過壓一個恆定標準測試(比如壓空的test.jsp),基本就在壓測初期檢查一次就差不多 
通過查看壓測指標變化情況定位資源瓶頸,再根據資源瓶頸定位程序瓶頸 
比如根據監控顯示每次GC後堆內存呈上漲趨勢,可在兩次GC之後分別查看佔用內存較大的對象,對比這些對象的數目和大小,如果有明顯上漲較厲害的,可特別查看下 
TODO圖片 
在第二步查看問題中,可能出現影響因素較多的情況,這樣可通過mock掉部分懷疑的因素或者單壓某懷疑因素 
比如系統中使用的外圍服務包括數據庫、redis,現在懷疑可能是查數據庫或者在查redis的時候導致吞吐量上不去,此時可通過mock掉數據庫的查詢查看下吞吐量是否有提升,如果有提升則說明多半是數據庫查詢導致

3.6 優化 
在定位到瓶頸點之後,相對來說優化就沒那麼複雜,無外乎對瓶頸資源的少用、複用和交換資源。 
比如在程序中一個請求下來會查詢N次賬號信息,可以考慮把該賬號信息放置到緩存或者內存之類的。 
可以從業務角度去考慮優化,比如某個擴展業務直接影響到主業務的性能,且該擴展業務本身不是那麼重要,可以考慮通過縮減該擴展業務的方式做到優化。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章