Metaspace泄漏排查

本文轉自:云溪社區 https://yq.aliyun.com/articles/603830?utm_content=m_1000003891

一、案件背景

近日,一個線上應用開始頻繁報警:異常日誌、接口rt超時、load高、tcp重傳率高等等。現場監控如下:image.png | left | 748x421

從基礎監控來看,cpu使用率不算特別異常,而load高說明等待cpu資源的線程隊列長,配合rt上漲來看,推測是線程出現了堆積,而線程堆積一般有兩種情況:

  • 線程內部處理耗時變長:比如緩存未命中、被下游請求block、慢sql、循環邏輯耗時等。
  • JVM因GC、鎖擦除等jvm操作原因觸發stop the world,導致線程等待。

下面進一步定位問題。

二、問題定位

線程耗時變長?

因爲應用重度依賴緩存,一起排查問題的同學發現tair成功率也有下降,於是找tair同學開始排查。image.png | left | 748x427

tair的同學表示集羣正常,觀察同機房其他應用讀寫tair也正常,推測問題還是出在應用自身。(其實應用自身負載高時,會引起tair超時和序列化失敗,表象來看都像tair有問題,心疼下tair的同學。。。)

另外從db監控來看也沒有慢sql出現,近期也沒有邏輯改動較大的發佈,暫時從其他方向看看。

STW

接下來比較醒目的就是GC監控了,監控如下:image.png | left | 748x213

可以看到問題期間GC次數和耗時明顯上升,這裏需要注意,因爲GC監控裏CMS(或G1)和Full GC都會歸到Full GC裏,所以登到機器上看看gc.log。image.png | left | 748x365

這裏看到Metadata GC Threshold引起Full GC的字樣較多,回收情況也很差(382626K->381836K)再通過jstat看下使用情況:image.png | left | 748x268

這裏看到M區(metaspace)使用率是73.22%,而FGC平均每秒一次,GC前後O區(old gen)變化不大,可以確定是metaspace泄漏導致的Full GC。

值得注意:M區沒到100%(73.22%),爲什麼會頻繁觸發fullGC呢?原因是classloader加過多時,會引起metaspace的碎片化問題。後面會進一步解釋classloader過多原因。

到此,原因可以確定是FullGC伴隨的大量STW引起了線程堆積。下面繼續定位爲什麼metaspace會出現泄漏。

groovy動態類加載問題

對付GC問題,堆dump是最好的排查手段。話不多說,psp進行dump。針對metaspace(Perm gen)的泄漏問題,從看"類加載器視圖"和"重複類定義"能很快定位泄漏對象,如下:image.png | left | 748x312
image.png | left | 748x276

可以發現,類加載器中上千個GroovyClassLoader&innerLoader,另外類定義裏的Comp_XXX也是應用裏使用的groovy類,原來是踩到groovy重複加載會導致泄漏的坑。

簡單來說,每次通過groovyClassLoader.parseClass()方法記載class時,都會生成一個GroovyClassLoader&innerLoader,並且因爲代碼設計問題無法被卸載。具體原因這篇文章有詳細說明:groovy腳本加載導致的FullGC問題

但是令人疑惑的是,應用對編譯出來的class都做了內存cache,命中就直接返回不再編譯,爲什麼這裏出現重複加載呢?

原來代碼裏通過guava cacheBuilder實現的內存cache,同時設置了緩存隊列大小和弱引用

CacheBuilder.newBuilder().maximumSize(1000).softValues().build();

有兩種可能導致緩存未命中,進而重新編譯

  • 達到maxSize
  • softReference:回收閾值受使用頻率和GC後空間大小影響,堆資源緊缺時,使用頻率低會被回收

一開始懷疑是softReference會被回收,因爲需要緩存的對象(官方模塊)只有幾十個,去掉softValues後跑一會,發現還是會復現問題,於是dump裏查看緩存對象數量:image.png | left | 752x141.34643734643734

發現​對象數量達到了1000,說明存在淘汰的對象,再次使用這些對象會重複編譯,進而導致泄漏。

