We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
2021.12.16 凌晨,我们的应用数据库因故发生了主备切换,之后某个 Pod 就持续报错 GetConnectionTimeoutException,并且该 Pod 的进程一直挂起,无法正常提供服务。
GetConnectionTimeoutException
不过比较奇怪的是,当时连接同一数据库的其他 Pod 虽然也有几条报错(主要是数据库连接超时的报错),但很快其他 Pod 都恢复了正常,就只有这一个 Pod 一直没有恢复。
异常 Pod 的部分报错信息如下:
[2021-12-16 02:10:06.617] [ERROR] [thread-30849] com.alibaba.druid.pool.DruidPooledStatement errorCheck:367 - CommunicationsException, druid version 1.2.5, jdbcUrl : jdbc:mysql://xxx:3306/xxx?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true, testWhileIdle true, idle millis 1785, minIdle 5, poolingCount 0, timeBetweenEvictionRunsMillis 300000, lastValidIdleMillis 1785, driver com.mysql.cj.jdbc.Driver, exceptionSorter com.alibaba.druid.pool.vendor.MySqlExceptionSorter ...... 2021-12-16 02:10:06.617] [ERROR] thread-30849] Caused by: com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 100000, active 399, maxActive 100, creating 0 at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1745) at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1415) at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1395) at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1385) at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:100) at org.springframework.jdbc.datasource.DataSourceUtils.fetchConnection(DataSourceUtils.java:158) at org.springframework.jdbc.datasource.DataSourceUtils.doGetConnection(DataSourceUtils.java:116) at org.springframework.orm.ibatis.SqlMapClientTemplate.execute(SqlMapClientTemplate.java:182) ... 26 common frames omitted
从报错信息来看, GetConnectionTimeoutException 即获取连接超时。但数据库和网络都是正常的,为什么获取数据库连接超时呢?
此外错误栈中的 active 居然比 maxActive 大,最大连接数 maxActive 是 100,为什么活跃连接数能达到 399?
active
maxActive
在 Druid 的 issues 中, GetConnectionTimeoutException 也是一个非常常见的问题,那这个异常到底是怎么产生的呢,又该如何避免呢?
接下来就让我们带着这些疑问,通过阅读 Druid 源码进行深入的分析。
在进行问题分析前,先看一下当前的 Druid 配置。Druid 的版本为 1.2.5,因此在接下来的内容中,我都将根据 1.2.5 版本的源码进行分析。Druid 的具体配置如下:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="${jdbc_url}" /> <property name="username" value="${jdbc_user}" /> <property name="password" value="${jdbc_password}" /> <property name="maxActive" value="100"/> <property name="initialSize" value="1"/> <property name="maxWait" value="100000"/> <property name="minIdle" value="1"/> <property name="timeBetweenEvictionRunsMillis" value="300000"/> <property name="validationQuery" value="SELECT 1"/> <property name="testWhileIdle" value="true"/> <property name="removeAbandoned" value="false"/> <property name="removeAbandonedTimeout" value="600"/> </bean>
在错误栈中,我们看到了 pollingCount 、active 、maxActive 等变量,那这些变量到底是什么含义呢?
pollingCount
这一章节就先来了解一下。同时了解了这些变量的含义,也更利于后续阅读源码分析问题。
这里列出一些本文会涉的及关键配置,更多配置可参考 DruidDataSource配置属性列表 。
连接池中至少需要保持的连接数。
连接池中最大连接数。
单次获取连接的最长等待时间。
DruidDataSource 是数据源,每个数据源都有一个对应的 DruidDataSource 实例。
当前活跃连接数。每当成功获取到一个连接后,activeCount 都会加一,同时也会将 holder 的 active 属性设置为 true。
holder
private DruidPooledConnection getConnectionInternal(long maxWait) { // ... if (holder != null) { if (holder.discard) { continue; } activeCount++; holder.active = true; } }
当前所数据源拥有的连接池。其类型是 DruidConnectionHolder[] 。
DruidConnectionHolder[]
连接池中的的连接数。
每当放入连接到连接池中,就会将 poolingCount 加一,例如:
private boolean put(DruidConnectionHolder holder, long createTaskId) { // ... connections[poolingCount] = holder; incrementPoolingCount(); // ... } private final void incrementPoolingCount() { poolingCount++; }
每当将连接从连接池中取出,就会将 poolingCount 减一,例如:
private DruidConnectionHolder pollLast(long nanos) throws InterruptedException, SQLException { // ... decrementPoolingCount(); DruidConnectionHolder last = connections[poolingCount]; connections[poolingCount] = null; // ... return last; } private final void decrementPoolingCount() { poolingCount--; }
数据库连接以及相关属性。
数据库连接,类型为 Connection。
用来标记连接是否活跃。
连接上次活跃时间。
连接上次保持活跃的时间。每次通过 keepAlive 保持连接活跃,就会更新此时间。
接下来,就让我们从错误栈开始深入源码,一层一层寻找问题的根源。
首先 GetConnectionTimeoutException 是由 getConnectionInternal 方法抛出的,该方法的主要作用就是获取数据库连接,当前没有获取到连接也就是 holder 为 null 的时候,就会抛出异常,同时打印出当前 active 和 maxActive 的值。
getConnectionInternal
null
if (holder == null) { // ... StringBuilder buf = new StringBuilder(128); buf.append("wait millis ")// .append(waitNanos / (1000 * 1000))// .append(", active ").append(activeCount)// .append(", maxActive ").append(maxActive)// .append(", creating ").append(creatingCount)// ; // ... String errorMessage = buf.toString(); if (this.createError != null) { throw new GetConnectionTimeoutException(errorMessage, createError); } else { throw new GetConnectionTimeoutException(errorMessage); } }
holder 是 DruidConnectionHolder 的实例。那为什么 holder 为 null 呢?接下来就让我们再看一下 holder 是如何创建的。
DruidConnectionHolder
接下来就看 holder 是如何创建的。
在 getConnectionInternal 中, holder 的创建是在 for 循环中进行的,一旦创建成功则退出循环。
在创建 holder 时,有同步和异步两种方式。
if (createScheduler != null && poolingCount == 0 && activeCount < maxActive && creatingCountUpdater.get(this) == 0 && createScheduler instanceof ScheduledThreadPoolExecutor) { ScheduledThreadPoolExecutor executor = (ScheduledThreadPoolExecutor) createScheduler; if (executor.getQueue().size() > 0) { createDirect = true; continue; } }
如果需要同步创建连接,需要同时满足以下几个条件:
createScheduler != null
poolingCount == 0
activeCount < maxActive
creatingCountUpdater.get(this) == 0
满足上述条件后,createDirect 会被设置为 true,然后在下一次循环中就会同步创建连接。也正因为有上述条件在,所以只有很少的线程会进入到同步创建连接的流程中。
createDirect
true
private DruidPooledConnection getConnectionInternal(long maxWait) { // ... if (createDirect) { if (creatingCountUpdater.compareAndSet(this, 0, 1)) { PhysicalConnectionInfo pyConnInfo = DruidDataSource.this.createPhysicalConnection(); holder = new DruidConnectionHolder(this, pyConnInfo); holder.lastActiveTimeMillis = System.currentTimeMillis(); // ... boolean discard = false; lock.lock(); try { if (activeCount < maxActive) { activeCount++; holder.active = true; if (activeCount > activePeak) { activePeak = activeCount; activePeakTime = System.currentTimeMillis(); } break; } else { discard = true; } } finally { lock.unlock(); } } } }
如果没有同步创建连接,则接下来会通过 pollLast(nanos) 或 takeLast() 异步创建连接。
pollLast(nanos)
takeLast()
nanos 是根据 maxWait 计算出来的,如果设置了 maxWait 就会调用 pollLast(nanos),否则调用 takeLast() 。
nanos
maxWait
private DruidPooledConnection getConnectionInternal(long maxWait) { final long nanos = TimeUnit.MILLISECONDS.toNanos(maxWait); // ... if (maxWait > 0) { holder = pollLast(nanos); } else { holder = takeLast(); } }
pollLast
takeLast
在 pollLast 方法中,如果当前连接池中的连接数 poolingCount 为 0,则调用 emptySignal 使用信号量 empty,并创建异步线程去创建连接。
emptySignal
empty
private DruidConnectionHolder pollLast(long nanos) throws InterruptedException, SQLException { long estimate = nanos; for (;;) { if (poolingCount == 0) { emptySignal(); // send signal to CreateThread create connection // ... } } }
然后,pollLast 自己再使用 notEmpty 信号量进入等待:
notEmpty
estimate = notEmpty.awaitNanos(estimate); // signal by // recycle or // creator
本次等待的最长时间 estimate 就是根据 maxWait 计算出来的。
estimate
在有连接被回收,会新创建出来后,就会被唤醒。如果唤醒后本线程没有成功创建连接,并且目前总的等待时间还没到maxWait,就会再次进入 await。
如果等待时间到了 maxWait ,但依旧没有成功创建连接,即 poolingCount 为 0,则会返回 null。
if (poolingCount == 0) { if (estimate > 0) { continue; } return null; }
通过信号创建的连接会被放入到连接池 connections 中,然后 pollLast(nanos) 收到创建成功的信号后,就可以从连接池中取出连接并返回了:
private DruidConnectionHolder pollLast(long nanos) throws InterruptedException, SQLException { // ... decrementPoolingCount(); DruidConnectionHolder last = connections[poolingCount]; connections[poolingCount] = null; // ... return last; }
无参的 takeLast() 就简单很多了,它的流程和 pollLast(nanos) 几乎一致,唯一不同的是它没有等待超时时间,只有等待回收线程,会创建线程唤醒后,才会继续执行。
DruidConnectionHolder takeLast() throws InterruptedException, SQLException { try { while (poolingCount == 0) { emptySignal(); // send signal to CreateThread create connection try { notEmpty.await(); // signal by recycle or creator } } } decrementPoolingCount(); DruidConnectionHolder last = connections[poolingCount]; connections[poolingCount] = null; return last; }
使用信号异步创建连接的 emptySignal() 方法代码如下:
emptySignal()
private void emptySignal() { if (createScheduler == null) { empty.signal(); return; } if (createTaskCount >= maxCreateTaskCount) { return; } if (activeCount + poolingCount + createTaskCount >= maxActive) { return; } submitCreateTask(false); }
如果设置的创建线程池 createScheduler 为 null,则会发送一个信号去创建连接。否则创建一个 CreateConnectionTask 来创建连接。
CreateConnectionTask
这里发出的创建信号会被 CreateConnectionThread 接收,CreateConnectionThread 会在数据源初始化的时候创建。
CreateConnectionThread
public void init() throws SQLException { // ... createAndStartCreatorThread(); createAndStartDestroyThread(); }
收到信号后,会判断当前活跃连接数以及连接池中的连接数是否大于 maxActive,如果大于则不创建,避免创建出超过 maxActive 数量的连接;如果小于,则创建了物理连接,然后在调用 DruidDataSource 的 put 方法将连接放入连接池中。
public void run() { for (;;) { // ... try { boolean emptyWait = true; if (createError != null && poolingCount == 0 && !discardChanged) { emptyWait = false; } if (emptyWait && asyncInit && createCount < initialSize) { emptyWait = false; } if (emptyWait) { // 必须存在线程等待,才创建连接 if (poolingCount >= notEmptyWaitThreadCount // && (!(keepAlive && activeCount + poolingCount < minIdle)) && !isFailContinuous() ) { empty.await(); } // 防止创建超过maxActive数量的连接 if (activeCount + poolingCount >= maxActive) { empty.await(); continue; } } } // ... PhysicalConnectionInfo connection = null; try { connection = createPhysicalConnection(); } // ... boolean result = put(connection); } }
CreateConnectionTask 也是类似的逻辑。
在 put 方法中,首先会判断当前连接数是否大于最大连接数,如果小于,则会将连接放入到 connections 中,然后将 poolingCount 的值加一。
connections
private boolean put(DruidConnectionHolder holder, long createTaskId) { if (poolingCount >= maxActive) { if (createScheduler != null) { clearCreateTask(createTaskId); } return false; } // ... connections[poolingCount] = holder; incrementPoolingCount(); //... }
以上便是便是大致的连接创建流程。
总的来说就是,应用需要执行 SQL 的时候,就通过 getConnectionInternal 获取连接,其中创建连接分为同步和异步,异步则通过信号量创建连接。成功获取连接后还会将 activeCount 加一。
activeCount
结合 1.2.1 中的异常信息,报错时 poolingCount 为 0, 所以会调用 emptySignal() 创建连接。但activeCount + poolingCount + createTaskCount 显然是大于 maxActive 的,因此 pollLast 最终会等待 maxWait 然后超时。所以最终 getConnectionInternal 抛出了 GetConnectionTimeoutException 异常。
poolingCount
activeCount + poolingCount + createTaskCount
所以接下来问题的关键就是,为什么 activeCount 会大于 maxActive?
要解答这个问题,我们就需要先知道 activeCount 什么时候减少。
前面已经知道了 activeCount 会在获取到连接后增加,那相应的,activeCount 也会在释放连接时减少。所以接下来就详细了解连接时如何释放的。
提到连接释放,首先想到的应该是我们在应用中主动调用 connection.close()方法来释放连接。
connection.close()
那 connection.close() 会立即释放物理连接吗?显然是不会的。
public void close() throws SQLException { // ... recycle(); } public void recycle() throws SQLException { // ... if (!this.abandoned) { DruidAbstractDataSource dataSource = holder.getDataSource(); dataSource.recycle(this); } }
connection.close() 会最终会调用 dataSource.recycle(DruidPooledConnection pooledConnection)释放连接。 那 dataSource.recycle 又是怎么实现的呢?
dataSource.recycle(DruidPooledConnection pooledConnection)
dataSource.recycle
在 recyle 中会有一些列的连接状态判断,如物理连接是否已关闭等,然后决定是否立即释放连接。不过大部分情况下会运行到下面的逻辑,即如果连接状态是活跃的,则将 activeCount 减一,并将状态再设置为 false。最后再调用 putLast。
putLast
/** * 回收连接 */ protected void recycle(DruidPooledConnection pooledConnection) throws SQLException { // ... if (holder.active) { activeCount--; holder.active = false; } result = putLast(holder, currentTimeMillis); // ... }
在 putLast 中则会更新连接的上次活跃时间,然后将连接再度放回到连接池中,并将 poolingcount 加一。
poolingcount
boolean putLast(DruidConnectionHolder e, long lastActiveTimeMillis) { if (poolingCount >= maxActive || e.discard) { return false; } e.lastActiveTimeMillis = lastActiveTimeMillis; connections[poolingCount] = e; incrementPoolingCount(); // ... return true; }
也就是说,通过 .close 方法主动释放连接,不会真的释放,而是将连接还回到连接池中。
.close
既然前面的 recycle 不会关闭连接,但连接池也不可能无限大,那连接到底是如何回收的呢?这就要提到 Druid 的连接释放任务了。
recycle
通 CreateConnectionThread 一样, Druid 在初始化数据源时会创建释放连接的线程 DestroyThread ,然后创建释放任务 DestroyTask。
DestroyThread
当应用设置的释放线程池 destroyScheduler 不为空时,就会在 destroyScheduler 中运行 DestroyTask;否则会创建一个消费连接的线程池,然后在该线程池中运行释放连接的任务。与 createScheduler 类似,大部分情况下我们不会设置 destroyScheduler。
protected void createAndStartDestroyThread() { destroyTask = new DestroyTask(); if (destroyScheduler != null) { // ... destroySchedulerFuture = destroyScheduler.scheduleAtFixedRate(destroyTask, period, period, TimeUnit.MILLISECONDS); return; } // ... destroyConnectionThread = new DestroyConnectionThread(threadName); destroyConnectionThread.start(); }
DestroyConnectionThread 本质上就是一个死循环,在循环中每隔 timeBetweenEvictionRunsMillis 执行一次释放连接的任务。
timeBetweenEvictionRunsMillis
public class DestroyConnectionThread extends Thread { // ... public void run() { for (;;) { // 从前面开始删除 try { // ... if (timeBetweenEvictionRunsMillis > 0) { Thread.sleep(timeBetweenEvictionRunsMillis); } else { Thread.sleep(1000); // } destroyTask.run(); } catch (InterruptedException e) { break; } } } }
释放连接的关键逻辑就在 DestroyTask 中。
在 DestroyTask 中,主要是通过 shrink 方法来释放连接。
shrink
public class DestroyTask implements Runnable { public DestroyTask() { } @Override public void run() { shrink(true, keepAlive); if (isRemoveAbandoned()) { removeAbandoned(); } } }
所以接下来的关键就是 shrink(boolean checkTime, boolean keepAlive)� 方法。
shrink(boolean checkTime, boolean keepAlive)
shrink 接收两个参数:
checkTime
keepAlive
在 shrink 中,首先会通过 for 循环,依次遍历连接池中的所有连接并进行处理。
在循环中,首先会判断当前是否有 fatalError,如果有,则会将连接放入 keepAliveConnections 列表中,keepAliveConnections 存储了需要保持活跃的连接。什么时候会出现 fatalError 呢?主要是执行 SQL 时可能出现,比如数据库断网、数据库重启等。
for (int i = 0; i < poolingCount; ++i) { DruidConnectionHolder connection = connections[i]; if ((onFatalError || fatalErrorIncrement > 0) && (lastFatalErrorTimeMillis > connection.connectTimeMillis)) { keepAliveConnections[keepAliveCount++] = connection; continue; } }
如果没有 fatalError 并且开启了 checkTime 为 true,则会继续根据连接时间判断连接应该被放入 keepAliveConnections 还是 evictConnections 队列。
具体逻辑如下:
-1
i < checkCount
final int checkCount = poolingCount - minIdle; // ... for (int i = 0; i < poolingCount; ++i) { // ... if (phyTimeoutMillis > 0) { long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis; if (phyConnectTimeMillis > phyTimeoutMillis) { evictConnections[evictCount++] = connection; continue; } } long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis; if (idleMillis < minEvictableIdleTimeMillis && idleMillis < keepAliveBetweenTimeMillis ) { break; } if (idleMillis >= minEvictableIdleTimeMillis) { if (checkTime && i < checkCount) { evictConnections[evictCount++] = connection; continue; } else if (idleMillis > maxEvictableIdleTimeMillis) { evictConnections[evictCount++] = connection; continue; } } if (keepAlive && idleMillis >= keepAliveBetweenTimeMillis) { keepAliveConnections[keepAliveCount++] = connection; } }
正常情况下,connections 中的连接都是按 lastActiveTimeMillis 先后顺序排列的,因此释放连接时,从前往后释放,一旦发现连接空闲时间小于 minEvictableIdleTimeMillis 和 keepAliveBetweenTimeMillis 就结束本次释放流程,这样就可以减少循环次数,提高性能。并且连接保活时,也是从前往后进行保活,因为前面的连接更可能超时。这也正是 Druid 设计的巧妙之处。
当然,这里是“正常情况”,那必然也会有异常情况,异常情况是什么样呢?这里暂时先不展开,后面会详细讲解。
在确定了需要释放和保持活跃的连接数量之后,下一步就是从连接池中将连接删除,删除的方式就是将 connections 中前 removeCount 个连接删掉,然后将剩余连接再依次从前往后放入到 connections,最后再用 null 填充 connections,以便后续存储保活的连接。
int removeCount = evictCount + keepAliveCount; if (removeCount > 0) { System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount); Arrays.fill(connections, poolingCount - removeCount, poolingCount, null); poolingCount -= removeCount; }
接下来对于 evictConnections 中的连接,就可以直接关闭了。
DruidConnectionHolder item = evictConnections[i]; Connection connection = item.getConnection(); JdbcUtils.close(connection); destroyCountUpdater.incrementAndGet(this);
对于 keepAliveConnections 中的连接,则先判断连接是否可用,如果连接可用,则调用 put 方法将连接再放回连接池。和前面应用主动释放连接时调用的 putLast 不同的是,put 不会更新连接的 lastActiveTimeMillis 。
put
如果连接不可用,则�再调用 emptySignal() 通过信号创建一个新的连接。
DruidConnectionHolder holer = keepAliveConnections[i]; Connection connection = holer.getConnection(); holer.incrementKeepAliveCheckCount(); boolean validate = false; try { this.validateConnection(connection); validate = true; } catch (Throwable error) { // skip } boolean discard = !validate; if (validate) { holer.lastKeepTimeMillis = System.currentTimeMillis(); boolean putOk = put(holer, 0L); if (!putOk) { discard = true; } } if (discard) { try { connection.close(); } // ... try { if (activeCount + poolingCount <= minIdle) { emptySignal(); } } // ... }
进行连接保活时,优先将时间更早的连接进行保活。但有时候我们可能期望一个连接不要太长时间空闲,比如连接空闲 1 分钟后,即使连接可用也要释放,这时应该怎么办呢?
Druid 也考虑到了这个问题,可通过设置 testWhileIdle 为 true 进行空闲连接释放。
testWhileIdle
在应用中我们会通过 getConnection 获取连接,getConnection 会调用 getConnectionDirect,getConnectionDirect 继续调用 getConnectionInternal。
getConnection
getConnectionDirect
public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException { // ... for (;;) { // handle notFullTimeoutRetry DruidPooledConnection poolableConnection; try { poolableConnection = getConnectionInternal(maxWaitMillis); } if (testOnBorrow) { // ... } else { // ... if (testWhileIdle) { final DruidConnectionHolder holder = poolableConnection.holder; long currentTimeMillis = System.currentTimeMillis(); long lastActiveTimeMillis = holder.lastActiveTimeMillis; long lastExecTimeMillis = holder.lastExecTimeMillis; long lastKeepTimeMillis = holder.lastKeepTimeMillis; if (checkExecuteTime && lastExecTimeMillis != lastActiveTimeMillis) { lastActiveTimeMillis = lastExecTimeMillis; } if (lastKeepTimeMillis > lastActiveTimeMillis) { lastActiveTimeMillis = lastKeepTimeMillis; } long idleMillis = currentTimeMillis - lastActiveTimeMillis; long timeBetweenEvictionRunsMillis = this.timeBetweenEvictionRunsMillis; if (timeBetweenEvictionRunsMillis <= 0) { timeBetweenEvictionRunsMillis = DEFAULT_TIME_BETWEEN_EVICTION_RUNS_MILLIS; } if (idleMillis >= timeBetweenEvictionRunsMillis || idleMillis < 0 // unexcepted branch ) { boolean validate = testConnectionInternal(poolableConnection.holder, poolableConnection.conn); if (!validate) { discardConnection(poolableConnection.holder); continue; } } } } return poolableConnection; } }
在 getConnectionDirect 中,获取到连接后,如果开启了 testWhileIdle ,则会继续判断连接是否空闲,如果是,则会调用 discardConnection� 释放连接,然后再进行下一次循环创建新的连接。连接是否空闲,主要根据 lastActiveTimeMillis 和 timeBetweenEvictionRunsMillis 进行判断。
discardConnection�
lastActiveTimeMillis
在 discardConnection 中,如果连接 active 状态为 true,则会将 activeCount 减一,并将 active 设置为 false。
public void discardConnection(DruidConnectionHolder holder) { // ... Connection conn = holder.getConnection(); if (conn != null) { JdbcUtils.close(conn); } lock.lock(); try { if (holder.discard) { return; } if (holder.active) { activeCount--; holder.active = false; } discardCount++; holder.discard = true; if (activeCount <= minIdle) { emptySignal(); } } finally { lock.unlock(); } }
以上便是便是连接释放的流程。
当应用主动调用 .close 时,并不会真的是否连接,而是更新连接的 lastActiveTimeMillis,然后将连接返回到连接池,然后将 activeCount 减一。
然后 DestroyTask 再定期清理连接,判断连接应该被释放还是被保活。如果连接应该被保活,则再将连接放回到连接池。
此外应用获取连接时,也会判断连接的空闲时间,如果连接是空闲的,也会释放连接,并将 activeCount 减一。
那为什么 activeCount 会大于 maxActive 呢?可能唯一的解释就是 activeCount 不准确。
这里先说结论,的确如此。这是 Druid 1.2.8 之前版本的一个 BUG。
通过前面的分析,我们可以大概推测:当连接池中存在两个相同连接时,activeCount 就会少减一。因为当应用第一次释放连接时,会将连接的 active 设置为 false 并执行 activeCount--,第二次释放时,由于 active 已经为 false 了,因此就不会再执行 activeCount-- 了。
activeCount--
那么如何证明推测的正确性呢?
为了证明前面的推测,我写了一段代码来验证。
代码如下,Druid 版本为 1.2.5 :
public class Test { public static void main(String[] args) throws Exception { String jdbcUrl = "jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true"; String user = "root"; String password = "11111111"; DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setUsername(user); druidDataSource.setPassword(password); druidDataSource.setUrl(jdbcUrl); druidDataSource.setValidationQuery("select 1"); druidDataSource.setMinEvictableIdleTimeMillis(10 * 1000); druidDataSource.setMaxEvictableIdleTimeMillis(DEFAULT_MAX_EVICTABLE_IDLE_TIME_MILLIS); druidDataSource.setKeepAliveBetweenTimeMillis(12 * 1000); druidDataSource.setMinIdle(2); druidDataSource.setMaxWait(1000); druidDataSource.setMaxActive(4); druidDataSource.setTimeBetweenEvictionRunsMillis(7 * 1000); druidDataSource.setKeepAlive(true); DruidPooledConnection connection1 = druidDataSource.getConnection(); DruidPooledConnection connection2 = druidDataSource.getConnection(); connection2.close(); Thread.sleep(9 * 1000); connection1.close(); Thread.sleep(14 * 1000); DruidPooledConnection connection3 = druidDataSource.getConnection(); DruidPooledConnection connection4 = druidDataSource.getConnection(); System.out.println(connection3.getConnectionHolder() == connection4.getConnectionHolder()); connection3.close(); connection4.close(); } }
最终会输出 true,也就是 connection3 和 connection4 获取到的连接是一样的。
connection3
connection4
为什么为 true 呢?接下来就结合前面的源码理解,梳理一下整个时间线。
获取 connection1 和 connection2,其 holder 如下:
{id: 110, useCount: 1, lastActiveTime: 00, active: true}
{id: 100, useCount: 1, lastActiveTime: 00, active: true}
然后关闭 connection2,此时 connections 为 [connection2]:
[connection2]
{id: 100, useCount: 1, lastActiveTime: 00, active: false}
最终 activeAcount 为 1。
DestroyTask 第一次执行,此时 connection2 的空闲时间均小于 minEvictableIdleTimeMillis 和keepAliveBetweenTimeMillis,跳过回收。
minEvictableIdleTimeMillis
keepAliveBetweenTimeMillis
关闭 connection1,connection1 返回连接池,此时 connections 为 [connection2,connection1],connections 中的连接空闲时间是递减的:
[connection2,connection1]
{id: 110, useCount: 1, lastActiveTime: 09, active: false}
最终 activeAcount 为 0。
DestroyTask 执行,此时 connection2 空闲时间大于 minEvictableIdleTimeMillis,但 checkCount 为 0(poolingCount - minIdle�),不满足 i < checkCount ,connection2 会被放入 keepAliveConnections 列表。
connection2
而 connection1 的空闲时间均小于 minEvictableIdleTimeMillis 和keepAliveBetweenTimeMillis ,不处理。
connection1
最终 connections 为 [connection1,connection2] :
[connection1,connection2]
{id: 100, useCount: 1, lastActiveTime: 00, lastKeepTime: 14, active: false}
此时 activeAcount 依旧为 0。但 connections 中的 lastActiveTime 就不是按时间顺序排列的了。
DestroyTask 再次执行,此时 connection1 空闲时间为 11s,由于不满足条件,即不放入 evictConnections, 也不放入 keepAliveConnections,connection2 空闲时间为 21s,大于 keepAliveBetweenTimeMillis,因此放入 keepAliveConnections。
由于 removeCount 为 1( removeCount = evictCount + keepAliveCount),所以接下来会删掉 connections 的第 0 个元素,也就是 connection1,然后将 keepAliveConnections 添加到 connections 中。
最终 connections 为 [connection2,connection2] :
[connection2,connection2]
这时继续获取 conenction3,优先从 connections 末尾获取。
获取到 id 为 100 的连接后,继续判断连接的空闲时间。由于 lastActiveTime 小于 lastKeepTime,所以会用 lastKeepTime 来计算空闲时间,空闲时间为 9,小于 timeBetweenEvictionRunsMillis,因此认为连接可用,并返回。
id
所以 conenction3 为 {id: 100, useCount: 2, lastActiveTime: 00, lastKeepTime: 14, active: true}
{id: 100, useCount: 2, lastActiveTime: 00, lastKeepTime: 14, active: true}
同时也会 activeCount 加一,最终 activeCount 为 1。
此时 connections 为:
同理,获取到 conenction4 为 {id: 100, useCount: 3, lastActiveTime: 00, lastKeepTime: 14, active: true} ,并且也会将 activeCount 加一,最终 activeCount 为 2。
{id: 100, useCount: 3, lastActiveTime: 00, lastKeepTime: 14, active: true}
所以 connection3 和 connection4 本质上是同一个 holder。
另外,此时 connections 为空。
紧接着调用 close 方法关闭 connection3,将连接返回连接池,同时将 activeCount 减一。执行完毕后connections 为 [connections3] :
[connections3]
{id: 100, useCount: 3, lastActiveTime: 23, lastKeepTime: 14, active: false}
最终 activeCount 为 1,并且 holder 的 active 属性为 false。
在关闭 connection3 之后,立即又关闭 connection4, 但由于其 holder 的 active 已经是 false 了,所以 activeCount 不会再减一了。
最终虽然连接会被释放,但 activeCount 少减了一次。
由此可见,在某些特定条件下,由于 keepAlive 机制会将连接再放回连接池,可能会导致 connections 中会存在多个完全相同的连接 holder,并且该 holder 又可能刚好被多个应用线程获取到了,然后关闭连接时,由于最早关闭的线程已经将 holder 的 active 标记为 false 了,所以其他线程关闭连接时,就不会再把 activeCount 减一。最终导致了 activeCount 不准确。
那什么时候会触发这个 BUG 呢?这个 BUG 是个小概率事件,以下几种情况会提升 BUG 出现的概率:
那这个问题如何修复呢?只要保证 connections 中不能存在两个完全相同的 holder 就可以了。由于问题是有 keepAlive 机制在放回连接时触发的,所以可以在此加个判断,如果连接已经存在连接池中,就不放入。
官方在 1.2.8 这个版本也修复了该问题,详细修复代码参考 druid/compare/1.2.5...1.2.8 。
因此我们应用中的终极修复方式就是,将 Druid 升级到 1.2.8。
通过对源码的一层层深入分析,终于弄清楚了为什么会应用报错后会一直挂起,为什么 activeCount 不准确,以及如何解决。
表面上看是获取连接超时,实际上时 activeCount 计算不准确导致无法创建出新的连接。
导致 activeCount 计算不准确的原因是连接池中存在多个相同的连接,因此连接释放时,多次释放相同连接只会执行一次 activeCount--。
之所以连接池可能存在多个相同连接,主要是数据库异常或 Druid 参数配置有误时,刚好导致 keepAlive 将连接重复放入连接池。并且再次获取该连接时,刚好连接空闲时间判断错误,导致连接不被释放而是返回给了应用使用。
本质上是 Druid 1.2.8 之前的一个 BUG,数据库异常以及配置导致的空闲时间判断错误等种种巧合,触发了这个 BUG。但出现问题后通过重启又能解决问题,所以很难定位。
最后,将 Druid 升级到 1.2.8 吧,这样才能彻底解决问题。另外也需要注意以下 Druid 配置:
至此,本文结束。感谢阅读!
The text was updated successfully, but these errors were encountered:
No branches or pull requests
1. 背景
1.1 现象
2021.12.16 凌晨,我们的应用数据库因故发生了主备切换,之后某个 Pod 就持续报错
GetConnectionTimeoutException
,并且该 Pod 的进程一直挂起,无法正常提供服务。不过比较奇怪的是,当时连接同一数据库的其他 Pod 虽然也有几条报错(主要是数据库连接超时的报错),但很快其他 Pod 都恢复了正常,就只有这一个 Pod 一直没有恢复。
1.2 报错信息
异常 Pod 的部分报错信息如下:
从报错信息来看, GetConnectionTimeoutException 即获取连接超时。但数据库和网络都是正常的,为什么获取数据库连接超时呢?
此外错误栈中的
active
居然比maxActive
大,最大连接数maxActive
是 100,为什么活跃连接数能达到 399?
在 Druid 的 issues 中,
GetConnectionTimeoutException
也是一个非常常见的问题,那这个异常到底是怎么产生的呢,又该如何避免呢?
接下来就让我们带着这些疑问,通过阅读 Druid 源码进行深入的分析。
2.1.3 Druid 配置
在进行问题分析前,先看一下当前的 Druid 配置。Druid 的版本为 1.2.5,因此在接下来的内容中,我都将根据 1.2.5 版本的源码进行分析。Druid 的具体配置如下:
2. 关键变量
在错误栈中,我们看到了
pollingCount
、active
、maxActive
等变量,那这些变量到底是什么含义呢?
这一章节就先来了解一下。同时了解了这些变量的含义,也更利于后续阅读源码分析问题。
2.1 配置项
这里列出一些本文会涉的及关键配置,更多配置可参考 DruidDataSource配置属性列表 。
2.1.1 minIdle
连接池中至少需要保持的连接数。
2.1.2 maxActive
连接池中最大连接数。
2.1.3 maxWait
单次获取连接的最长等待时间。
2.2 DruidDataSource
DruidDataSource 是数据源,每个数据源都有一个对应的 DruidDataSource 实例。
2.2.1 activeCount
当前活跃连接数。每当成功获取到一个连接后,activeCount 都会加一,同时也会将
holder
的active
属性设置为 true。
2.2.2 connections
当前所数据源拥有的连接池。其类型是
DruidConnectionHolder[]
。2.2.3 poolingCount
连接池中的的连接数。
每当放入连接到连接池中,就会将 poolingCount 加一,例如:
每当将连接从连接池中取出,就会将 poolingCount 减一,例如:
2.3 DruidConnectionHolder
数据库连接以及相关属性。
2.3.1 conn
数据库连接,类型为 Connection。
2.3.2 active
用来标记连接是否活跃。
2.3.3 lastActiveTimeMillis
连接上次活跃时间。
2.3.4 lastKeepTimeMillis
连接上次保持活跃的时间。每次通过 keepAlive 保持连接活跃,就会更新此时间。
2. 源码分析
接下来,就让我们从错误栈开始深入源码,一层一层寻找问题的根源。
2.2.1 异常是如何抛出的?
首先
GetConnectionTimeoutException
是由getConnectionInternal
方法抛出的,该方法的主要作用就是获取数据库连接,当前没有获取到连接也就是holder
为null
的时候,就会抛出异常,同时打印出当前active
和maxActive
的值。
holder
是DruidConnectionHolder
的实例。那为什么holder
为null
呢?接下来就让我们再看一下holder
是如何创建的。2.2 连接是如何创建的?
接下来就看
holder
是如何创建的。
在
getConnectionInternal
中,holder
的创建是在 for 循环中进行的,一旦创建成功则退出循环。
在创建
holder
时,有同步和异步两种方式。
2.2.1 同步创建连接
如果需要同步创建连接,需要同时满足以下几个条件:
createScheduler != null
poolingCount == 0
activeCount < maxActive
creatingCountUpdater.get(this) == 0
满足上述条件后,
createDirect
会被设置为true
,然后在下一次循环中就会同步创建连接。也正因为有上述条件在,所以只有很少的线程会进入到同步创建连接的流程中。2.2.2 异步创建连接
如果没有同步创建连接,则接下来会通过
pollLast(nanos)
或takeLast()
异步创建连接。nanos
是根据maxWait
计算出来的,如果设置了maxWait
就会调用pollLast(nanos)
,否则调用takeLast()
。2.2.3
pollLast
和takeLast
在
pollLast
方法中,如果当前连接池中的连接数 poolingCount 为 0,则调用emptySignal
使用信号量empty
,并创建异步线程去创建连接。然后,
pollLast
自己再使用notEmpty
信号量进入等待:本次等待的最长时间
estimate
就是根据maxWait
计算出来的。在有连接被回收,会新创建出来后,就会被唤醒。如果唤醒后本线程没有成功创建连接,并且目前总的等待时间还没到
maxWait
,就会再次进入 await。如果等待时间到了
maxWait
,但依旧没有成功创建连接,即 poolingCount 为 0,则会返回 null。通过信号创建的连接会被放入到连接池 connections 中,然后
pollLast(nanos)
收到创建成功的信号后,就可以从连接池中取出连接并返回了:
无参的
takeLast()
就简单很多了,它的流程和pollLast(nanos)
几乎一致,唯一不同的是它没有等待超时时间,只有等待回收线程,会创建线程唤醒后,才会继续执行。2.2.4 emptySignal
使用信号异步创建连接的
emptySignal()
方法代码如下:
如果设置的创建线程池 createScheduler 为 null,则会发送一个信号去创建连接。否则创建一个
CreateConnectionTask
来创建连接。这里发出的创建信号会被
CreateConnectionThread
接收,CreateConnectionThread
会在数据源初始化的时候创建。
收到信号后,会判断当前活跃连接数以及连接池中的连接数是否大于 maxActive,如果大于则不创建,避免创建出超过 maxActive 数量的连接;如果小于,则创建了物理连接,然后在调用 DruidDataSource 的 put 方法将连接放入连接池中。
CreateConnectionTask
也是类似的逻辑。2.2.5 DruidDataSource#put
在 put 方法中,首先会判断当前连接数是否大于最大连接数,如果小于,则会将连接放入到
connections
中,然后将 poolingCount 的值加一。2.2.6 小结
以上便是便是大致的连接创建流程。
总的来说就是,应用需要执行 SQL 的时候,就通过
getConnectionInternal
获取连接,其中创建连接分为同步和异步,异步则通过信号量创建连接。成功获取连接后还会将activeCount
加一。结合 1.2.1 中的异常信息,报错时
poolingCount
为 0, 所以会调用emptySignal()
创建连接。但activeCount + poolingCount + createTaskCount
显然是大于maxActive
的,因此pollLast
最终会等待maxWait
然后超时。所以最终getConnectionInternal
抛出了GetConnectionTimeoutException
异常。所以接下来问题的关键就是,为什么 activeCount 会大于 maxActive?
2.3 连接是如何释放的?
要解答这个问题,我们就需要先知道 activeCount 什么时候减少。
前面已经知道了 activeCount 会在获取到连接后增加,那相应的,activeCount 也会在释放连接时减少。所以接下来就详细了解连接时如何释放的。
2.3.1 应用主动关闭连接
提到连接释放,首先想到的应该是我们在应用中主动调用
connection.close()
方法来释放连接。那
connection.close()
会立即释放物理连接吗?显然是不会的。
connection.close()
会最终会调用dataSource.recycle(DruidPooledConnection pooledConnection)
释放连接。 那dataSource.recycle
又是怎么实现的呢?
2.3.2 recycle
在 recyle 中会有一些列的连接状态判断,如物理连接是否已关闭等,然后决定是否立即释放连接。不过大部分情况下会运行到下面的逻辑,即如果连接状态是活跃的,则将 activeCount 减一,并将状态再设置为 false。最后再调用
putLast
。在 putLast 中则会更新连接的上次活跃时间,然后将连接再度放回到连接池中,并将
poolingcount
加一。也就是说,通过
.close
方法主动释放连接,不会真的释放,而是将连接还回到连接池中。
2.3.3 连接释放
既然前面的
recycle
不会关闭连接,但连接池也不可能无限大,那连接到底是如何回收的呢?这就要提到 Druid 的连接释放任务了。
通
CreateConnectionThread
一样, Druid 在初始化数据源时会创建释放连接的线程DestroyThread
,然后创建释放任务 DestroyTask。
当应用设置的释放线程池 destroyScheduler 不为空时,就会在 destroyScheduler 中运行 DestroyTask;否则会创建一个消费连接的线程池,然后在该线程池中运行释放连接的任务。与 createScheduler 类似,大部分情况下我们不会设置 destroyScheduler。
DestroyConnectionThread 本质上就是一个死循环,在循环中每隔
timeBetweenEvictionRunsMillis
执行一次释放连接的任务。
释放连接的关键逻辑就在 DestroyTask 中。
2.3.4 DestroyTask
在 DestroyTask 中,主要是通过
shrink
方法来释放连接。所以接下来的关键就是
shrink(boolean checkTime, boolean keepAlive)
� 方法。2.3.5 DestroyTask#shrink
shrink
接收两个参数:checkTime
是否校验连接的时间,如连接空闲时间等;keepAlive
是否开启了 keepAlive
在
shrink
中,首先会通过 for 循环,依次遍历连接池中的所有连接并进行处理。
在循环中,首先会判断当前是否有 fatalError,如果有,则会将连接放入 keepAliveConnections 列表中,keepAliveConnections 存储了需要保持活跃的连接。什么时候会出现 fatalError 呢?主要是执行 SQL 时可能出现,比如数据库断网、数据库重启等。
如果没有 fatalError 并且开启了
checkTime
为 true,则会继续根据连接时间判断连接应该被放入 keepAliveConnections 还是 evictConnections 队列。
具体逻辑如下:
-1
;i < checkCount
,也就是如果当前连接池数量小于 minIdle,则连接池前面的连接不会被释放,而是从连接池后面的连接开始释放;正常情况下,connections 中的连接都是按 lastActiveTimeMillis 先后顺序排列的,因此释放连接时,从前往后释放,一旦发现连接空闲时间小于 minEvictableIdleTimeMillis 和 keepAliveBetweenTimeMillis 就结束本次释放流程,这样就可以减少循环次数,提高性能。并且连接保活时,也是从前往后进行保活,因为前面的连接更可能超时。这也正是 Druid 设计的巧妙之处。
当然,这里是“正常情况”,那必然也会有异常情况,异常情况是什么样呢?这里暂时先不展开,后面会详细讲解。
在确定了需要释放和保持活跃的连接数量之后,下一步就是从连接池中将连接删除,删除的方式就是将 connections 中前 removeCount 个连接删掉,然后将剩余连接再依次从前往后放入到 connections,最后再用 null 填充 connections,以便后续存储保活的连接。
接下来对于 evictConnections 中的连接,就可以直接关闭了。
对于 keepAliveConnections 中的连接,则先判断连接是否可用,如果连接可用,则调用
put
方法将连接再放回连接池。和前面应用主动释放连接时调用的putLast
不同的是,put
不会更新连接的 lastActiveTimeMillis 。
如果连接不可用,则�再调用
emptySignal()
通过信号创建一个新的连接。进行连接保活时,优先将时间更早的连接进行保活。但有时候我们可能期望一个连接不要太长时间空闲,比如连接空闲 1 分钟后,即使连接可用也要释放,这时应该怎么办呢?
Druid 也考虑到了这个问题,可通过设置
testWhileIdle
为 true 进行空闲连接释放。2.3.6 空闲连接释放
在应用中我们会通过
getConnection
获取连接,getConnection
会调用getConnectionDirect
,getConnectionDirect
继续调用getConnectionInternal
。在
getConnectionDirect
中,获取到连接后,如果开启了testWhileIdle
,则会继续判断连接是否空闲,如果是,则会调用discardConnection�
释放连接,然后再进行下一次循环创建新的连接。连接是否空闲,主要根据lastActiveTimeMillis
和timeBetweenEvictionRunsMillis
进行判断。
在 discardConnection 中,如果连接 active 状态为 true,则会将 activeCount 减一,并将 active 设置为 false。
2.3.7 小结
以上便是便是连接释放的流程。
当应用主动调用
.close
时,并不会真的是否连接,而是更新连接的 lastActiveTimeMillis,然后将连接返回到连接池,然后将activeCount
减一。然后 DestroyTask 再定期清理连接,判断连接应该被释放还是被保活。如果连接应该被保活,则再将连接放回到连接池。
此外应用获取连接时,也会判断连接的空闲时间,如果连接是空闲的,也会释放连接,并将
activeCount
减一。
那为什么
activeCount
会大于maxActive
呢?可能唯一的解释就是activeCount
不准确。
这里先说结论,的确如此。这是 Druid 1.2.8 之前版本的一个 BUG。
通过前面的分析,我们可以大概推测:当连接池中存在两个相同连接时,
activeCount
就会少减一。因为当应用第一次释放连接时,会将连接的active
设置为 false 并执行activeCount--
,第二次释放时,由于active
已经为 false 了,因此就不会再执行activeCount--
了。
那么如何证明推测的正确性呢?
3. 问题复现
为了证明前面的推测,我写了一段代码来验证。
3.1 复现代码
代码如下,Druid 版本为 1.2.5 :
最终会输出
true
,也就是connection3
和connection4
获取到的连接是一样的。
为什么为 true 呢?接下来就结合前面的源码理解,梳理一下整个时间线。
3.2 时间线
3.2.1 时间点 00:初始化并关闭 connection2
获取 connection1 和 connection2,其 holder 如下:
{id: 110, useCount: 1, lastActiveTime: 00, active: true}
{id: 100, useCount: 1, lastActiveTime: 00, active: true}
然后关闭 connection2,此时 connections 为
[connection2]
:{id: 100, useCount: 1, lastActiveTime: 00, active: false}
最终 activeAcount 为 1。
3.2.2 时间点 07:第一次回收
DestroyTask 第一次执行,此时 connection2 的空闲时间均小于
minEvictableIdleTimeMillis
和keepAliveBetweenTimeMillis
,跳过回收。
3.2.3 时间点 09:关闭 connection1
关闭 connection1,connection1 返回连接池,此时 connections 为
[connection2,connection1]
,connections 中的连接空闲时间是递减的:{id: 100, useCount: 1, lastActiveTime: 00, active: false}
{id: 110, useCount: 1, lastActiveTime: 09, active: false}
最终 activeAcount 为 0。
3.2.4 时间点 14:第二次回收
DestroyTask 执行,此时
connection2
空闲时间大于minEvictableIdleTimeMillis
,但 checkCount 为 0(poolingCount - minIdle�),不满足i < checkCount
,connection2
会被放入 keepAliveConnections 列表。
而
connection1
的空闲时间均小于minEvictableIdleTimeMillis
和keepAliveBetweenTimeMillis
,不处理。
最终 connections 为
[connection1,connection2]
:{id: 110, useCount: 1, lastActiveTime: 09, active: false}
{id: 100, useCount: 1, lastActiveTime: 00, lastKeepTime: 14, active: false}
此时 activeAcount 依旧为 0。但 connections 中的 lastActiveTime 就不是按时间顺序排列的了。
3.2.5 时间点 21:第三次回收
DestroyTask 再次执行,此时 connection1 空闲时间为 11s,由于不满足条件,即不放入 evictConnections, 也不放入 keepAliveConnections,connection2 空闲时间为 21s,大于 keepAliveBetweenTimeMillis,因此放入 keepAliveConnections。
由于 removeCount 为 1( removeCount = evictCount + keepAliveCount),所以接下来会删掉 connections 的第 0 个元素,也就是 connection1,然后将 keepAliveConnections 添加到 connections 中。
最终 connections 为
[connection2,connection2]
:
{id: 100, useCount: 1, lastActiveTime: 00, lastKeepTime: 14, active: false}
{id: 100, useCount: 1, lastActiveTime: 00, lastKeepTime: 14, active: false}
3.2.6 时间点 23:获取 connection3
这时继续获取 conenction3,优先从 connections 末尾获取。
获取到
id
为 100 的连接后,继续判断连接的空闲时间。由于 lastActiveTime 小于 lastKeepTime,所以会用 lastKeepTime 来计算空闲时间,空闲时间为 9,小于 timeBetweenEvictionRunsMillis,因此认为连接可用,并返回。
所以 conenction3 为
{id: 100, useCount: 2, lastActiveTime: 00, lastKeepTime: 14, active: true}
同时也会 activeCount 加一,最终 activeCount 为 1。
此时 connections 为:
{id: 100, useCount: 1, lastActiveTime: 00, lastKeepTime: 14, active: false}
3.2.7 时间点 23:获取 connection4
同理,获取到 conenction4 为
{id: 100, useCount: 3, lastActiveTime: 00, lastKeepTime: 14, active: true}
,并且也会将 activeCount 加一,最终 activeCount 为 2。所以 connection3 和 connection4 本质上是同一个 holder。
另外,此时 connections 为空。
3.2.8 时间点 23:关闭 connection3
紧接着调用 close 方法关闭 connection3,将连接返回连接池,同时将 activeCount 减一。执行完毕后connections 为
[connections3]
:{id: 100, useCount: 3, lastActiveTime: 23, lastKeepTime: 14, active: false}
最终 activeCount 为 1,并且 holder 的 active 属性为 false。
3.2.9 时间点 23:关闭 connection4
在关闭 connection3 之后,立即又关闭 connection4, 但由于其 holder 的 active 已经是 false 了,所以 activeCount 不会再减一了。
最终虽然连接会被释放,但 activeCount 少减了一次。
3.3 小结
由此可见,在某些特定条件下,由于 keepAlive 机制会将连接再放回连接池,可能会导致 connections 中会存在多个完全相同的连接 holder,并且该 holder 又可能刚好被多个应用线程获取到了,然后关闭连接时,由于最早关闭的线程已经将 holder 的 active 标记为 false 了,所以其他线程关闭连接时,就不会再把 activeCount 减一。最终导致了 activeCount 不准确。
那什么时候会触发这个 BUG 呢?这个 BUG 是个小概率事件,以下几种情况会提升 BUG 出现的概率:
4. 问题修复
那这个问题如何修复呢?只要保证 connections 中不能存在两个完全相同的 holder 就可以了。由于问题是有 keepAlive 机制在放回连接时触发的,所以可以在此加个判断,如果连接已经存在连接池中,就不放入。
官方在 1.2.8 这个版本也修复了该问题,详细修复代码参考 druid/compare/1.2.5...1.2.8 。
因此我们应用中的终极修复方式就是,将 Druid 升级到 1.2.8。
5. 总结
通过对源码的一层层深入分析,终于弄清楚了为什么会应用报错后会一直挂起,为什么 activeCount 不准确,以及如何解决。
表面上看是获取连接超时,实际上时 activeCount 计算不准确导致无法创建出新的连接。
导致 activeCount 计算不准确的原因是连接池中存在多个相同的连接,因此连接释放时,多次释放相同连接只会执行一次 activeCount--。
之所以连接池可能存在多个相同连接,主要是数据库异常或 Druid 参数配置有误时,刚好导致 keepAlive 将连接重复放入连接池。并且再次获取该连接时,刚好连接空闲时间判断错误,导致连接不被释放而是返回给了应用使用。
本质上是 Druid 1.2.8 之前的一个 BUG,数据库异常以及配置导致的空闲时间判断错误等种种巧合,触发了这个 BUG。但出现问题后通过重启又能解决问题,所以很难定位。
最后,将 Druid 升级到 1.2.8 吧,这样才能彻底解决问题。另外也需要注意以下 Druid 配置:
至此,本文结束。感谢阅读!
参考
The text was updated successfully, but these errors were encountered: