系統運行緩慢,CPU 100%,以及FULL GC次數過多問題的排查思路

對於線上系統突然產生的運行緩慢問題,如果該問題導致線上系統不可用,那麼首先需要做的就是,導出jstack和內存信息,然後重啓系統,儘快保證系統的可用性。這種情況可能的原因主要有兩種:

代碼中某個位置讀取數據量較大,導致系統內存耗盡,從而導致Full GC次數過多,系統緩慢;
代碼中有比較耗CPU的操作,導致CPU過高,系統運行緩慢;
相對來說,這是出現頻率最高的兩種線上問題,而且它們會直接導致系統不可用。另外有幾種情況也會導致某個功能運行緩慢,但是不至於導致系統不可用:

代碼某個位置有阻塞性的操作,導致該功能調用整體比較耗時,但出現是比較隨機的;

  1. 某個線程由於某種原因而進入WAITING狀態,此時該功能整體不可用,但是無法復現;
  2. 由於鎖使用不當,導致多個線程進入死鎖狀態,從而導致系統整體比較緩慢。

對於這三種情況,通過查看CPU和系統內存情況是無法查看出具體問題的,因爲它們相對來說都是具有一定阻塞性操作,CPU和系統內存使用情況都不高,但是功能卻很慢。下面我們就通過查看系統日誌來一步一步甄別上述幾種問題。

1. Full GC次數過多

相對來說,這種情況是最容易出現的,尤其是新功能上線時。對於Full GC較多的情況,其主要有如下兩個特徵:
- 線上多個線程的CPU都超過了100%,通過jstack命令可以看到這些線程主要是垃圾回收線程
- 通過jstat命令監控GC情況,可以看到Full GC次數非常多,並且次數在不斷增加

首先我們可以使用top命令查看系統CPU的佔用情況,如下是系統CPU較高的一個示例:

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
    9 root      20   0 2557160 288976  15812 S  98.0 14.1   0:42.60 java

可以看到,有一個Java程序此時CPU佔用量達到了98.8%,此時我們可以複製該進程id9,並且使用如下命令查看呢該進程的各個線程運行情況

top -Hp 9

該進程下的各個線程運行情況如下

PID USER      PR  NI    VIRT    RES    SHR S %CPU %MEM     TIME+ COMMAND
   10 root      20   0 2557160 289824  15872 R 79.3 14.2   0:41.49 java
   11 root      20   0 2557160 289824  15872 S 13.2 14.2   0:06.78 java

可以看到,在進程爲9的Java程序中各個線程的CPU佔用情況,接下來我們可以通過jstack命令查看線程id爲10的線程爲什麼耗費CPU最高。需要注意的是,在jsatck命令展示的結果中,線程id都轉換成了十六進制形式。可以用如下命令查看轉換結果,也可以找一個科學計算器進行轉換:

printf "%x\n" 10

這裏打印結果說明該線程在jstack中的展現形式爲0xa,通過jstack命令我們可以看到如下信息:

"main" #1 prio=5 os_prio=0 tid=0x00007f8718009800 nid=0xb runnable [0x00007f871fe41000]
   java.lang.Thread.State: RUNNABLE
    at com.sinosun.chapter2.eg2.UserDemo.main(UserDemo.java:9)

“VM Thread” os_prio=0 tid=0x00007f871806e000 nid=0xa runnable

這裏的VM Thread一行的最後顯示nid=0xa,這裏nid的意思就是操作系統線程id的意思。而VM Thread指的就是垃圾回收的線程。這裏我們基本上可以確定,當前系統緩慢的原因主要是垃圾回收過於頻繁,導致GC停頓時間較長。我們通過如下命令可以查看GC的情況:

jstat -gcutil 9 1000 10
  S0     S1     E      O      M     CCS    YGC     YGCT    FGC    FGCT     GCT
  0.00   0.00   0.00  75.07  59.09  59.60   3259    0.919  6517    7.715    8.635
  0.00   0.00   0.00   0.08  59.09  59.60   3306    0.930  6611    7.822    8.752
  0.00   0.00   0.00   0.08  59.09  59.60   3351    0.943  6701    7.924    8.867
  0.00   0.00   0.00   0.08  59.09  59.60   3397    0.955  6793    8.029    8.984

