分析 “druid com.alibaba.druid.util.JdbcUtils Line:75 - close connection error” 出现的根源

背景

最近发现司内的一个微服务运行的时候,会出现如下报错信息:

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) 方法,研究了下源代码,总结该方法主要做了以下几件事情:

  1. 将连接池中符合 close 条件的连接,放到 evictConnections 队列中。
  2. 如果 keepAlive 如果为 true,则将连接池中符合 keepAlive 条件的连接,放到 keepAliveConnections 队列中;如果 keepAlive 如果为 false,则跳过该步骤。
  3. 从连接池中删除掉【1】【2】中涉及的连接,将连接池中剩余的连接重新排列,安排到队首,这一步也就是所谓的收缩连接池。
  4. 处理 evictConnections 队列中的连接,也就是 close 掉这些连接。队列中的连接都处理完成后,清空 evictConnections 队列。
  5. 如果 keepAliveConnections 队列中不为空,则处理 keepAliveConnections 队列中的连接,通过执行 validationQuery 检查队列中的连接是否可用,如果可用,则将当前连接 put 回连接池;如果不可用,则将当前连接 close 掉。队列中的连接都处理完成后,清空 keepAliveConnections 队列。

下面通过几张的图片,再说明一下:

  1. 连接池初始状态:连接总数=9、minIdle=5
    分析 “druid com.alibaba.druid.util.JdbcUtils Line:75 - close connection error” 出现的根源_第1张图片
  2. 根据具体的情况将连接池的连接分别放入 evictConnections、keepAliveConnections 队列(对应上边介绍的第1、2件事儿)
    分析 “druid com.alibaba.druid.util.JdbcUtils Line:75 - close connection error” 出现的根源_第2张图片

根据自己的理解,我将 druid 连接池中的连接分为两类:

  • 非 minIdle 类,连接池里 conn1、conn2、conn3、conn4 都属于 非minIdle 类
    该类的连接如果空闲时间 >=minEvictableIdleTimeMillis,则会被放入 evictConnections 队列中。
  • minIdle 类,连接池里 conn5、conn6、conn7、conn8、conn9 都属于 minIdle 类
    该类的连接只有当空闲时间 >=maxEvictableIdleTimeMillis,才会被放入 evictConnections 队列中。

对于这两类连接,如果同时满足以下三个条件:

  • 不存在于 evictConnections 队列中
  • keepAlive=true
  • 空闲时间>=keepAliveBetweenTimeMillis

那么该连接将会被放入 keepAliveConnections 队列中,如上图中的conn3、conn4、conn5、conn6 就是这样的连接。

  1. 收缩连接池(对应上边介绍的第3件事儿)
    分析 “druid com.alibaba.druid.util.JdbcUtils Line:75 - close connection error” 出现的根源_第3张图片

问题回顾

我们再回头看本文开篇的异常信息,当时 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

你可能感兴趣的:(分析 “druid com.alibaba.druid.util.JdbcUtils Line:75 - close connection error” 出现的根源)