後續查看緩存對象,是一些不需要編譯的三方模塊,和同學確定是一次變更,把大量不需要編譯的對象(三方模塊)也緩存進來。修復方案分析如下:

  • 官方模塊數量少,需要編譯,加載成本高
  • 三方模塊數量多,不需要編譯,加載成本低
  • 兩者進行分開緩存,針對官方模塊進行強引用緩存,針對三方模塊進行弱引用緩存,並設置失效時間

發佈上線後,運行良好。

三、後言

於此,問題原因已經水落石出,集羣問題期間出現的表現,比如load高、rt高、cpu上漲、tair成功率下降通過gc引起的stw都可以合理解釋。

但tcp重傳率上漲的原因是什麼呢?所有現象如果沒有合理解釋清楚,怕有問題遺漏,下面嘗試分析下原因:

TCP重傳率

TCP重傳一種是爲了保證數據可靠性的機制,其原理是在發送某一個數據以後就開啓一個計時器,在一定時間內如果沒有得到發送的數據報的ACK,那麼就重新發送數據,直到發送成功爲止。

TCP相關問題排查步驟大致如下

  • tcp抓包:sudo tcpdump -i eth0 -w ~/11131114249_fgc_tcpdump1.cap
  • 通過oss上傳11131114249_fgc_tcpdump1.cap,然後下載到本地
  • 用wireshark對cap文件進行分析

通過wireshark打開dump文件後,對重傳相關的報文進行過濾:tcp.analysis.retransmissionransis image.png | left | 748x176
抓包機器是11.131.114.249,發現出現tcp重傳的端口都是8006,追蹤一條tcp流看看image.png | left | 748x173
發現對端(11.137.22.197)嘗試通過對8006進行HTTP POST /metrics/specific 進行請求,但8006端口長時間未ACK,於是對端進行了大量retry。

這裏需要解釋8006端口是哪個進程的?是做什麼的?爲什麼沒有及時ACK?

通過netstat可以看到8006端口是java(1970)進程的,也就是tomcat直接暴露的。而8006是metrics的agent暴露給外部採集機器數據用的端口。image.png | left | 748x54
因爲agent掛在在JVM進程裏,網絡io線程自然也受JVM STW影響,導致ACK不及時,從而引起對端TCP重傳。

小小延展下,對比一下80端口的情況,沒有出現TCP重傳。image.png | left | 748x165
原因自然是因爲80端口由nginx暴露,然後反向代理給7001端口(JVM),所以JVM的STW不會對nginx ack有影響,而只會影響HTTP的返回時間。

相較BIO模式下的tomcat,NIO的nginx擅長做TCP建聯,這​大概也是在同機部署tomcat和nginx的一點益處。

小心“併發症”的擾亂

另外一點感想是,在多次排查線上問題時,比如出現各種“病症”:機器load飆高、GC變多、內存上漲、線程數開始堆積、RT升高、HSF大量超時、tair失敗率上升、流量曲線集中等等。

一旦應用因爲某個“病因”出現情況,就會出現很多“病症”,這些“病症”絕大部分都是“併發症”,是由“病因”引起的連鎖反應,孰是因孰是果呢?

比如tair集羣如果出現問題,導致讀取失敗,那麼依賴緩存的應用肯定會出現rt變長,線程數開始堆積,進而導致內存上漲,FGC,load上漲等問題。

而反過來看,如同本例,GC太過頻繁,也會導致tair成功率下降,和其他類似現象。

我們需要快速判斷,找到“病因”才能準確止血。

這裏的一點心得是,

  • 先救命再治病:首先想辦法止血,嘗試重啓、回滾,保留一兩臺現場
  • 系統一定要做好監控,這樣病發現場纔能有跡可循
  • 利用監控對各個“病症”出現時間進行比對,找到誰前誰後,前者往往大概率就是"病因",後者是"併發症"

本例中,FGC開始出現的時間,比tair成功率下降、HSF流量異常出現時間早2分鐘左右,由此判定前者是因,後者都是果。

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