登錄接口壓測響應慢頻繁GC問題排查
2020.5.22
最近項目組針對幾個較重要的接口進行了幾十個小時的壓測,發現登錄接口的壓測呈現了一種響應慢且越來越慢的趨勢,CPU 也居高不下
壓測情況
查看CPU佔用情況如圖所示:
找到對應服務包是鑑權服務(auth):
持續運行3小時的CPU佔用曲線圖: 結論:sc-auth包中的登錄接口,佔用CPU較高,需要優化。
排查思路
業務場景很簡單,賬號密碼鑑權登錄接口。
先排查爲什麼CPU佔用率高
從top命令的結果發現。pid爲15082的java進程CPU利用持續佔用過高,達到了驚人的222.3%
定位問題線程
使用ps -mp pid -o THREAD,tid,time
命令查看該進程的線程情況,發現該進程的15805到15099線程佔用率都比較高
查看問題線程堆棧
# 將線程id轉換爲16進制
[root@app-03 ~]# printf "%x\n" 15085
3aed
[root@app-03 root]$ printf "%x\n" 15089
3af1
jstack查看線程堆棧信息
jstack命令打印線程堆棧信息,命令格式(pid爲進程id,tid爲線程):jstack pid |grep tid
[xx@app-03 root]$ jstack 15082 | grep 3af1
"Gang worker#3 (Parallel GC Threads)" os_prio=0 tid=0x00007ff54c05f800 nid=0x3af1 runnable
[xx@app-03 root]$ jstack 15082 | grep 3aed
"Gang worker#0 (Parallel GC Threads)" os_prio=0 tid=0x00007ff54c05a000 nid=0x3aed runnable
從上面可以看出,這些都是GC的線程。那麼可以推斷,很有可能就是內存不夠導致GC不斷執行。接下來我們就需要查看
gc 內存的情況
查看Spring Admin監控中的JVM信息發現parNew和CMS 的GC都非常頻繁,GC時間也很長,達到了一個多小時,而且幾乎壓測賬號的每次請求都會導致一次CMS GC,起初沒有分析我們懷疑是圖中鮮紅的Non-Heap Memory佔用過高的問題
jstat -gc 查看垃圾回收統計
- **S0C:**第一個倖存區的大小
- **S1C:**第二個倖存區的大小
- **S0U:**第一個倖存區的使用大小
- **S1U:**第二個倖存區的使用大小
- **EC:**伊甸園區的大小
- **EU:**伊甸園區的使用大小
- **OC:**老年代大小
- **OU:**老年代使用大小
- **MC:**方法區大小
- **MU:**方法區使用大小
- **CCSC:**壓縮類空間大小
- **CCSU:**壓縮類空間使用大小
- **YGC:**年輕代垃圾回收次數*
- **YGCT:**年輕代垃圾回收消耗時間
- **FGC:**老年代垃圾回收次數*
- **FGCT:**老年代垃圾回收消耗時間
- **GCT:**垃圾回收消耗總時間
從其中的YGC、FGC等列可以看到 每秒鐘大約有4、5次FGC和6次YGC
要分析具體的GC情況,首先可以通過增加JVM配置參數-XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintHeapAtGC
來查看更詳細GC日誌。再用以下的heap dump定量分析實時進程內存情況。(Heap Dump也叫堆轉儲文件,是一個Java進程在某個時間點上的內存快照,常用作分析內存泄漏)
jstack
和 jmap
分析進程堆棧和內存狀況
我這邊選擇導出heap dump文件heapdump2020-05-21-20-21-live795350237996816401.hprof.gz
到本地用MAT工具進行內存分析(mat下載地址 https://www.eclipse.org/mat/downloads.php,使用可參考MAT使用-jvm內存溢出問題分析定位)
最終在這裏定位了問題具體的原因和進一步定位到具體代碼
使用MAT File -> open heap dump…打開並解析dump文件,
總覽結果,可以看到
Actions > The Histogram (查看堆棧中java類 對象[Objects]個數、[Shallow Heap]individual objects此類對象佔用大小、[Retained Heap]關聯對象佔用大小) -> Retained Heap倒敘排序(重點關注內存佔用高對象) -> Merge Shortest Paths to GC roots -> exclude all phantom/weak/soft etc.reference(排除所有虛弱軟引用) ->查看剩餘未被回收的強引用對象佔用原因 & GC Roots。
Merge到GC roots之後之後,排除所有虛弱軟引用
進入Overall 的 Leak Suspects
再進入Details查看詳情
Accumulated Objects in Dominator Tree*(以當前問題對象爲根的Dominator支配樹)
線程棧情況,太棒了,可以直接看到此線程在運行的代碼具體類方法和行號
問題代碼
下面直接到問題源代碼處
驗證猜想和修復驗證
使用新賬號不停調用登錄驗證問題,果然接口響應時間都在幾十毫秒左右,對比之前的壓測賬號,可以發現明顯響應快,但也呈一個持續微幅增大的趨勢,和我們之前猜想符合。
下圖爲跑了幾十個小時的壓測賬號的響應時間統計,都在1.5s以上
修改問題代碼之後的壓測結果、CPU佔用率和GC情況如下,可以看到,響應時間都在幾十毫秒100s單用戶成功請求4329次,比之前提升了10倍性能,非常快:
電腦本地模擬Set大對象FGC:
{Heap before GC invocations=25 (full 4):
PSYoungGen total 818176K, used 91111K [0x000000076b200000, 0x00000007a9500000, 0x00000007c0000000)
eden space 727040K, 0% used [0x000000076b200000,0x000000076b200000,0x0000000797800000)
from space 91136K, 99% used [0x0000000799580000,0x000000079ee79d10,0x000000079ee80000)
to space 145920K, 0% used [0x00000007a0680000,0x00000007a0680000,0x00000007a9500000)
ParOldGen total 265728K, used 252614K [0x00000006c1600000, 0x00000006d1980000, 0x000000076b200000)
object space 265728K, 95% used [0x00000006c1600000,0x00000006d0cb1bc0,0x00000006d1980000)
Metaspace used 76389K, capacity 77814K, committed 77848K, reserved 1116160K
class space used 10007K, capacity 10271K, committed 10280K, reserved 1048576K
2020-05-29T19:37:05.672+0800: [Full GC (Ergonomics) [PSYoungGen: 91111K->0K(818176K)] [ParOldGen: 252614K->257705K(587776K)] 343726K->257705K(1405952K), [Metaspace: 76389K->76389K(1116160K)], 0.1815747 secs] [Times: user=0.84 sys=0.00, real=0.18 secs]
Heap after GC invocations=25 (full 4):
PSYoungGen total 818176K, used 0K [0x000000076b200000, 0x00000007a9500000, 0x00000007c0000000)
eden space 727040K, 0% used [0x000000076b200000,0x000000076b200000,0x0000000797800000)
from space 91136K, 0% used [0x0000000799580000,0x0000000799580000,0x000000079ee80000)
to space 145920K, 0% used [0x00000007a0680000,0x00000007a0680000,0x00000007a9500000)
ParOldGen total 587776K, used 257705K [0x00000006c1600000, 0x00000006e5400000, 0x000000076b200000)
object space 587776K, 43% used [0x00000006c1600000,0x00000006d11aa5a8,0x00000006e5400000)
Metaspace used 76389K, capacity 77814K, committed 77848K, reserved 1116160K
class space used 10007K, capacity 10271K, committed 10280K, reserved 1048576K
}
dev set大小 9w = byte[]佔用大小 43MB
推算 壓測環境 Set大小 36w = byte[]佔用大小 170M
覆盤總結
排查思路:
-
觀察到壓測報告中接口慢同時伴隨着CPU高佔用,且都出現在同一接口中,伴隨情況大概率指向同一問題,先排查易排查的CPU高佔用問題
-
確定到具體java服務後通過jstat、jps、jinfo等JDK命令觀察運行情況
-
Spring Admin觀察JVM運行和內存佔用情況,有明顯的頻繁FGC問題,考慮大對象佔用
-
用GC日誌和heap dump分析當時的具體線程和對象的內存佔用情況,找到大對象對應的線程和執行程序代碼
-
快速瀏覽接口邏輯代碼 找到可能產生大對象的問題代碼(或者dump文件分析時候可以直接看到項目代碼位置)
-
本地debug調試,啓動時加上JVM配置參數分析復現和定位問題
-
修改代碼本地運行觀察GC頻率,提交更改,觀察接口響應時間、CPU佔用率、GC頻繁情況、內存佔用等綜合情況,驗證是否解決問題
中間走了一些彎路,比如看到Non-heap Memory佔用很高以爲是元空間配置-XX:MetaspaceSize不夠,jstat -gc的情況也顯示元空間佔用率很高,但是後來發現服務器上的配置 -XX:MetaspaceSize = 256m其實夠用,也問詢了一下寫這段代碼的同事,他也說是去年一次業務上的改造要兼容多客戶端登錄不被踢的奇怪需求,後來沒有這個需求後代碼也沒有還原導致一直運行。
過程中還用到了壓測工具Gatling的使用,輕量級的壓測工具也比較容易上手。
附上壓測demo
自己簡單寫了一個壓測腳本登錄單接口單用戶壓測一定時間
package computerdatabase.advanced
import io.gatling.core.Predef._
import io.gatling.http.Predef._
import scala.concurrent.duration._
class AuthLoginPressureTest extends Simulation {
val httpConf = http
.baseUrl("https://xxx.com/api")
//注意這裏,設置提交內容type
val contentType = Map(
"Content-Type" -> "application/x-www-form-urlencoded",
"Authorization" -> "Basic aW9jOmlvYw=="
)
//持續時長
val duration = 100
//聲明scenario
val scn = scenario("form Scenario")
.during(duration){
exec(http("authLogin_test") //http 請求name
.post("/auth/oauth/token") //post地址, 真正發起的地址會拼上上面的baseUrl http://computer-database.gatling.io/computers
.headers(contentType)
.formParam("username", "zzz") //form 表單的property name = name, value=Beautiful Computer
.formParam("password", "123456")
)
}
setUp(scn.inject(atOnceUsers(1)).protocols(httpConf))
}
參考資料
線上java程序CPU佔用過高問題排查:https://blog.csdn.net/u010862794/article/details/78020231
MAT使用-jvm內存溢出問題分析定位:https://www.jianshu.com/p/82b25cf8cfde?utm
CPU佔用過高問題排查](https://blog.csdn.net/u010862794/article/details/78020231):https://blog.csdn.net/u010862794/article/details/78020231
MAT使用-jvm內存溢出問題分析定位:https://www.jianshu.com/p/82b25cf8cfde?utm