数据库连接池引起的FullGC问题分析 中,我们经过分析和排查,解决了由于数据库设置的wait_timeout和Druid的minEvictableIdleTimeMillis配置不合理以至于心跳未生效,导致JDBC驱动中的AbandonConnectionCleanThread中的connectionFinalizerPhantomRefs对象无限增长引起的FullGC问题。 在上次修改了配置之后,岁月静好,终于可以不用被午夜铃声叫起来处理问题。但是生活不如意的事十有八九,还有一二特别如意。 在某一次的性能优化过程中,为了更加极致的性能,抛弃了Druid而拥抱了HikariCP,伴随而来的又是内存的持续上升。而且比上一次更加的凶猛!
最好的东西都不是独来的,它伴了所有的东西同来。 ——泰戈尔
确认问题
有了上次的经验,这次一出现问题第一反应就是又是AbandonConnectionCleanThread的锅,直接jstat和arthas验证一波:
1. 下载并启动arthas$ curl -O https://alibaba.github.io/arthas/arthas-boot.jar$ java -jar arthas-boot.jar2. 选择对应的Java进程3. 获取`AbandonConnectionCleanThread`中的`connectionFinalizerPhantomRefs`大小arthas$ getstatic com.mysql.cj.jdbc.AbandonedConnectionCleanupThread connectionFinalizerPhantomRefs 'size()'复制代码
arthas算是线上调试神器了,个人觉得比Btrace要方便和易用很多。至于更加详细的使用方法,大家这么聪明,Google一下就知道啦。
果然不出所料:
connectionFinalizerPhantomRefs对象已经增长到1万多了,明显不是一个合理的数字,并且在持续的增长
再看一下内存情况:
$ jstat -gc 1复制代码
老年代已经基本快爆了,吓得我赶紧先手动重启了一下实例
老年代容量大小 OC: old capacity, 老年代使用大小 OU old used,其它的随便猜一下就知道啦,实在不行还有Google大法,我就不再啰嗦了。
分析
同样的问题,但是奇怪的是为什么这次FullGC的时间间隔比上次还要更短呢? 几乎缩短了近一倍。 看了一下我们的连接池配置,由于HikariCP是不支持心跳检测机制的,为了保证连接的可用性,我们配置了HikariCP官方墙裂推荐的配置max-lifetime
max-lifetime=240000 #4分钟,比数据库的wait_timeout短一分钟复制代码
那让我们来看看在配置了max-lifetime之后,HikariCP里面做了啥
可以看到,在HikariCP创建一个连接对象的时候,如果有设置max-lifetime,则会开启一个定时任务,在max-lifetime +- max-lifetime * 0.025毫秒后将其关闭。 那么问题来了,在我们应用使用的活跃期内,实际上我们的数据库配置的连接和实际上使用的连接大部分是一致的,也就是很多连接没有必要杀掉。但是HikariCP为了保证连接池内的连接必定有效,则不论如何只要时间一到,就会全部杀掉。 我们应用使用读写分离,一共有两个连接池,每个连接池配置30个连接,也就是每过3分钟左右,HikariCP都会杀掉60个连接,再创建60个。这可真的是稳定上升呢~
确认一下:
$ netstat -anp |grep 3306|grep CLOSE_WAIT |wc -l$ netstat -anp |grep 3306|grep ESTABLISHED | wc -l复制代码
可以看到应用是会主动断开数据库连接的。
netstat 查看网络状态
TIME_WAIT,TCP四次挥手中, 主动断开方接收到被动断开方的ACK时的状态
wc -l 统计行数
更详细的就留给大家自己了解啦
解决
Druid可以通过设置心跳解决问题,但是HikariCP不支持,怎么办? 为了以后不再因为这个问题而起夜,决定一劳永逸,直接从源头掐断问题。 AbandonConnectionCleanThread主要是为了关闭那些应用忘记关闭的连接。但是目前我们基本都是使用连接池,不需要我们手动来创建和关闭连接,因此完全可以不需要这个线程。
直接hack AbandonConnectionCleanThread:
@Component@Slf4jpublic class ConnectionCleanupThreadHack implements ApplicationListener { @Override public void onApplicationEvent(ContextRefreshedEvent event) { log.info("hack AbandonedConnectionCleanupThread"); try { //加载类,初始化static Class.forName("com.mysql.cj.jdbc.AbandonedConnectionCleanupThread"); //停掉定时任务 Field cleanupThreadExcecutorService = AbandonedConnectionCleanupThread.class.getDeclaredField("cleanupThreadExcecutorService"); cleanupThreadExcecutorService.setAccessible(true); ExecutorService executorService = (ExecutorService) cleanupThreadExcecutorService.get(null); executorService.shutdownNow(); //把threadRef置为空,在traceConnection时则不会再往connectionFinalizerPhantomRefs放数据 Field threadRef = AbandonedConnectionCleanupThread.class.getDeclaredField("threadRef"); threadRef.setAccessible(true); threadRef.set(null, null); log.info("hack AbandonedConnectionCleanupThread success"); } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException e) { log.error("can not access AbandonedConnectionCleanupThread", e); } }}