一、问题背景
最近在某个项目的生产环境碰到一个数据库连接问题,使用的连接池是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失效的因素,作为配置的一个考量。
五、参考资料
- issue链接:https://github.com/alibaba/druid/issues/2323
- 配置文档链接:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE#1-%E9%80%9A%E7%94%A8%E9%85%8D%E7%BD%AE