数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
问题现象
在某个工作日,突然收到线上的服务告警,有大量的请求延时产生,查看线上服务发现基本上都是获取数据库连接超时,而且影响时间只有34秒钟,服务又恢复了正常。隔了几分钟之后,又出现了大量的告警,还是影响34秒后又恢复正常。 由于我们是底层服务,被重多的上层服务所依赖,这么频繁的异常波动已经严重影响到了业务使用。开始排查问题
排查过程
DB的影响?
- 当第一次告警产生时,第一反应是可能上层服务有大量的接口调用,并且涉及到一些复杂的SQL查询导致数据库连接数不够用,但是在分析了接口调用情况后发现异常前后的请求并没有明显的变化,排除突发流量造成的影响
- 查询DB情况,负载良好,无慢查询,排除DB造成的影响
容器或JVM的影响?
排除了DB的影响之后,再往上排查容器的影响 我们再次回过头看异常告警,发现在每一波告警的时间段内,基本上都是同一个容器IP所产生,这个时候基本上已经有80%的概率是GC的问题了。 查询告警时间段内的容器CPU负载正常。再看JVM的内存和GC情况,发现整个内存使用曲线是像下面这样:
Heap
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
Old Gen
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
从上图可以发现内存中存在长时间被引用,无法被YongGC所回收的对象,并且对象大小一直在增长。直到Old Gen被堆满之后触发Full GC后对象才会回收。
临时措施
现在问题已经找到了,到目前为止只是3台实例触发了FullGC,但是在查看其它实例内存使用情况时,发现基本上所有的实例Old Gen都快到达临界点了。所以临时解决方案是保留一台实例现场,滚动重启其它所有的实例,避免大量的实例同时进行FullGC。否则很可能导致服务雪崩。
原本服务是有设置jvm监控告警的,理论上来说当内存使用率达到一定值时会有告警通知,但是由于一次服务迁移导致告警配置失效,没有提前发现问题。
问题分析
什么对象没有被回收?
目前了解到的情况: 内存无法被YoungGC回收,且无限增加,只有FullGC才能够回收这批对象
jmap -histo:live pid
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
先简单在线上观察了一波,排第2的HashMap$Node看起来比较异常,但是看不出更详细的情况了。最好的办法还是将内存快照dump出来,使用MAT分析一波
jmap -dump:format=b,file=filename pid
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
使用MAT打开之后,可以发现很明显的问题:
class com.mysql.cj.jdbc.AbandonedConnectionCleanupThread
这个类占用了80%以上的内存,那么这个类是干嘛的呢? 看类名就知道,应该是MySQL Driver中用来清理过期连接的一个线程。让我们看一下源码:
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
这个类是一个单例,会且仅会开一个线程,用来清理那些没有被显式的关闭的数据库连接。
可以看到这个类里面维护了一个Set
private static final Set<ConnectionFinalizerPhantomReference> connectionFinalizerPhantomRefs = ConcurrentHashMap.newKeySet();
对应我们上面看到的内存占用率排第二的HashMap$Node,基本上可以确定大概率是这里存在内存泄露了。在MAT上使用list_object确认一发:
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
果然没错,罪魁祸首找到了! 那么它里面存的是啥东西呢? 为什么一直增长且无法被YoungGC回收?看名字 ConnectionFinalizerPhantomReference 我们可以猜到它里面保存的应该是数据库连接的phantom引用
什么是phantom reference? 当一个对象只有phantom reference引用时,则会在虚拟机GC时被回收,同时会将phantom reference的对象放入一个referenceQueue中。
让我们来跟踪源码确认一下
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
果然是PhantomReference,里面存放的是创建的MySQL连接,看一下是在哪里被放进来的:
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
可以看到,每次创建一个新的数据库连接时,都会将创建的连接包装成PhantomReference后放入 connectionFinalizerPhantomRefs中,然后这个清理线程会在一个无限循环中,获取referenceQueue中的连接并关闭。
只有在 connection对象 没有其它的引用,仅存在phantom reference时,才能够被GC,并且放入referenceQueue中
为什么Connection会无限增长?
现在问题找到了,数据库连接被创建之后,则会放入 connectionFinalizerPhantomRefs中,但是由于某种原因,连接前期正常使用,经过了多次minor GC都没有被回收,晋升到了老年代。但是一段时间过后,由于某种原因连接失效,导致连接池又新建了连接。
我们项目用的数据库连接池是Druid,以下为连接池配置:
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
可以看到是设置了keepAlive,且 minEvictableIdleTimeMillis设置的是5分钟,连接初始化之后,在DB请求数没有频繁的波动时,连接池应该都是维护着最小的30个连接,且会在连接空闲时间超过5分钟时进行一次keepAlive操作:
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
理论上来说,连接池是不会频繁的创建连接的,除非有活跃连接很少,且存在波动,并且keepAlive操作没有生效,在连接池进行keepAlive操作时,MySQL连接就已经失效,那么则会丢弃这个无效连接,下次再重建。
下面就是验证这个猜想,我们首先查看我们的活跃连接数,发现在大部分时候,单实例的数据库的活跃连接数都在320个左右波动,并且业务上还存在定时任务,每隔30分钟1个小时会有大量的DB请求。 Druid既然有每隔5分钟有心跳行为,那为什么连接还会失效? 最大的可能是MySQL服务端的操作,MySQL默认服务端的wait_timeout是8小时,难道是有变更对应的配置?
show global variables like '%timeout%'
数据库连接池引起的FullGC问题,看我如何一步步排查、分析、解决
果然,数据库的超时时间被设置成了5分钟!那么问题就很明显了。
结论
- 空闲连接依赖于Druid的keepAlive定时任务来进行心跳检测和keepAlive,定时任务默认每60秒检测一次,并且只有当连接的空闲时间大于minEvictableIdleTimeMillis时才会进行心跳检测。
- 由于minEvictableIdleTimeMillis被设置为了5分钟,理论上空闲连接会在5分钟±60秒的时间区间内进行心跳检测。但是由于MySQL服务端的超时时间只有5分钟,所以大概率当Druid进行keepAlive操作时连接已经失效了。
- 由于数据库的活跃连接是波动的,且min-idle设置的是30,活跃连接处于波峰时,需要创建大量的连接,并且维护在连接池中。但是当活跃降到低谷时,大量的连接由于keepAlive失败,从连接池中被移除。周而复始。
- 每次创建连接时,又会将Connection对象放入入connectionFinalizerPhantomRefs中,并且由于创建完之后连接是处于活跃状态,短时间内不会被miniorGC所回收,直至晋升到老年代。导致这个SET越来越大。
解决
知道问题的产生原因,要解决就很简单了,将 minEvictableIdleTimeMillis设置为3分钟,保证keepAlive的有效性,避免一直重建连接即可。
- TiDB 在 G7 的实践和未来
- 投资钛值的你,知道钛链是什么吗?
- Is this a MS EnterLib DAAB BUG or not?
- Silverlight 2 has a Timer (DispatcherTimer)
- 难道调用ThreadPool.QueueUserWorkItem()的时候,真是必须调用Thread.Sleep(N)吗?
- silverlight 《Hands-On-Labs》教程系列
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(67)-MVC与ECharts
- 2018年机器学习和数据科学重要会议概览
- ASP.NET MVC5+EF6+EasyUI 后台管理系统(60)-系统总结
- WCF技术剖析之一:通过一个ASP.NET程序模拟WCF基础架构
- 使用动态语言来制作silverlight
- 《资讯》霍金:人工智能的威胁就像核武器,世界将发生10大变化!
- [原创]WCF技术剖析之三:如何进行基于非HTTP的IIS服务寄宿
- 厉害了,连美图CEO都开始热捧区块链了!
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- Java的位运算符详解实例。
- 用一个测试类简化排序算法时间复杂度的研究
- R语言之可视化(33)绘制差异基因分析统计图
- Java transient不被持久化的原因
- 举一个有趣的例子,让你轻松搞懂JVM内存管理
- gulp的使用
- SpringBoot中 使用[info]日志级别打印mybatis sql语句
- Spring Boot 使用策略模式指定Service实现类
- 55. Vue webpack的基本使用
- ajax无刷新页面切换,历史记录后退前进解决方案
- 一起来学演化计算-SBX模拟二进制交叉算子和DE差分进化算子
- 通过与C++程序对比,彻底搞清楚JAVA的对象拷贝
- Spring - application.yml 数字读取错误、eg: 000001
- Swagger2 UI 提示"请确保swagger资源接口正确"解决办法
- Manytasking Jmetal 代码反向解析 2_MMDTLZ