一、背景介紹:
運行在k8s集群中負責支付業務一個服務,運營一段時間就會被k8s kill,然后重啟, 通過查看k8s 的event發現系統達到了memory到達了上限被集群kill調。
服務配置:jdk:1.8、堆內存:-Xmx800m -Xms800m 設置為800M, k8s的memory.limit設置為1G。
二、排查問題:
1.初步分析: 由於系統的請求量不大,所以設置的堆內存足夠了,所以可以排除堆內存設置過小原因。同時由於服務被kill的原因是因為物理內存占用過大。所以懷疑是堆外內存溢出。
jvm內存結構分為:堆內存(新生代、老年代), 堆外內存(線程棧、元空間、直接內存)
2.排查:
2.1 gc狀態分析:jstat -gcutil pid 5s
結果:各區域的占用情況,gc情況無明顯異常
2.2 堆dump: jmap -dump:format=b,file=heap.hprof pid, 使用mat分析如下:
很明顯 com.mysql.jdbc.NonRegisteringDriver占用堆內存的33%。其中java.util.concurrent.ConcurrentHashMap$Node[] 存在內存泄漏的可能。
ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference> connectionPhantomRefs 保存mysql connection的虛引用。
當 mysql connection被釋放后,虛引用的refQueue中會收到釋放的connection。 定時清理線程會取出refQueue中保存的connection,然后將connection從 connectionPhantomRefs中清除。
當查看詳細情況發現: connectionPhantomRefs 保存的連接個數600多個,而mysql的tomcat的連接池最大設置的max-active=100,
這說明了兩個問題:
- 1. 有大部分的連接池沒有被回收
- 2. 最大連接數max-active:100,而實際生成的卻遠遠大於max-active
2.3 連接池配置分析
根據2.2中堆dump的情況發現了兩個問題:
- (1) 有大部分的連接池沒有被回收
- (2)最大連接數max-active:100,而實際生成的卻遠遠大於max-active,
關於問題(1):通過jstat -gcutil pid 5s發現jvm old區占用50%,一直沒有FGC。 通過jcmd GC.run手動gc后,再次dump發現om.mysql.jdbc.NonRegisteringDriver保存的連接數目減少了
關於問題(2): 通過排查,發現tomcat連接池設置的最大生命周期 max-age:60000, 即連接創建一分鍾后就會被銷毀。
三、解決問題:
1. 增加連接池超時時間,超時時間稍微小於mysql的waitTimeout即可
2. jvm遲遲沒有fgc導致連接池中連接沒有釋放,可以稍微調小堆內存
3. 回歸到最開始的疑問,為什么會是堆外內存溢出呢? 通過堆外內存幾個區域的分析,發現其中大量的MysqlStatement Cancellation Timer:
mysql每個連接一個超時檢測線程用於檢測sql語句是否超時,mysql的連接沒有釋放導致線程也未釋放。 而線程棧占用默認大小為1m,所以導致了堆外內存溢出