记一次数据库连接池导致的OOM的问题

5 篇文章 0 订阅
4 篇文章 0 订阅

问题现象

正式环境的某一天,服务的总占用内存超过了告警的阈值,出现了大量的告警,在内存的边缘不断的试探,但是这个不是顺时上来的压力,但是服务所占用的内存不断的增加,丝毫没有回收的痕迹.

排查过程

  1. 首先从监控系统查看内存的一个增加情况,确实如报警所示(也有可能报警是误报,都有可能)
    在这里插入图片描述
  2. 查看JVM的堆栈情况,从下图中我们可以看到老年代在不断的增长,一直没有得到释放

在这里插入图片描述3. 从服务的日志系统查找一点蛛丝马迹
根据当日的日志系统中的日志中,基本上也没有什么大量的错误日志,也没有大量的调用三方服务的异常,这就很诡异了,从目前手上的数据没有看出来任何的端倪.
4. 现在只能让运维先dump一份线上的堆栈日志才能查询出来一些我们想要的数据,趁着节点还没有重启,赶紧dump一份,要不然节点重启之后,啥都没有了,幸运的是临界点dump了一份

问题分析

堆栈分析的工具,使用的是MemoryAnalyzer,下载链接在文章下方,直接拉到最下面即可.
也可以使用jdk自带的jvisualvm,个人感觉MemoryAnalyzer更好一点.
下图就是导入线上服务的堆栈日志的首页预览图
在这里插入图片描述
打开之后,我们首先点击Leak Suspects,查看大致发现的问题范围.
在这里插入图片描述现在我们可以看到是第一个问题就是:

com.mysql.cj.jdbc.AbandonedConnectionCleanupThread

从字面上我们大致可以猜测到它是干什么的?
用于清理连接mysql的连接的,目前的开发上都是采用的数据库连接池,当连接的空闲时间超过我们设定的时间后,或者主动废弃,都是通过清理线程进行处理的.
接下来我们看下这个类的具体逻辑.
在这里插入图片描述它是一个单例的实现Runnable的bean,构造方法是private的,大家可以看到
通过Executors创建了一个只有单个线程的线程池对象,主要是为了清理哪些不是显示关闭的连接池中的连接
在MemoryAnalyzer的分析中我们可以明显的看到主要问题是java.util.concurrent.ConcurrentHashMap$Node[]" ,占用了大量的内存,没有被释放.

    private static final Set<AbandonedConnectionCleanupThread.ConnectionFinalizerPhantomReference> connectionFinalizerPhantomRefs = ConcurrentHashMap.newKeySet();

ConnectionFinalizerPhantomeReference是一个内部类,并且实现了虚引用,引用的是MysqlConnection,也就是客户端与数据库的连接.
在这里插入图片描述那么什么是虚引用呢?

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

在AbandonedConnectionCleanupThread定义中就看到了ReferenceQueue和Set的定义关系.

protected static void trackConnection(MysqlConnection conn, NetworkResources io) {
        threadRefLock.lock();
        try {
            if (isAlive()) {
                ConnectionFinalizerPhantomReference reference = new ConnectionFinalizerPhantomReference(conn, io, referenceQueue);
                connectionFinalizerPhantomRefs.add(reference);
            }
        } finally {
            threadRefLock.unlock();
        }
    }

当创建新的连接的时候就会调用trackConnection方法,把MysqlConnection添加到set集合和虚引用对应关联的queue中.
从这里我们基本上我们可以猜测到是由于大量的数据库连接进来,然后短时间内又被清理掉.

这个时候我们就需要先从数据库连接池的配置入手查看具体问题,当前项目采用的是springboot默认的hikari连接池
配置如下:

sparringapi.datasource.main.idle-timeout=60000
sparringapi.datasource.main.max-lifetime=
sparringapi.datasource.main.minimum-idle=
sparringapi.datasource.main.maximum-pool-size=

#最大连接数,小于等于0会被重置为默认值10;大于零小于1会被重置为minimum-idle的值
spring.datasource.hikari.maximum-pool-size
#最小空闲连接,默认值10,小于0或大于maximum-pool-size,都会重置为maximum-pool-size
spring.datasource.hikari.minimum-idle=10
#空闲连接超时时间,默认值600000(10分钟),大于等于max-lifetime且max-lifetime>0,会被重置为0;不等于0且小于10秒,会被重置为10秒。
#只有空闲连接数大于最大连接数且空闲时间超过该值,才会被释放
spring.datasource.hikari.idle-timeout=
#连接最大存活时间.不等于0且小于30秒,会被重置为默认值30分钟.设置应该比mysql设置的超时时间短
spring.datasource.hikari.max-lifetime=

idle-timeout当前设置的是1分钟,官方给的默认值是10分钟,现在首先要做的事情就是要调整它的大小,适当的增加idle-timeout的值,延长数据库连接池的空闲等待时间,进一步减少大量的连接被回收.

方案

把idle-timeout增加到5分钟,上线以后,观察了几天,内存比较稳定

资料

MemoryAnalyzer下载地址: http://wiki.eclipse.org/MemoryAnalyzer

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值