druid释放空闲连接的问题 re

一、问题背景


  最近在某个项目的生产环境碰到一个数据库连接问题,使用的连接池是alibaba的druid_1.1.10,问题表现为:DBA监测到应用集群到oracle的连接数总会在半夜降低,并且大大低于每个节点druid配置的minIdle总和。
  一开始怀疑此问题产生的原因是oracle侧主动关闭了连接,但很难去验证这个点,一方面是和DBA沟通起来比较麻烦,另一方面是没有确切的证据,纯粹靠猜想很难服众,所以退而求其次,尝试在druid连接池上去找原因。既然是半夜这种交易量小的时间点降低连接数,那么应该和druid对空闲连接的处理有关。
  在github拉取了druid源码后,载入idea,使用minEvictableIdleTimeMillis进行了全局搜索,在结果列表中找到了一些可能与连接回收有关的类,最终定位到了DruidDataSource的内部类DestoryTask,简单的扫了一眼代码之后,基本就能确定DestroyTask是用于负责检测和销毁空闲连接的类了。
  由于druid源码编译还得花时间研究,我直接搭建了一个简单的springboot工程,引入druid后对DruidDataSource的init()方法打断点,启动应用开始一步步调试...
 

二、源码分析


  DruidDataSource init时会启动一个销毁连接的线程,由于destoryScheduler为空,因此创建了DestroyConnectionThread线程去执行,如下图:

   DestroyConnectionThread做的事情很简单,就是每隔固定的时间去执行一下DestoryTask的run方法,执行的间隔时间基于druid配置timeBetweenEvictionRunsMillis的值:

 

   DestoryTask的run方法调用shrink方法,该方法是空闲连接检查的核心方法,至于removeAbandoned方法是用于回收借出去但一直未归还的连接(这种连接可能导致连接泄露),它与druid的配置removeAbandoned有关,这里就不细讲了:

  shrink方法逻辑如下:

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];

            //DestroyThread调用shrink时,checkTime=true,keepAlive基于配置的值(默认为false)
            if (checkTime) {
                //phyTimeoutMillis参数(默认值为-1)设定了一条物理连接的存活时间,
                //不同的数据库对一个连接有最大的维持时间,比如mysql是8小时,设置该
                //参数是为了防止应用获取某连接时,该连接在数据库侧已关闭而导致异常。
                if (phyTimeoutMillis > 0) {
                    //如果某条连接已超过phyTimeoutMillis,则将其放入需要剔除的连接数组evictConnections中
                    long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
                    if (phyConnectTimeMillis > phyTimeoutMillis) {
                        evictConnections[evictCount++] = connection;
                        continue;
                    }
                }

                //获取连接空闲时间
                long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
				
                //如果某条连接空闲时间小于minEvictableIdleTimeMillis,则不用继续检查剩下的连接了
                if (idleMillis < minEvictableIdleTimeMillis) {
                    break;
                }

                //判断此连接的状态,将其放入不同处理的连接数组中
                if (checkTime && i < checkCount) {
                    //这里checkTime有点多余,一定为true,因为它是if(checkTime)分支中的逻辑
                    //如果此连接仍在checkCount范围之内,即它是一个多出最小空闲连接数的连接,
                    //那么就将它加入到需要剔除的连接数组evictConnections中
                    evictConnections[evictCount++] = connection;
                } else if (idleMillis > maxEvictableIdleTimeMillis) {
                    //如果连接空闲时间已经大于maxEvictableIdleTimeMillis,也将它加入到需要
                    //剔除的连接数组evictConnections中
                    evictConnections[evictCount++] = connection;
                } else if (keepAlive) {
                    //如果连接超过checkCount范围,并且空闲时间小于maxEvictableIdleTimeMillis,
                    //并且开启了keepAlive,那么就将它加入到需要维持的连接数组keepAliveConnections中
                    keepAliveConnections[keepAliveCount++] = connection;
                }
            } else {
                //对于不需要checkTime的情形,就非常简单了,将比minIdle连接数多的连接放入
                //需要剔除的连接数组evictConnections中
                if (i < checkCount) {
                    evictConnections[evictCount++] = connection;
                } else {
                    break;
                }
            }
        }

        //剔除连接和需要维持的连接都作为被移出连接,然后对连接池中的connections元素进行移动,
        //使得有用的连接重新放在连接数组connections的头部,并将其余元素置为null
        int removeCount = evictCount + keepAliveCount;
        if (removeCount > 0) {
            System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
            Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
            poolingCount -= removeCount;
        }
        keepAliveCheckCount += keepAliveCount;
    } finally {
        lock.unlock();
    }

    //处理需要剔除的连接数组evictConnections,对其中的连接进行关闭,
    //并维护监控指标:destroyCountUpdater,然后将evictConnections清空
    if (evictCount > 0) {
        for (int i = 0; i < evictCount; ++i) {
            DruidConnectionHolder item = evictConnections[i];
            Connection connection = item.getConnection();
            JdbcUtils.close(connection);
            destroyCountUpdater.incrementAndGet(this);
        }
        Arrays.fill(evictConnections, null);
    }

    //处理需要维持连接的连接数组keepAliveConnections
    if (keepAliveCount > 0) {
        //维护监控指标
        this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount); 
        for (int i = keepAliveCount - 1; i >= 0; --i) {
            DruidConnectionHolder holer = keepAliveConnections[i];
            Connection connection = holer.getConnection();
            //更新连接的keepAlive检查计数器
            holer.incrementKeepAliveCheckCount(); 

            boolean validate = false;
            try {
                //使用配置的validationQuery Sql检查当前连接是否有效,validateConnection
                //方法非常简单,如果检查过程中抛出异常都会被此处catch住并处理
                this.validateConnection(connection);
                validate = true;
            } catch (Throwable error) {
                if (LOG.isDebugEnabled()) {
                    LOG.debug("keepAliveErr", error);
                }
                // skip
            }
			
            if (validate) {
                //如果连接有效性检查成功,则更新连接的最近活跃时间,并尝试将连接放回连接池,
                //put(holder)不一定保证放回成功,在连接池已满的情况下将不会放入,方法中通过
                //使用条件变量以及poolingPeak等机制保证了连接不会被泄露
                holer.lastActiveTimeMillis = System.currentTimeMillis();
                put(holer);
            } else {
                //如果连接有效性检查失败,则关闭此连接
                JdbcUtils.close(connection);
            }
        }
        //清空连接数组keepAliveConnections
        Arrays.fill(keepAliveConnections, null);
    }
}

 