可以看到,這裏FGC指的是Full GC數量,這裏高達6793,而且還在不斷增長。從而進一步證實了是由於內存溢出導致的系統緩慢。那麼這裏確認了內存溢出,但是如何查看你是哪些對象導致的內存溢出呢,這個可以dump出內存日誌,然後通過eclipse的mat工具進行查看,如下是其展示的一個對象樹結構:

經過mat工具分析之後,我們基本上就能確定內存中主要是哪個對象比較消耗內存,然後找到該對象的創建位置,進行處理即可。這裏的主要是PrintStream最多,但是我們也可以看到,其內存消耗量只有12.2%。也就是說,其還不足以導致大量的Full GC,此時我們需要考慮另外一種情況,就是代碼或者第三方依賴的包中有顯示的System.gc()調用。這種情況我們查看dump內存得到的文件即可判斷,因爲其會打印GC原因:

[Full GC (System.gc()) [Tenured: 262546K->262546K(349568K), 0.0014879 secs] 262546K->262546K(506816K), [Metaspace: 3109K->3109K(1056768K)], 0.0015151 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
[GC (Allocation Failure) [DefNew: 2795K->0K(157248K), 0.0001504 secs][Tenured: 262546K->402K(349568K), 0.0012949 secs] 265342K->402K(506816K), [Metaspace: 3109K->3109K(1056768K)], 0.0014699 secs] [Times: user=0.00

比如這裏第一次GC是由於System.gc()的顯示調用導致的,而第二次GC則是JVM主動發起的。總結來說,對於Full GC次數過多,主要有以下兩種原因:

  • 代碼中一次獲取了大量的對象,導致內存溢出,此時可以通過eclipse的mat工具查看內存中有哪些對象比較多;
  • 內存佔用不高,但是Full GC次數還是比較多,此時可能是顯示的System.gc()調用導致GC次數過多,這可以通過添加-XX:+DisableExplicitGC來禁用JVM對顯示GC的響應。

2. CPU過高

在前面第一點中,我們講到,CPU過高可能是系統頻繁的進行Full GC,導致系統緩慢。而我們平常也肯能遇到比較耗時的計算,導致CPU過高的情況,此時查看方式其實與上面的非常類似。首先我們通過top命令查看當前CPU消耗過高的進程是哪個,從而得到進程id;然後通過top -Hp 來查看該進程中有哪些線程CPU過高,一般超過80%就是比較高的,80%左右是合理情況。這樣我們就能得到CPU消耗比較高的線程id。接着通過該線程id的十六進制表示在jstack日誌中查看當前線程具體的堆棧信息。

在這裏我們就可以區分導致CPU過高的原因具體是Full GC次數過多還是代碼中有比較耗時的計算了。如果是Full GC次數過多,那麼通過jstack得到的線程信息會是類似於VM Thread之類的線程,而如果是代碼中有比較耗時的計算,那麼我們得到的就是一個線程的具體堆棧信息。如下是一個代碼中有比較耗時的計算,導致CPU過高的線程信息:

這裏可以看到,在請求UserController的時候,由於該Controller進行了一個比較耗時的調用,導致該線程的CPU一直處於100%。我們可以根據堆棧信息,直接定位到UserController的34行,查看代碼中具體是什麼原因導致計算量如此之高。

3. 不定期出現的接口耗時現象

對於這種情況,比較典型的例子就是,我們某個接口訪問經常需要2~3s才能返回。這是比較麻煩的一種情況,因爲一般來說,其消耗的CPU不多,而且佔用的內存也不高,也就是說,我們通過上述兩種方式進行排查是無法解決這種問題的。而且由於這樣的接口耗時比較大的問題是不定時出現的,這就導致了我們在通過jstack命令即使得到了線程訪問的堆棧信息,我們也沒法判斷具體哪個線程是正在執行比較耗時操作的線程。

對於不定時出現的接口耗時比較嚴重的問題,我們的定位思路基本如下:首先找到該接口,通過壓測工具不斷加大訪問力度,如果說該接口中有某個位置是比較耗時的,由於我們的訪問的頻率非常高,那麼大多數的線程最終都將阻塞於該阻塞點,這樣通過多個線程具有相同的堆棧日誌,我們基本上就可以定位到該接口中比較耗時的代碼的位置。如下是一個代碼中有比較耗時的阻塞操作通過壓測工具得到的線程堆棧日誌:

"http-nio-8080-exec-2" #29 daemon prio=5 os_prio=31 tid=0x00007fd08cb26000 nid=0x9603 waiting on condition [0x00007000031d5000]
   java.lang.Thread.State: TIMED_WAITING (sleeping)
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at com.sinosun.user.controller.UserController.detail(UserController.java:18)

“http-nio-8080-exec-3” #30 daemon prio=5 os_prio=31 tid=0x00007fd08cb27000 nid=0x6203 waiting on condition [0x00007000032d8000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.sinosun.user.controller.UserController.detail(UserController.java:18)

“http-nio-8080-exec-4” #31 daemon prio=5 os_prio=31 tid=0x00007fd08d0fa000 nid=0x6403 waiting on condition [0x00007000033db000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at com.sinosun.user.controller.UserController.detail(UserController.java:18)

從上面的日誌可以看你出,這裏有多個線程都阻塞在了UserController的第18行,說明這是一個阻塞點,也就是導致該接口比較緩慢的原因。

4. 死鎖

對於死鎖,這種情況基本上很容易發現,因爲jstack可以幫助我們檢查死鎖,並且在日誌中打印具體的死鎖線程信息。如下是一個產生死鎖的一個jstack日誌示例:

可以看到,在jstack日誌的底部,其直接幫我們分析了日誌中存在哪些死鎖,以及每個死鎖的線程堆棧信息。這裏我們有兩個用戶線程分別在等待對方釋放鎖,而被阻塞的位置都是在ConnectTask的第5行,此時我們就可以直接定位到該位置,並且進行代碼分析,從而找到產生死鎖的原因。

5. 小結、

本文主要講解了線上可能出現的五種導致系統緩慢的情況,詳細分析了每種情況產生時的現象,已經根據現象我們可以通過哪些方式定位得到是這種原因導致的系統緩慢。簡要的說,我們進行線上日誌分析時,主要可以分爲如下步驟:

  • 通過 top命令查看CPU情況,如果CPU比較高,則通過top -Hp 命令查看當前進程的各個線程運行情況,找出CPU過高的線程之後,將其線程id轉換爲十六進制的表現形式,然後在jstack日誌中查看該線程主要在進行的工作。這裏又分爲兩種情況

  • 如果是正常的用戶線程,則通過該線程的堆棧信息查看其具體是在哪處用戶代碼處運行比較消耗CPU;

  • 如果該線程是VM Thread,則通過jstat -gcutil 命令監控當前系統的GC狀況,然後通過jmap dump:format=b,file= 導出系統當前的內存數據。導出之後將內存情況放到eclipse的mat工具中進行分析即可得出內存中主要是什麼對象比較消耗內存,進而可以處理相關代碼;

  • 如果通過 top 命令看到CPU並不高,並且系統內存佔用率也比較低。此時就可以考慮是否是由於另外三種情況導致的問題。具體的可以根據具體情況分析:

  • 如果是接口調用比較耗時,並且是不定時出現,則可以通過壓測的方式加大阻塞點出現的頻率,從而通過jstack查看堆棧信息,找到阻塞點;

  • 如果是某個功能突然出現停滯的狀況,這種情況也無法復現,此時可以通過多次導出jstack日誌的方式對比哪些用戶線程是一直都處於等待狀態,這些線程就是可能存在問題的線程;

  • 如果通過jstack可以查看到死鎖狀態,則可以檢查產生死鎖的兩個線程的具體阻塞點,從而處理相應的問題。

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