線上問題處理案例:出乎意料的資料庫連線池

2023-05-23 06:01:22

導讀

本文是線上問題處理案例系列之一,旨在通過真實案例向讀者介紹發現問題、定位問題、解決問題的方法。本文講述了從垃圾回收耗時過長的表象,逐步定位到資料庫連線池保活問題的全過程,並對其中用到的一些知識點進行了總結。

一、問題描述

大促期間,某介面超時次數增多,經排查直接原因是GC耗時過長,檢視監控FullGC達500ms以上,介面超時時間與FullGC發生時間吻合。

圖1 FullGC耗時監控

二、應用基本情況

  • 容器:8C12G;
  • JVM設定:-XX:+UseConcMarkSweepGC -Xms6144m -Xmx6144m -Xmn2048m -XX:ParallelGCThreads=8 -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+ParallelRefProcEnabled;
  • 資料庫型別:MySQL;
  • 資料庫連線池:DBCP;

三、排查過程

1、 GC耗時過長,說明記憶體中垃圾物件很多。

2、 首先懷疑是否有記憶體漏失,觀察FullGC後堆記憶體回收情況,尚屬正常,暫時排除記憶體漏失原因。

圖2 發生FullGC後堆記憶體回收監控

3、 推斷FullGC耗時過長是否因為老年代有大量死亡物件,遂匯出FullGC前後堆記憶體dump,通過比對「保留大小」,發現FullGC後大量資料庫相關物件被回收。

圖3 堆記憶體物件分析

4、 資料庫連線正常應該不會頻繁建立和斷開,進入老年代後,正常不應該被回收,通過堆dump內容OQL分析每個資料庫連線數量,發現很多庫連線數都大於「maxActive」數量,可以肯定有很多失效連線。

5、 初步判斷直接原因是很多失效資料庫連線進入老年代,導致FullGC耗時過長。

6、 懷疑連線池驗證週期過長,導致資料庫因空閒過長關閉連線,將連線池引數「
timeBetweenEvictionRunsMillis」由1分鐘調整到10秒,問題依舊。

7、 閱讀DBCP原始碼,發現是通過
org.apache.commons.pool.impl.GenericObjectPool.Evictor定時任務,按照timeBetweenEvictionRunsMillis設定的週期定時驅逐失效連線,驅逐條件:若連線空閒時間大於「minEvictableIdleTimeMillis」,則會驅逐連線,等待垃圾回收。若開啟「testWhileIdle」則會執行「validationQuery」。進一步閱讀程式碼,發現執行「validationQuery」後,連線空閒時間並不會重新計算,導致連線在業務低谷時很容易被淘汰,而資料庫連線會關聯大量物件,建立、回收成本昂貴,並且影響GC。

8、 反向思考,為何只有在大促期間才發生問題?

圖4 平時和大促時回收頻率對比

可以看到平時由於業務量小,GC不頻繁,過期連線沒有達到進入老年代閾值,在年輕代被回收。而大促時業務量大,GC頻繁,連線在進入老年代以後才過期,導致老年代FullGC時間過長。

9、 至此,基本可以肯定問題原因是資料庫連線池不具備「保活」能力,導致連線不斷淘汰和新建,在業務高峰時段,連線進入老年代然後失效,造成FullGC耗時過長,最終導致介面超時次數增多。

四、解決方案

方案1:改為G1回收器,對老年代回收是分塊進行,可以防止長時間停頓。另外預設MaxTenuringThreshold值是15,可以防止失效連線過早進入老年代;

方案2
minEvictableIdleTimeMillis設定為0,使資料庫連線不會自動失效,進入老年代以後一直存活,避免在老年代失效回收;

五、問題總結

資料庫連線池並不具備通常理解的「保活」能力,資料庫連線在業務不活躍的應用中,會不斷淘汰和重連,而連線會通過虛參照方式(
com.mysql.jdbc.NonRegisteringDriver$ConnectionPhantomReference)攜帶大量物件,如果連線存活時間內YGC次數達到壽命閾值,則會進入老年代,老年代是使用「標記-清除」演演算法,回收成本更高,進而造成FullGC耗時過長。

六、拓展知識點

1、 Druid連線池同樣存在不能「保活」問題,較新版本提供「KeepAlive」選項(未驗證);

2、 Druid連線池設定的「validationQuery」語句通常並不會被執行,MySqlValidConnectionChecker在檢查連線有效性時,會判斷驅動是否實現pingInternal方法,如果實現則會通過此方法驗證有效性。MySQL的JDBC驅動實現了該方法,因此「validationQuery」設定的語句通常不會執行;

圖5 連線有效性校驗程式碼

3、 DBCP和Druid連線池預設都是FILO,如果業務不繁忙,會導致只有最前邊的連線被使用-歸還-使用,後邊連線基本都在無謂的驅逐、重建連線;

4、 虛參照對GC的影響:這些參照只有經過兩次GC才能被回收掉,如果進入老年代,則必須經過兩次FullGC才能釋放記憶體。本例中由於不斷有新的虛參照物件在老年代失效,導致FullGC後,記憶體水位仍然偏高,會加劇GC壓力。新版本JVM已對此做了優化,一次GC可以回收掉;

5、 類似的影響還有finalize方法;

6、 CMS回收器預設MaxTenuringThreshold為6,而ParallelGC和G1均預設15;

結語

本文對資料庫連線失效引起的GC問題進行了詳細分析,希望讀者通過本文對資料庫連線「保活」機制、GC問題基本分析方法有所收益,後續該系列文章會繼續推出其他案例分享。

作者:京東零售 王利輝

內容來源:京東雲開發者社群