天天看點

線上服務運作時間長變慢問題的分析定位

問題

最近線上有一個服務,運作個一兩天,就會變得很慢。這裡很慢指的是經常對于請求的響應時間很大,有時甚至有二三十秒,導緻别人調接口,經常出現逾時問題。這裡簡單介紹些這個問題的定位方法過程以及最後怎麼解決的。

很多時候,我們更應該積累分析問題、解決問題的方法,這也是一個人工作能力的表現吧。當然專業知識的積累也是必不可少的,這是基礎。就像本文的分析,要是沒有點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真的很少。

以上就是解決這個線上服務運作時間長變慢問題的一個過程。

總結

我們在定位問題的時候,要根據自己已有的知識和經驗進行猜想和判斷,然後對猜想和判斷就行驗證,一步一步的去接近真相。

問題