上周五晚上主营出现部分设备掉线,通过查看日志发现是因为缓存系统出现长时间gc致使的。这里的gc日志的特色是:java
初步判断长时间gc的问题应该是因为 com.mysql.jdbc.NonRegisteringDriver$ConnectionPhantomReference 这个对象大量堆积引发的。mysql
目前正式环境使用数据库相关依赖以下:linux
依赖 | 版本 |
---|---|
mysql | 5.1.47 |
hikari | 2.7.9 |
Sharding-jdbc | 3.1.0 |
根据以上描述,提出如下问题:git
简单来讲,NonRegisteringDriver类有个虚引用集合connectionPhantomRefs用于存储全部的数据库链接,NonRegisteringDriver.trackConnection方法负责把新建立的链接放入connectionPhantomRefs集合。源码以下:github
1.public class NonRegisteringDriver implements java.sql.Driver {
2. protected static final ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference> connectionPhantomRefs = new ConcurrentHashMap<ConnectionPhantomReference, ConnectionPhantomReference>();
3. protected static final ReferenceQueue<ConnectionImpl> refQueue = new ReferenceQueue<ConnectionImpl>();
4.
5. ....
6.
7. protected static void trackConnection(Connection newConn) {
8.
9. ConnectionPhantomReference phantomRef = new ConnectionPhantomReference((ConnectionImpl) newConn, refQueue);
10. connectionPhantomRefs.put(phantomRef, phantomRef);
11. }
12. ....
13. }
复制代码
咱们追踪建立数据库链接的过程源码,发现其中会调到com.mysql.jdbc.ConnectionImpl的构造函数,该方法会调用createNewIO方法建立一个新的数据库链接MysqlIO对象,而后调用咱们上面提到的NonRegisteringDriver.trackConnection方法,把该对象放入NonRegisteringDriver.connectionPhantomRefs集合。源码以下:算法
1.public class ConnectionImpl extends ConnectionPropertiesImpl implements MySQLConnection {
2.
3. public ConnectionImpl(String hostToConnectTo, int portToConnectTo, Properties info, String databaseToConnectTo, String url) throws SQLException {
4. ...
5. createNewIO(false);
6. ...
7. NonRegisteringDriver.trackConnection(this);
8. ...
9. }
10.}
复制代码
connectionPhantomRefs 是一个虚引用集合,何为虚引用?为何设计为虚引用队列sql
- 虚引用队列也称为“幽灵引用”,它是最弱的一种引用关系。
- 若是一个对象仅持有虚引用,那么它就和没有任何引用同样,在任什么时候候均可能被垃 圾回收器回收。
- 为一个对象设置虚 引用关联的惟一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
- 当垃圾回收器准备回收一个对象时,若是发现它还有虚引用,就会在垃圾回收后,将这个虚引用加入引用队列,在其关联的虚引用出队前,不会完全销毁该对象。因此能够经过检查引用队列中是否有相应的虚引用来判断对象是否已经被回收了。
这里结合项目中hikaricp数据配置和官方文档结合说明~shell
咱们先查阅hikaricp数据池的官网地址,看看部分属性介绍以下:数据库
maximumPoolSize缓存
This property controls the maximum size that the pool is allowed to reach, including both idle and in-use connections. Basically this value will determine the maximum number of actual connections to the database backend. A reasonable value for this is best determined by your execution environment. When the pool reaches this size, and no idle connections are available, calls to getConnection() will block for up to connectionTimeout milliseconds before timing out. Please read about pool sizing. Default: 10
maximumPoolSize控制最大链接数,默认为10
minimumIdle
This property controls the minimum number of idle connections that HikariCP tries to maintain in the pool. If the idle connections dip below this value and total connections in the pool are less than maximumPoolSize, HikariCP will make a best effort to add additional connections quickly and efficiently. However, for maximum performance and responsiveness to spike demands, we recommend not setting this value and instead allowing HikariCP to act as a fixed size connection pool. Default: same as maximumPoolSize
minimumIdle控制最小链接数,默认等同于maximumPoolSize,10。
⌚idleTimeout
This property controls the maximum amount of time that a connection is allowed to sit idle in the pool. This setting only applies when minimumIdle is defined to be less than maximumPoolSize. Idle connections will not be retired once the pool reaches minimumIdle connections. Whether a connection is retired as idle or not is subject to a maximum variation of +30 seconds, and average variation of +15 seconds. A connection will never be retired as idle before this timeout. A value of 0 means that idle connections are never removed from the pool. The minimum allowed value is 10000ms (10 seconds). Default: 600000 (10 minutes)
链接空闲时间超过idleTimeout(默认10分钟)后,链接会被抛弃
⌚maxLifetime
This property controls the maximum lifetime of a connection in the pool. An in-use connection will never be retired, only when it is closed will it then be removed. On a connection-by-connection basis, minor negative attenuation is applied to avoid mass-extinction in the pool. We strongly recommend setting this value, and it should be several seconds shorter than any database or infrastructure imposed connection time limit. A value of 0 indicates no maximum lifetime (infinite lifetime), subject of course to the idleTimeout setting. Default: 1800000 (30 minutes)
链接生存时间超过 maxLifetime(默认30分钟)后,链接会被抛弃.
咱们再回头看看项目的hikari配置:
猜想问题产生的根源:
每次新建一个数据库链接,都会把该链接放入connectionPhantomRefs集合中。数据链接在空闲时间超过idleTimeout或生存时间超过maxLifetime后会被废弃,在connectionPhantomRefs集合中等待回收。由于链接资源通常存活时间比较久,通过屡次Young GC,通常都能存活到老年代。若是这个数据库链接对象自己在老年代,connectionPhantomRefs中的元素就会一直堆积,直到下次 full gc。若是等到full gc 的时候connectionPhantomRefs集合的元素很是多,该次full gc就会很是耗时。
那么怎么解决呢?能够考虑优化minimumIdle、maximumPoolSize、idleTimeout、maxLifetime这些参数,下一小节咱们分析一波
为了验证问题,咱们须要模拟线上环境,调整maxLifetime等参数~压测思路以下:
这里有如下注意点:
最终环境配置以下:
确认hikari和jvm配置生效
期间观察gc日志,gc时间间隔约20s,100s后发生5次 gc
持续观察,1000s后理论上会产生220个对象(20 + 20 * 1000s / 100s),查看 jvisualvm 以下
再结合咱们生产的问题,假设咱们天天14个小时高峰期(12:00 ~ 凌晨2:00),期间链接数20,10个小时低峰期,期间链接数10,每次 full gc 间隔14天,等到下次 full gc 堆积的 NonRegisteringDriver 对象为 (20 * 14 + 10 * 10) * 2 * 14 = 10640,与问题dump里面NonRegisteringDriver对象的数量10140 个基本吻合。
至此问题根源已经获得彻底确认!!!
由上面分析可知,问题产生的废弃的数据库链接对象堆积,最终致使 full gc 时间过长。因此咱们能够从如下方面思考解决方案:
咱们能够考虑设置 maxLifetime 为一个较大的值,用于延长链接的生命周期,减小产生被废弃的数据库链接的频率,等到下次 full gc 的时候须要清理的数据库链接对象会大大减小。
Hikari 推荐 maxLifetime 设置为比数据库的 wait_timeout 时间少 30s 到 1min。若是你使用的是 mysql 数据库,可使用 show global variables like '%timeout%'; 查看 wait_timeout,默认为 8 小时。
下面开始验证,设置maxLifetime = 1小时,其余条件不变。压测启动前观察jvisualvm,NonRegisteringDriver 对象数量为20
同时另外注意:minimumIdle和maximumPoolSize不要设置得太大,通常来讲配置minimumIdle=10,maximumPoolSize=10~20便可。
G1回收器是目前java垃圾回收器的最新成果,是一款低延迟高吞吐的优秀回收器,用户能够自定义最大暂停时间目标,G1会尽量在达到高吞吐量同时知足垃圾收集暂停时间目标。
下面开始验证G1回收器的实用性,该验证过程须要一段较长时间的观察,同时借助链路追踪工具skywalking。最终观察了10天,结果图以下: 使用G1回收器,部分jvm参数-Xms3G -Xmx3G -XX:+UseG1GC
使用java 8默认的Parallel GC回收器组合,部分jvm参数-Xms3G -Xmx3G
咱们能够看到使用Parallel GC回收器组合的服务消耗的内存速度较快,发生了6996次young gc且发生了一次full gc,full gc时间长达5s。另一组使用G1回收器的服务消耗内存速度较为平稳,只发生3827次young gc且没有发生full gc。由此能够看到G1回收器确实能够用来解决咱们的数据库链接对象堆积问题。
这个咱们目前尚未通过实践,可是根据上面分析结果判断,按期触发full gc能够达到每次清理少许堆积的数据库链接的做用,避免过多数据库链接一直堆积。采用该方法须要对业务的内容和高低峰周期很是熟悉。实现思路参考以下:
咱们此次问题产生的根源是数据库链接对象堆积,致使full gc时间过长。解决思路能够从如下三点入手: