天天看點

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

作者:京東雲開發者

導讀

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

一、問題描述

大促期間,某接口逾時次數增多,經排查直接原因是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問題基本分析方法有所收益,後續該系列文章會繼續推出其他案例分享。

作者:京東零售 王利輝

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

繼續閱讀