最近发现司内的一个微服务运行的时候,会出现如下报错信息:
019-10-10 07:15:00.937[ERROR]com.alibaba.druid.util.JdbcUtils: 75-close connection error
java.sql.SQLException: Io exception: Connection reset
at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:112)
at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:146)
at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:255)
at oracle.jdbc.driver.T4CConnection.logoff(T4CConnection.java:480)
at oracle.jdbc.driver.PhysicalConnection.close(PhysicalConnection.java:1175)
at com.alibaba.druid.filter.FilterChainImpl.connection_close(FilterChainImpl.java:175)
at com.alibaba.druid.filter.FilterAdapter.connection_close(FilterAdapter.java:776)
at com.alibaba.druid.filter.logging.LogFilter.connection_close(LogFilter.java:440)
at com.alibaba.druid.filter.FilterChainImpl.connection_close(FilterChainImpl.java:171)
at com.alibaba.druid.filter.FilterAdapter.connection_close(FilterAdapter.java:776)
at com.alibaba.druid.filter.FilterChainImpl.connection_close(FilterChainImpl.java:171)
at com.alibaba.druid.filter.stat.StatFilter.connection_close(StatFilter.java:261)
at com.alibaba.druid.filter.FilterChainImpl.connection_close(FilterChainImpl.java:171)
at com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl.close(ConnectionProxyImpl.java:115)
at com.alibaba.druid.util.JdbcUtils.close(JdbcUtils.java:73)
at com.alibaba.druid.pool.DruidDataSource.shrink(DruidDataSource.java:2795)
at com.alibaba.druid.pool.DruidDataSource$DestroyTask.run(DruidDataSource.java:2560)
at com.alibaba.druid.pool.DruidDataSource$DestroyConnectionThread.run(DruidDataSource.java:2547)
从错误信息来看,明显是 druid 在做某些操作的时候出现了异常,这里微服务引用的是 1.1.9 版本的druid。
从日志信息表面来看,应该 druid 在关闭连接池中的连接的时候出现了异常,该类问题的原因十有八九是因为关闭了一个已无效的连接而导致的。
问题:连接池中的连接是怎么变成无效的呢?
大部分数据库对那些处于空闲状态的连接都会有一个机制,就是如果连接处于空闲状态超过一个时间限制(MYSQL 数据库默认8小时),则会该连接将会被数据库主动 close 掉。这下就明了了,数据库将「连接A」(处于空闲状态超过8小时) close 掉,但是处于连接池中的「连接A」并不知道这件事儿,所以,当「连接A」从连接池中被取出来使用的时候,发现「连接A」经无法连上数据库了。
貌似问题出现的原因找到了,但是究竟是谁触发了 druid 而导致 druid 报错的呢?再仔细查看一下错误日志的堆栈信息,会发现原来源头是 druid 的 DestroyConnectionThread 线程,该类是 DruidDataSource 的一个内部类。
问题:DestroyConnectionThread 线程在 druid 都干了啥?
字面上理解就是“销毁连接线程”,该线程主要任务是 close 掉连接池中那些符合 close 条件的连接,源代码如下:
public class DestroyConnectionThread extends Thread {
public DestroyConnectionThread(String name){
super(name);
this.setDaemon(true);
}
public void run() {
initedLatch.countDown();
for (;;) {
// 死循环
try {
if (closed) {
break;
}
// 先睡 timeBetweenEvictionRunsMillis 毫秒,再执行下边的销毁任务
if (timeBetweenEvictionRunsMillis > 0) {
Thread.sleep(timeBetweenEvictionRunsMillis);
} else {
Thread.sleep(1000); //
}
if (Thread.interrupted()) {
break;
}
// 运行销毁连接任务
destroyTask.run();
} catch (InterruptedException e) {
break;
}
}
}
}
通过代码我们可以看到,该线程的主逻辑是一个死循环,每隔 timeBetweenEvictionRunsMillis 毫秒就会运行一次 DestroyTask(销毁任务) ,DestroyTask 类也是 DruidDataSource 的一个内部类,源代码如下:
public class DestroyTask implements Runnable {
public DestroyTask() {
}
@Override
public void run() {
shrink(true, keepAlive);
if (isRemoveAbandoned()) {
removeAbandoned();
}
}
}
可以从上文的错误堆栈信息中我们可以看到,错误是发生在 shrink(true, keepAlive) 方法内的,所以,这里主要讲下 shrink(boolean checkTime, boolean keepAlive) 方法,研究了下源代码,总结该方法主要做了以下几件事情:
下面通过几张的图片,再说明一下:
根据自己的理解,我将 druid 连接池中的连接分为两类:
对于这两类连接,如果同时满足以下三个条件:
那么该连接将会被放入 keepAliveConnections 队列中,如上图中的conn3、conn4、conn5、conn6 就是这样的连接。
我们再回头看本文开篇的异常信息,当时 druid 到底发生了什么?!!
由于未显示的设置 keepAlive 值,则 druid 采用的是缺省值 false,这样就导致了 minIdle 类的连接一直不会被放入 keepAliveConnections 队列中,也就更不可能定期执行 validationQuery 来保持连接的有效性了。 当这些 minIdle 类的连接空闲时间 >=maxEvictableIdleTimeMillis(默认 7 小时) 的时候,就都被放到了evictConnections 队列中。随后 druid 在 close evictConnections 队列中连接的时候,此时的这些连接已是无效连接(原因是:由于这些连接处于空闲状态的时间(>=7小时)超过了 Oracle 数据库对非会话连接处于空闲时间的最大限制,也就是说 Oracle 数据库已经关闭了这些连接),所以,在执行 close 的时候,就出现了本文开篇中的异常!!!
那为啥其他微服务没有这样的情况出现呢?!
这是由于其他的微服务大都连接的是 MYSQL 数据库,而 MYSQL 数据库默认的非会话连接处于空闲状态的时间最大限制是 8 个小时(也就是说只要连接处于空闲状态时间不超过8小时,MYSQL 数据库就不会主动关闭连接),所以其他微服务都没有发生这样的事情!!!
https://www.cnblogs.com/hama1993/p/11421576.html