三、验证结论


  根据调试过程中的源码分析,可知druid_1.1.10判断连接是否销毁还是保活的逻辑如下(只讨论checkTime为true的情况):
 

  到这里,我们就可以下一个结论了:druid对于空闲连接还是有可能回收的,只要它未开启keepAlive并且闲置时间过长就会回收空闲连接,从而使得连接池中的连接数小于配置的minIdle值。

  为了验证结论,我开启了druid monitor的web页面访问,然后在如下的页面中去观察池中连接的情况:

 

  与druid空闲连接回收的相关参数配置如下图:

 

  首先不开启keepAlive功能(druid也是默认关闭的),在应用启动的时候,从druid monitor中观察到连接池中的连接数如下:

  等待大约2~3分钟之后(再此期间不要发起任何数据库请求),再次观察连接池中的连接数,可以发现连接数为0:

 

  接着配置"spring.datasource.druid.keep-alive=true"以打开keepAlive,重启应用并重复上述过程,结果如下:

 

  可以发现keepAlive起作用了,池中连接数维持在20,结论得到验证。接着回过头去查看了一下maxEvictableIdleTimeMillis这个参数的默认值为25200000,刚好7个小时,差不多能和DBA监测到的连接降低时间对上。

 

四、其他发现


  在解决问题的过程中,参考了官方文档以及他人在druid项目中提的issue,经历了怀疑问题、确认问题、解决问题三个阶段,不过个人在调试过程中仍然发现有如下问题:
(1)官方的配置文档中对属性minEvictableIdleTimeMillis做了如下描述:

  然而实际上代码体现出来的逻辑并不是这么一回事,maxEvictableIdleTimeMillis更像起到了决定性的作用。

(2)timeBetweenEvictionRunsMillis、minEvictableIdleTimeMillis、maxEvictableIdleTimeMillis这三者设置的大小如果满足一定条件,也会导致keepAlive失效。根据源码,如果在某一轮扫描中(间隔时间timeBetweenEvictionRunsMillis),检测到连接的空闲时间小于minEvictableIdleTimeMillis,那么这些连接不需要keepAlive,自然也不会更新lastActiveTimeMillis,这里存在一个临界条件,使得连接空闲时间同时大于minEvictableIdleTimeMillis和maxEvictableIdleTimeMillis,这个临界条件触发的前提是:
//1.满足下面不等式
maxEvictableIdleTimeMillis - minEvictableIdleTimeMillis <= timeBetweenEvictionRunsMillis
//2.连接一直处于未使用状态,那么在空闲时间小于minEvictableIdleTimeMillis之前,连接的lastActiveTimeMillis都不会被更新

  下面是我的一个测试,druid相关配置情况如图:

 

  启用应用并静静等待1~2分钟,通过druid monitor查看连接池状态:

 

  通过浏览器调用一个http查询接口,连接池连接数恢复:

 

  静静等待1~2分钟,可以看到连接池中的连接又被清空:

 

  结论:虽然maxEvictableIdleTimeMillis这个参数我们一般不配置,它的默认值也比较大(7小时),但是实际在配置druid时,还是建议考虑keepAlive失效的因素,作为配置的一个考量。

 

五、参考资料


原文地址:https://www.cnblogs.com/manayi/p/15555040.html