記一次生產環境性能壓測優化的經歷
對線上服務進行性能壓力測試的一次優化過程。
項目背景:
1.服務器的硬件配置(48核120G內存2T硬盤);
2.網絡部署結構,用戶請求報文首先進入負載均衡Nginx,Nginx后端負載兩台Tomcat。
現象描述:
對線上的兩台服務器做性能壓測時,發現單台Tomcat的QPS達到600左右處理業務就明顯變慢,一次請求處理時間大約上升到七秒左右(正常情況下一秒內就處理完成),給人的感覺就是Tomcat跑不動。
優化過程:
1.查看Tomcat和Nginx各自的log日志,發現Nginx的日志中有大量的“worker_connections are not enough while connecting to upstream”,錯誤信息已明確給出了提示,因此修改配置文件nginx.conf,修改為:
worker_processes auto; events { worker_connections 10240; }
2.繼續壓測時,當QPS達到600時,發現Tomcat處理業務仍然很慢,查看ngxin的日志一切正常並無任何線索,因此,把重心轉向Tomcat,查看Tomcat日志,發現日志打印的非常慢,特別是定位到兩條相隔很近的日志間所需時間都在2秒左右,而且這兩條日志在代碼層面相隔很近,中間也無耗時的業務邏輯。
此時的第一反應是jvm的參數設置的不合理,於是就去查看gc的日志,並改動JVM的參數,但是多次調優JVM后QPS仍然未有明顯提升。修改后的jvm參數:
JAVA_OPTS="-server -Xmx32g -Xms32g -Xmn12g -Xss256k -XX:SurvivorRatio=6 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:MaxGCPauseMillis=n -XX:CMSInitiatingOccupancyFraction=60 -XX:CMSTriggerRatio=70 -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:/home/xiaoju/logs/stargate-service/gc/gc_${DT}.log -XX:+PrintHeapAtGC"
3.既然排除JVM和業務代碼的原因,把目標又鎖定在了日志框架上,我們的業務代碼中引用了log4j(版本1.12.7)框架進行日志操作,在log4j的官網上找到了對性能影響的因素,見官網Performance菜單下的如下章節所示
Asynchronous Logging with Caller Location Information
也就是說如果在日志信息中打印location信息(例如:包名、類名、函數名稱和行號)(即:在log4j的配置文件中,配置了%C or $class, %F or %file, %l or %location, %L or %line, %M or %method),會嚴重影響log4j的性能。
跟蹤log4j源碼后發現,log4j為了拿到函數名稱和行號信息,利用了異常機制,首先拋出一個異常,之后捕獲異常並打印出異常信息的堆棧內容,再從堆棧內容中解析出行號,代碼截取如下:
/** Set the location information for this logging event. The collected information is cached for future use. */ public LocationInfo getLocationInformation() { if(locationInfo == null) { locationInfo = new LocationInfo(new Throwable(), fqnOfCategoryClass); } return locationInfo; }
我們知道,Java的異常機制很耗性能(注意:如果單純的是new異常並拋出並未耗性能,如果對異常棧進行操作,如打印輸出則很耗性能),因此我們將log4j的配置文件去掉了函數名稱和行號打印后,性能QPS立馬提升到了2500。
4.在log4j的配置文件中去掉log4j的location信息后,繼續壓力測試,待QPS達到2500后服務器又會報服務超時的錯誤,通過看log4j官網文檔中的性能章節,發現console對性能的影響也會非常大,因此在log4j的配置文件中又將console關閉了,繼續壓測后QPS性能達到8000左右了。
5.在log4j的配置文件中關閉location信息和關閉console后,繼續壓力測試進行優化,發現日志文件的大小也會影響到性能,建議控制日志文件的大小或者采用日志追加的方式;同時將日志改為異步打印也會有性能提升有所幫助。(在我們測試的過程中,日志文件大小和異步打印,對性能的提升有限,不如去掉location信息和關閉console那么明顯)。
總結:
1.Java語言不像C語言或PHP語言,對於行號獲取有宏定義__LINE__,可以很方便的獲取到行號信息,而Java語言中並未有這樣的宏定義,因此為了獲取到行號信息,Java只能從線程池棧或異常棧中獲取,為了獲得異常棧信息,又只能構造異常並拋異常,依次來拿到棧信息。即:在Java語言中,凡是涉及到行號信息的獲取,只能通過構造異常new Throwable()拋出,之后在函數內部通過異常或上層捕獲異常來拿到棧信息,從棧信息中解析出行號信息,因此在Java中凡是涉及到行號信息的獲取操作,都非常的耗性能,這一點尤其要注意。
2.log4j影響性能的程度依次為:日志的location信息(如:行號函數名) > console(關閉日志輸出到控制台) > 異步打印 > 日志文件的大小(日志追加模式)。線上環境如果對性能有一定要求的話,建議關閉location和console控制台。
附錄:
JVM的優化,JVM調優一般來說都是出問題或告警的時候注意進行優化,這塊可謂”水無常形 兵無常勢”,具體問題具體分析。
回收器的選擇:
- CMS
- G1
關鍵參數
– 決定Heap大小:-xms(最小) -xmx(最大) –xmn(年輕代大小)(建議-xms=-xmx) ,初始堆的大小==可調堆最大值,避免堆動盪
● 取決與操作系統位數和CPU能力
● 過小則GC頻繁,過大則GC中斷時間過長,注意GC發生時業務代碼會出現暫停一會,即:Stop the world。
– Eden/From/To:決定YounGC,如:-XX:SurvivorRatio
– 新生代存活周期:決定FullGC,如:–XX:MaxTenuringThreshold
新生代/舊生代
– 避免新生代設置過小
● 頻繁YoungGC
● 大對象,From/To不足拿Old增長快,FullGC
– 避免新生代設置過大
● 舊生代變小,頻繁FullGC
● 新生代變大,YoungGC更耗時
– 對於我們組大部分系統,可以分配 : 新生代:Heap=33%,即Young:Old=1:2
-XX:NewRatio=4:設置年輕代(包括Eden和兩個Survivor區)與年老代的比值(除去持久代),設置為4,則年輕代與年老代所占比值為1:4,年輕代占整個堆棧的1/5。
-XX:SurvivorRatio=4:設置年輕代中Eden區與Survivor區的大小比值,設置為4,則兩個Survivor區與一個Eden區的比值為2:4,一個Survivor區占整個年輕代的1/6,Eden區占整個年輕代的4/6。
供參考:
1.GC專家系列3-GC調優 https://segmentfault.com/a/1190000004303843
2.JVM GC中Stop the world案例實戰 https://blog.csdn.net/sinat_25306771/article/details/52374498
3.從實際案例聊聊Java應用的GC優化 https://tech.meituan.com/2017/12/29/jvm-optimize.html
4.垃圾回收算法 https://blog.csdn.net/d6619309/article/details/53358250
5.關於Jvm知識看這一篇就夠了https://zhuanlan.zhihu.com/p/34426768?group_id=956114978579750912