問題
最近線上有一個服務,運作個一兩天,就會變得很慢。這裡很慢指的是經常對于請求的響應時間很大,有時甚至有二三十秒,導緻别人調接口,經常出現逾時問題。這裡簡單介紹些這個問題的定位方法過程以及最後怎麼解決的。
很多時候,我們更應該積累分析問題、解決問題的方法,這也是一個人工作能力的表現吧。當然專業知識的積累也是必不可少的,這是基礎。就像本文的分析,要是沒有點JVM的知識基礎,這個問題肯定是解決不了的。
分析與解決
1. 看日志
這是最基本的,一般線上服務出現問題,第一反應應該就是看日志。看看是不是程式抛出了異常。當然這裡是服務響應比較慢,并不是不傳回,也不是http傳回500。果不其然,線上服務是沒有異常程式日志的。因為程式是部署在tomcat上的,再看看tomcat的日志,發現也是正常的,沒有異常日志。
2. 看服務監控
現在好多的公司都有服務監控系統,有cpu、記憶體、網卡、磁盤、url請求的響應時間請求并發、jvm相關等等的監控。當然公司沒有監控系統也不用慌,監控系統隻是看起來直覺一點。就算沒有監控系統,還是可以通過linux指令以及jdk裡面帶的JVM調試工具來看。可能url請求響應時間和請求并發量沒有現成的指令工具,其他的都有,最基本的top指令看cpu、記憶體,JVM相關的就是用jdk自帶的工具,像jps,jmap,jstat,jstack等。
看了監控,發現cpu、網卡啥的都沒問題,線上機器記憶體16G,記憶體也完全夠。看Url響應時間發現有問題,最大調用時間很大,平均響應時間也偏大:
點開平均響應時間:
發現層次不齊,有時候快有時候慢。說明服務并不是所有的請求都很慢。
再看看監控的程式的一些方法,發現沒有io的方法,純記憶體計算的有時候也很慢,最慢調用都達到了27秒:
這裡就可以猜測是服務本身有問題了。這裡首先想到的就是服務在某個時間點卡住了,能想到的一個就是JVM在fullGC的時候stop the world。是以去看下jvm監控:
垃圾回收:
這裡能看到的是fullGC太頻繁了,而且點在上面還可以看到fullGC的時間很長,一次fullGC平均都用了27S的樣子。這也跟上面監控的url和方法監控的最慢調用時間相吻合。因為JVM在進行fullGC的時候,使用者線程都會被暫停等到fullGC完成再回來繼續執行。
看下虛拟機記憶體:
發現記憶體一直趨于飽和,發現就算fullGC,虛拟機堆記憶體并沒有得到有效的回收壓縮。
到這裡基本可以定位到,線上服務變慢的問題是由于JVM頻繁的fullGC導緻的,而fullGC很頻繁的原因是每一次fullGC并沒有實作記憶體的大量回收。是以過不了多久,記憶體又會滿,滿了又會進行fullGC一直循環。
那麼接下來就要分析為什麼fullGC不能實作記憶體的有效回收呢。
首先服務運作的虛拟機參數:
-Xmn2048M
-XX:ParallelGCThreads=6
-XX:SurvivorRatio=2
-XX:CMSFullGCsBeforeCompaction=0
-XX:CMSInitiatingOccupancyFraction=75
-XX:InitiatingHeapOccupancyPercent=50
-XX:MaxGCPauseMillis=200
-XX:+UseG1GC
-XX:+PrintGCApplicationConcurrentTime
-XX:+PrintGCApplicationStoppedTime
-XX:+PrintGCDateStamps
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
從這上面好像也看不出有什麼問題。那我們用jdk工具看看線上服務運作的記憶體資訊:
jmap -heap <pid>
- 1
其中上面的pid可以使用jps檢視,是java程序的一個程序id。堆記憶體資訊如下:
可以看到很奇怪的一點就是老年代滿的,這個跟剛剛監控看到的也是符合的。如果沒有監控看堆記憶體的垃圾回收資訊,可以使用jdk的工具:
jstat -gcutil <pid> 10000
- 1
表示的是每10秒輸出一次記憶體回收資訊,對gc進行統計。
這裡解釋下:
S0:新生代的susvivor0區,空間使用率為0%
S1:新生代的susvivor1區,空間使用率為100%(因為還沒有執行第二次minor收集)
E:eden區,空間使用率28.02%
O:老年代,空間使用率65.76%
YGC:minor gc執行次數215次
YGCT:minor gc耗費的時間37.996毫秒
FGC:full gc執行次數0
FGCT:full gc耗費的時間0毫秒
GCT:gc耗費的總時間37.996毫秒
當然這個并不是那個服務線上的環境的輸出,這裡隻是做一下解釋這些參數表示什麼意思。
回歸正題,這裡為什麼老年代一直都是滿的,fullGC對于老年代的回收基本起不到作用,而且上面看到的Survivor空間一直都是0,沒有配置設定空間。能想到兩種可能:1. 代碼問題,代碼裡面有大對象,而且是配置設定的時候直接進入老年代,而且這些對象被一直引用着沒有釋放,是以fullGC的作用不是很明顯。2. 虛拟機參數有問題,是參數的設定導緻的。
先說第一種可能,結合上面的輸出,Eden空間的大小是1872M,大對象在這裡面肯定是能得到配置設定的,而且看虛拟機參數的設定,-XX:SurvivorRatio=2 表示Eden和Survivor的比例為2,但是上面的jmap看到的對記憶體資訊裡面Survivor空間為0,這顯然跟設定的參數是沖突的。然後再去看代碼,也不存在大對象一直引用沒有釋放的情況,是以基本排除第一種情況。
第二種情況,我上網去吧那些參數基本都查了一遍,好像也看不出啥問題。有個參數-XX:+UseG1GC ,這個指定的收集器為G1收集器。是以我詳細的去查了下這個收集器:下面是我查找的資料的連結:
1. Getting Started with the G1 Garbage Collector
2. G1垃圾收集器入門
3. 神奇的 G1:Java 全新垃圾回收機制
4. Java Hotspot G1 GC的一些關鍵技術
其中第一篇是java 的官方文檔,裡面有個最佳實踐裡就提到:
裡面提到不要設定年輕代大小:
假若通過 -Xmn 顯式地指定了年輕代的大小, 則會幹擾到 G1收集器的預設行為.
G1在垃圾收集時将不再關心暫停時間名額.是以從本質上說,設定年輕代的大小将禁用暫停時間目标. G1在必要時也不能夠增加或者縮小年輕代的空間.
因為大小是固定的,是以對更改大小無能為力.
而我們的程式虛拟機參數剛好就設定了新生代大小:-Xmn2048M。會不會就是這個原因導緻的。我先把這個參數和-XX:SurvivorRatio=2 這個參數去掉,然後再部署服務,發現現在垃圾回收變得很正常,已經一天了,服務并沒有變慢。問題得到解決。中午吃飯的時候還跟同僚讨論是不是G1收集器的問題,讀了其他幾篇關于介紹G1收集器的部落格後,完全否定了這種想法。
後面jstat -gcutil 10000 觀察垃圾收集很正常,而且G1收集器fullGC真的很少。
以上就是解決這個線上服務運作時間長變慢問題的一個過程。
總結
我們在定位問題的時候,要根據自己已有的知識和經驗進行猜想和判斷,然後對猜想和判斷就行驗證,一步一步的去接近真相。