Apache Druid连接回收引发的血案

问题

线上执行大批量定时任务,发现SQL执行失败的报错:

CommunicationsException, druid version 1.1.10, 
jdbcUrl : jdbc:mysql://xxx?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull,
 testWhileIdle true, idle millis 658108, minIdle 2, poolingCount 1,
  timeBetweenEvictionRunsMillis 60000, lastValidIdleMillis 658108, 
  driver com.mysql.jdbc.Driver, exceptionSorter com.alibaba.druid.pool.vendor.MySqlExceptionSorter

com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

The last packet successfully received from the server was 658,107 milliseconds ago.  The last packet sent successfully to the server was 0 milliseconds ago.
	at sun.reflect.GeneratedConstructorAccessor60.newInstance(Unknown Source)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.mysql.jdbc.Util.handleNewInstance(Util.java:404)
	at com.mysql.jdbc.SQLError.createCommunicationsException(SQLError.java:981)
	at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3465)
	at com.mysql.jdbc.MysqlIO.reuseAndReadPacket(MysqlIO.java:3365)
	at com.mysql.jdbc.MysqlIO.checkErrorPacket(MysqlIO.java:3805)
	at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2478)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2625)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2551)
	at com.mysql.jdbc.PreparedStatement.executeInternal(PreparedStatement.java:1861)
	at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:1962)
	at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_executeQuery(FilterChainImpl.java:3188)
	at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_executeQuery(FilterEventAdapter.java:465)
	at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_executeQuery(FilterChainImpl.java:3185)
	at com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl.executeQuery(PreparedStatementProxyImpl.java:181)
	at com.alibaba.druid.pool.DruidPooledPreparedStatement.executeQuery(DruidPooledPreparedStatement.java:228)
	at org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:57)
	at org.hibernate.loader.Loader.getResultSet(Loader.java:2304)

追查

从错误日志看来,是连接MySQL超时,但是这个超时时间感觉有点离谱,查看了一下执行的SQL,是非常简单的SQL,理论上不可能造成超时。

联系DBA,看看MySQL的日志能否发现什么蛛丝马迹。

原因

经过DBA排查,确认是MySQL的空闲连接清理参数配置,MySQL的interactive_timeout参数指定了连接空闲多久后会被回收掉,缺省配置为600秒。

问题初步定位,是因为数据库连接池Druid持有的数据库连接,已经被MySQL回收,此时有SQL执行时,已经被回收掉的连接再此被分配,就会导致这个报错。

那么问题来了,为什么数据库连接池没有感知到这个连接已经被回收了呢?

连接池是怎么判断一条连接是Idle状态的?

带着疑问,翻找Druid文档,发现两个配置:

  • minEvictableIdleTimeMillis:最小空闲时间,默认30分钟,如果连接池中非运行中的连接数大于minIdle,并且那部分连接的非运行时间大于minEvictableIdleTimeMillis,则连接池会将那部分连接设置成Idle状态并关闭;也就是说如果一条连接30分钟都没有使用到,并且这种连接的数量超过了minIdle,则这些连接就会被关闭了。

  • maxEvictableIdleTimeMillis:最大空闲时间,默认7小时,如果minIdle设置得比较大,连接池中的空闲连接数一直没有超过minIdle,这时那些空闲连接是不是一直不用关闭?当然不是,如果连接太久没用,数据库也会把它关闭,这时如果连接池不把这条连接关闭,系统就会拿到一条已经被数据库关闭的连接。为了避免这种情况,Druid会判断池中的连接如果非运行时间大于maxEvictableIdleTimeMillis,也会强行把它关闭,而不用判断空闲连接数是否小于minIdle。

原来Druid是根据这两个配置来确认什么时候回收空闲的连接,为了验证是否如此,看一下源码中如何使用这两个配置。

我们使用的Druid版本是1.1.10。

com.alibaba.druid.pool.DruidDataSource

java 复制代码
    public class DestroyTask implements Runnable {

        @Override
        public void run() {
            shrink(true, keepAlive);

            if (isRemoveAbandoned()) {
                removeAbandoned();
            }
        }

    }

追踪源码,定位到DruidDataSource,*DestroyTask shrink()*负责释放连接:

java 复制代码
public void shrink(boolean checkTime, boolean keepAlive) {
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException e) {
            return;
        }

        int evictCount = 0;
        int keepAliveCount = 0;
        try {
            if (!inited) {
                return;
            }

            final int checkCount = poolingCount - minIdle;
            final long currentTimeMillis = System.currentTimeMillis();
            for (int i = 0; i < poolingCount; ++i) {
                DruidConnectionHolder connection = connections[i];

                if (checkTime) {
                    if (phyTimeoutMillis > 0) {
                        long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
                        if (phyConnectTimeMillis > phyTimeoutMillis) {
                            evictConnections[evictCount++] = connection;
                            continue;
                        }
                    }

                    long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;

                    if (idleMillis < minEvictableIdleTimeMillis) {
                        break;
                    }

                    if (checkTime && i < checkCount) {
                        evictConnections[evictCount++] = connection;
                    } else if (idleMillis > maxEvictableIdleTimeMillis) {
                        evictConnections[evictCount++] = connection;
                    } else if (keepAlive) {
                        keepAliveConnections[keepAliveCount++] = connection;
                    }
                } else {
                    if (i < checkCount) {
                        evictConnections[evictCount++] = connection;
                    } else {
                        break;
                    }
                }
            }
			.........
			
    }

在*shrink()*方法中我们找到了minEvictableIdleTimeMillis和maxEvictableIdleTimeMillis,等等,好像哪里不对?

java 复制代码
		if (idleMillis < minEvictableIdleTimeMillis) {
          	break;
         }

         if (checkTime && i < checkCount) {
             evictConnections[evictCount++] = connection;
         } else if (idleMillis > maxEvictableIdleTimeMillis) {
             evictConnections[evictCount++] = connection;
         } else if (keepAlive) {
             keepAliveConnections[keepAliveCount++] = connection;
         }

这里与文档的解释好像不一样?当连接空闲时间小于minEvictableIdleTimeMillis,退出循环检测,如果大于minEvictableIdleTimeMillis,判断空闲时间大于maxEvictableIdleTimeMillis,将连接加入需要释放的数组中。

那也就是说,如果按照缺省配置,minEvictableIdleTimeMillis 30分钟,maxEvictableIdleTimeMillis 7天,确实可能会出现Druid认为连接还存活着,但MySQL判断空闲时间超过配置,将会回收连接。

配置建议

maxEvictableIdleTimeMillis 要小于等于 MySQL Server端interactive_timeout 配置

相关推荐
zxrhhm5 分钟前
MySQL中使用PROFILING来查看SQL执行流程
数据库·mysql
醇氧5 分钟前
【postgresql】表操作
数据库·sql·postgresql
PGCCC6 分钟前
使用预加载库优化 PostgreSQL 函数#postgresql认证
数据库·postgresql
清风撩耳ing26 分钟前
MySQL之MHA高可用集群及故障切换
mysql·adb
baozongwi38 分钟前
CTF常用sql注入(一)联合注入和宽字节
数据库·sql·web安全
freesharer1 小时前
Zabbix 配置WEB监控
前端·数据库·zabbix
尾巴尖上的阳光1 小时前
ETCD概述--使用/特性/架构/原理
数据库·架构·etcd
关兮月1 小时前
MySQL存储与优化 一、MySQL架构原理
数据库·mysql
我想我不够好。1 小时前
SQL-DQL
数据库·sql
JasonHome1 小时前
ElasticSearch与MySQL概念对比
mysql·elasticsearch