本文是《線上問題處理案例》系列之一,該系列旨在通過真實案例向讀者介紹發現問題、定位問題、解決問題的方法。本文講述了從垃圾回收耗時過長的表象,逐步定位到數據庫連接池保活問題的全過程,並對其中用到的一些知識點進行了總結。
大促期間,某接口超時次數增多,經排查直接原因是GC耗時過長,查看監控FullGC達500ms以上,接口超時時間與FullGC發生時間吻合。
容器:8C12G;
JVM配置:-XX:+UseConcMarkSweepGC -Xms6144m -Xmx6144m -Xmn2048m -XX:ParallelGCThreads=8 -XX:+UseCMSInitiatingOccupancyOnly -XX:CMSInitiatingOccupancyFraction=70 -XX:+ParallelRefProcEnabled;
數據庫類型:MySQL;
數據庫連接池:DBCP;
3.推斷FullGC耗時過長是否因爲老年代有大量死亡對象,遂導出FullGC前後堆內存dump,通過比對“保留大小”,發現FullGC後大量數據庫相關對象被回收。
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.反向思考,爲何只有在大促期間才發生問題?
可以看到平時由於業務量小,GC不頻繁,過期連接沒有達到進入老年代閾值,在年輕代被回收。而大促時業務量大,GC頻繁,連接在進入老年代以後才過期,導致老年代FullGC時間過長。
9.至此,基本可以肯定問題原因是數據庫連接池不具備“保活”能力,導致連接不斷淘汰和新建,在業務高峯時段,連接進入老年代然後失效,造成FullGC耗時過長,最終導致接口超時次數增多。
方案1:改爲G1回收器;
方案2:minEvictableIdleTimeMillis設置爲0;
1.Druid連接池同樣存在不能“保活”問題,較新版本提供“KeepAlive”選項(未驗證);
2.Druid連接池配置的“validationQuery”語句通常並不會被執行,MySqlValidConnectionChecker在檢查連接有效性時,會判斷驅動是否實現pingInternal方法,如果實現則會通過此方法驗證有效性。MySQL的JDBC驅動實現了該方法,因此“validationQuery”配置的語句通常不會執行;
3.DBCP和Druid連接池默認都是FILO,如果業務不繁忙,會導致只有最前邊的連接被使用-歸還-使用,後邊連接基本都在無謂的驅逐、重建連接;
4.虛引用對GC的影響:這些引用只有經過兩次GC才能被回收掉,如果進入老年代,則必須經過兩次FullGC才能釋放內存。本例中由於不斷有新的虛引用對象在老年代失效,導致FullGC後,內存水位仍然偏高,會加劇GC壓力。新版本JVM已對此做了優化,一次GC可以回收掉;
5.類似的影響還有finalize方法;
6.CMS回收器默認MaxTenuringThreshold爲6,而ParallelGC和G1均默認15;
-end-
本文分享自微信公衆號 - 京東雲開發者(JDT_Developers)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。