Druid數據庫連接池引起的FullGC問題排查、分析、解決

問題現象

在某個工作日,突然收到線上的服務告警,有大量的請求延時產生,查看線上服務發現基本上都是獲取數據庫連接超時,而且影響時間只有3~4秒鐘,服務又恢復了正常。隔了幾分鐘之後,又出現了大量的告警,還是影響3~4秒後又恢復正常。 由於我們是底層服務,被重多的上層服務所依賴,這麼頻繁的異常波動已經嚴重影響到了業務使用。開始排查問題

排查過程

DB的影響?

  1. 當第一次告警產生時,第一反應是可能上層服務有大量的接口調用,並且涉及到一些複雜的SQL查詢導致數據庫連接數不夠用,但是在分析了接口調用情況後發現異常前後的請求並沒有明顯的變化,排除突發流量造成的影響
  2. 查詢DB情況,負載良好,無慢查詢,排除DB造成的影響

容器或JVM的影響?

排除了DB的影響之後,再往上排查容器的影響 我們再次回過頭看異常告警,發現在每一波告警的時間段內,基本上都是同一個容器IP所產生,這個時候基本上已經有80%的概率是GC的問題了。 查詢告警時間段內的容器CPU負載正常。再看JVM的內存和GC情況,發現整個內存使用曲線是像下面這樣:

Heap

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

Old Gen

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

從上圖可以發現內存中存在長時間被引用,無法被YongGC所回收的對象,並且對象大小一直在增長。直到Old Gen被堆滿之後觸發Full GC後對象纔會回收。

臨時措施

現在問題已經找到了,到目前爲止只是3臺實例觸發了FullGC,但是在查看其它實例內存使用情況時,發現基本上所有的實例Old Gen都快到達臨界點了。所以臨時解決方案是保留一臺實例現場,滾動重啓其它所有的實例,避免大量的實例同時進行FullGC。否則很可能導致服務雪崩。

原本服務是有設置jvm監控告警的,理論上來說當內存使用率達到一定值時會有告警通知,但是由於一次服務遷移導致告警配置失效,沒有提前發現問題。

問題分析

什麼對象沒有被回收?

目前瞭解到的情況: 內存無法被YoungGC回收,且無限增加,只有FullGC才能夠回收這批對象

jmap -histo:live pid

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

先簡單在線上觀察了一波,排第2的HashMap$Node看起來比較異常,但是看不出更詳細的情況了。最好的辦法還是將內存快照dump出來,使用MAT分析一波

jmap -dump:format=b,file=filename pid 

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

使用MAT打開之後,可以發現很明顯的問題:

class com.mysql.cj.jdbc.AbandonedConnectionCleanupThread

這個類佔用了80%以上的內存,那麼這個類是幹嘛的呢? 看類名就知道,應該是MySQL Driver中用來清理過期連接的一個線程。讓我們看一下源碼:

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

 

這個類是一個單例,會且僅會開一個線程,用來清理那些沒有被顯式的關閉的數據庫連接。

可以看到這個類裏面維護了一個Set

private static final Set<ConnectionFinalizerPhantomReference> connectionFinalizerPhantomRefs = ConcurrentHashMap.newKeySet();

對應我們上面看到的內存佔用率排第二的HashMap$Node,基本上可以確定大概率是這裏存在內存泄露了。在MAT上使用list_object確認一發:

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

 

果然沒錯,罪魁禍首找到了! 那麼它裏面存的是啥東西呢? 爲什麼一直增長且無法被YoungGC回收?看名字
ConnectionFinalizerPhantomReference 我們可以猜到它裏面保存的應該是數據庫連接的phantom引用

什麼是phantom reference? 當一個對象只有phantom reference引用時,則會在虛擬機GC時被回收,同時會將phantom reference的對象放入一個referenceQueue中。

讓我們來跟蹤源碼確認一下

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

 

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

果然是PhantomReference,裏面存放的是創建的MySQL連接,看一下是在哪裏被放進來的:

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

 

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

可以看到,每次創建一個新的數據庫連接時,都會將創建的連接包裝成PhantomReference後放入
connectionFinalizerPhantomRefs中,然後這個清理線程會在一個無限循環中,獲取referenceQueue中的連接並關閉。

只有在 connection對象 沒有其它的引用,僅存在phantom reference時,才能夠被GC,並且放入referenceQueue中

爲什麼Connection會無限增長?

現在問題找到了,數據庫連接被創建之後,則會放入
connectionFinalizerPhantomRefs中,但是由於某種原因,連接前期正常使用,經過了多次minor GC都沒有被回收,晉升到了老年代。但是一段時間過後,由於某種原因連接失效,導致連接池又新建了連接。

我們項目用的數據庫連接池是Druid,以下爲連接池配置:

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

可以看到是設置了keepAlive,且minEvictableIdleTimeMillis設置的是5分鐘,連接初始化之後,在DB請求數沒有頻繁的波動時,連接池應該都是維護着最小的30個連接,且會在連接空閒時間超過5分鐘時進行一次keepAlive操作:

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

 

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

理論上來說,連接池是不會頻繁的創建連接的,除非有活躍連接很少,且存在波動,並且keepAlive操作沒有生效,在連接池進行keepAlive操作時,MySQL連接就已經失效,那麼則會丟棄這個無效連接,下次再重建。

下面就是驗證這個猜想,我們首先查看我們的活躍連接數,發現在大部分時候,單實例的數據庫的活躍連接數都在3~20個左右波動,並且業務上還存在定時任務,每隔30分鐘~1個小時會有大量的DB請求。 Druid既然有每隔5分鐘有心跳行爲,那爲什麼連接還會失效? 最大的可能是MySQL服務端的操作,MySQL默認服務端的wait_timeout是8小時,難道是有變更對應的配置?

show global variables like '%timeout%'

數據庫連接池引起的FullGC問題,看我如何一步步排查、分析、解決

果然,數據庫的超時時間被設置成了5分鐘!那麼問題就很明顯了。

結論

  1. 空閒連接依賴於Druid的keepAlive定時任務來進行心跳檢測和keepAlive,定時任務默認每60秒檢測一次,並且只有當連接的空閒時間大於minEvictableIdleTimeMillis時纔會進行心跳檢測。
  2. 由於minEvictableIdleTimeMillis被設置爲了5分鐘,理論上空閒連接會在5分鐘±60秒的時間區間內進行心跳檢測。但是由於MySQL服務端的超時時間只有5分鐘,所以大概率當Druid進行keepAlive操作時連接已經失效了。
  3. 由於數據庫的活躍連接是波動的,且min-idle設置的是30,活躍連接處於波峯時,需要創建大量的連接,並且維護在連接池中。但是當活躍降到低谷時,大量的連接由於keepAlive失敗,從連接池中被移除。週而復始。
  4. 每次創建連接時,又會將Connection對象放入入connectionFinalizerPhantomRefs中,並且由於創建完之後連接是處於活躍狀態,短時間內不會被miniorGC所回收,直至晉升到老年代。導致這個SET越來越大。

解決

知道問題的產生原因,要解決就很簡單了,將minEvictableIdleTimeMillis設置爲3分鐘,保證keepAlive的有效性,避免一直重建連接即可。

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