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