马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
背景
最近碰到一个 case,一个 Java 应用无法获取新的数据库连接,日志中出现了以下错误:- com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 5001, active 20, maxActive 20, creating 0
- at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1894)
- at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1502)
- at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1482)
- at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1463)
复制代码 active 等于 maxActive,说明连接池中的连接已耗尽。
分析报错时间段的数据库连接情况,发现数据库的连接数(Threads_connected)显著增加,但活跃线程数(Threads_running)较低且平稳。活跃线程数低且平稳意味着没有慢查询占用连接。但连接数增加明显,说明连接未被及时释放回连接池。
对于这种在肯定时间内没有进行任何利用,但又未及时归还到连接池的连接,其实有个专用名词,即泄漏连接(Leaked Connection)。
下面,我们聊聊泄漏连接的相干题目,包括:
- 泄漏连接的危害。
- 泄漏连接的产生缘故原由。
- Druid 中如何定位泄漏连接。
- HikariCP 中如何定位泄漏连接。
泄漏连接的危害
泄漏连接大概引发以下题目:
- 连接池耗尽:泄漏的连接会连续占用连接池中的资源,导致可用连接渐渐减少,最终耗尽连接池。
- 应用性能下降:当连接池中的连接被耗尽时,新的数据库利用无法获取连接,导致哀求阻塞或失败,这大概导致应用程序无法正常运行。
- 数据库资源浪费:泄漏的连接会占用数据库的连接资源,大概导致数据库的连接数达到上限。
- 连接失效风险:长时间未释放的连接无法通过连接池的 Keep-Alive 机制保持活跃,更容易因空闲超时被 MySQL 服务端或中间件关闭。
当使用这些已关闭的连接执行数据库利用时,会触发经典的 “Communications link failure. The last packet successfully received from the server was xxx milliseconds ago.” 错误。
泄漏连接的产生缘故原由
泄漏连接通常由以下缘故原由导致:
1. 长变乱或长连接。
变乱长时间未提交或连接长时间未释放。
2. 未关闭连接。
在使用完连接后,未调用close()方法将连接归还到连接池。如,- Connection conn = dataSource.getConnection();
- // 执行数据库操作
- // 忘记调用 conn.close();
复制代码 3. 非常未处理。
在数据库利用过程中发生非常,导致连接未正常关闭。如,- Connection conn = null;
- try {
- conn = dataSource.getConnection();
- // 执行数据库操作
- thrownew RuntimeException("模拟异常");
- } catch (SQLException e) {
- e.printStackTrace();
- } finally {
- if (conn != null) {
- try {
- conn.close(); // 异常发生后,可能不会执行到此处
- } catch (SQLException e) {
- e.printStackTrace();
- }
- }
- }
复制代码 Druid 中如何定位泄漏连接
在 Druid 连接池中,可以通过以下参数开启未归还连接的检测:
- removeAbandoned:是否接纳超时未归还的连接,默认值为 false,表现不接纳。
- removeAbandonedTimeoutMillis:未归还连接的超时时间(单位:毫秒)。默认值为 300000(即 300 秒)。
- logAbandoned:是否将超时未归还的连接信息打印到日志中。默认值为 false,表现不打印。
需要留意的是,logAbandoned 仅在 removeAbandoned 为 true 时生效。也就是说,Druid 连接池不支持仅打印,但不接纳超时未归还连接的功能。
实现细节
在从连接池获取连接时,如果removeAbandoned为 true,则会记录连接的堆栈信息和创建时间,用于检测未归还连接。- public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
- ...
- for (; ; ) {
- DruidPooledConnection poolableConnection;
- try {
- poolableConnection = getConnectionInternal(maxWaitMillis);
- } catch (GetConnectionTimeoutException ex) {
- ...
- }
- ...
- if (removeAbandoned) {
- // 记录堆栈信息,方便调试,找出未及时关闭连接的代码位置
- StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
- poolableConnection.connectStackTrace = stackTrace;
- // 设置连接的connectedTimeNano为当前时间
- poolableConnection.setConnectedTimeNano();
- poolableConnection.traceEnable = true;
- // 将连接加入活跃连接列表,用于后续的未归还连接检测。
- activeConnectionLock.lock();
- try {
- activeConnections.put(poolableConnection, PRESENT);
- } finally {
- activeConnectionLock.unlock();
- }
- }
- ...
- return poolableConnection;
- }
- }
复制代码 什么时间会检测连接是否超时呢?
这个实际上是在DestroyConnectionThread的周期使命中进行的,在上一篇文章中,我们提到过DestroyConnectionThread按照肯定的时间间隔(由 timeBetweenEvictionRunsMillis 参数决定,默以为 60秒)调用shrink(true, keepAlive)方法,销毁连接池中的过期连接。其实,除了 shrink 方法,它还会调用removeAbandoned()来关闭那些超时未归还的连接。- public class DestroyTask implements Runnable {
- public DestroyTask() {
- }
- @Override
- public void run() {
- shrink(true, keepAlive);
- if (isRemoveAbandoned()) {
- removeAbandoned();
- }
- }
- }
复制代码 下面,我们看看removeAbandoned()具体的实现细节。- public int removeAbandoned() {
- int removeCount = 0;
- // 如果当前没有活跃连接(activeConnections 为空),则直接返回
- if (activeConnections.size() == 0) {
- return removeCount;
- }
- long currrentNanos = System.nanoTime();
- List<DruidPooledConnection> abandonedList = new ArrayList<DruidPooledConnection>();
- activeConnectionLock.lock();
- try {
- Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator();
- // 遍历活跃连接
- for (; iter.hasNext(); ) {
- DruidPooledConnection pooledConnection = iter.next();
- // 如果连接正在运行(isRunning()),则跳过
- if (pooledConnection.isRunning()) {
- continue;
- }
- // 计算连接的使用时间(timeMillis),即当前时间减去连接的借出时间。
- long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);
- // 如果连接的使用时间超过了 removeAbandonedTimeoutMillis,则将其从活跃连接列表中移除,并加入 abandonedList
- if (timeMillis >= removeAbandonedTimeoutMillis) {
- iter.remove();
- pooledConnection.setTraceEnable(false);
- abandonedList.add(pooledConnection);
- }
- }
- } finally {
- activeConnectionLock.unlock();
- }
- // 遍历 abandonedList,对每个未归还的连接调用 JdbcUtils.close() 关闭连接
- if (abandonedList.size() > 0) {
- for (DruidPooledConnection pooledConnection : abandonedList) {
- ...
- JdbcUtils.close(pooledConnection);
- pooledConnection.abandond();
- removeAbandonedCount++;
- removeCount++;
- // 如果 logAbandoned 为 true,则记录未归还连接的详细信息
- if (isLogAbandoned()) {
- StringBuilder buf = new StringBuilder();
- buf.append("abandon connection, owner thread: ");
- buf.append(pooledConnection.getOwnerThread().getName());
- buf.append(", connected at : ");
- ...
- }
- LOG.error(buf.toString());
- }
- }
- }
- return removeCount;
- }
复制代码 该方法的处理流程如下:
- 遍历当前活跃连接(activeConnections),查抄每个连接的使用时间。连接的使用时间等于当前时间减去连接的借出时间(即borrow时刻的时间戳)。
- 如果某个连接的使用时间超过了removeAbandonedTimeoutMillis,则将其加入 abandonedList。
- 遍历 abandonedList,关闭这些未归还的连接。如果logAbandoned为 true,则会在日志中打印未归还连接的详细信息。通过分析日志,可以定位泄漏连接的代码位置。
HikariCP 中如何定位泄漏连接
在 HikariCP 连接池中,可以通过以下参数开启连接泄漏检测:
- leakDetectionThreshold:连接泄漏检测阈值(单位:毫秒)。如果一个连接在从连接池获取后超过指定时间未被关闭,则以为是泄漏连接。默以为 0,表现禁用连接泄漏检测。最小可设置为 2000(2 秒)。
当出现泄漏连接时,HikariCP 日志中会打印以下信息- Connection leak detection triggered for com.mysql.cj.jdbc.ConnectionImpl@5dd31d98 on thread com.example.HikariCPTest.main(), stack trace follows
- java.lang.Exception: Apparent connection leak detected
- at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
- at com.example.HikariCPTest.main(HikariCPTest.java:27)
- ...
复制代码 实现细节
在从连接池获取连接后,系统会调用leakTaskFactory.schedule(poolEntry)启动一个 ProxyLeakTask 定时使命。该使命将在leakDetectionThreshold毫秒后触发run()方法,用于检测并打印连接泄漏信息。- public Connection getConnection(final long hardTimeout) throws SQLException
- {
- suspendResumeLock.acquire();
- finalvar startTime = currentTime();
- try {
- var timeout = hardTimeout;
- do {
- // 从连接池中获取空闲连接
- var poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
- if (poolEntry == null) {
- break; // We timed out... break and throw exception
- }
- finalvar now = currentTime();
- // 若连接已被标记为驱逐 (evict) 或检测到无效,则关闭该连接
- if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && isConnectionDead(poolEntry.connection))) {
- closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
- timeout = hardTimeout - elapsedMillis(startTime);
- }
- else {
- ...
- // 返回一个代理连接,并启动连接泄漏检测任务
- return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry));
- }
- } while (timeout > 0L);
- ...
- }
复制代码 如果连接在leakDetectionThreshold时间内被归还(即调用了close()方法),系统会调用leakTask.cancel()取消定时使命,从而避免触发run()方法。
如果连接超时未归还,系统将执行 run() 方法,打印连接泄漏信息。
以下是 ProxyLeakTask 的具体实现。- class ProxyLeakTask implements Runnable
- {
- ...
- ProxyLeakTask(final PoolEntry poolEntry)
- {
- this.exception = new Exception("Apparent connection leak detected");
- this.threadName = Thread.currentThread().getName();
- this.connectionName = poolEntry.connection.toString();
- }
- ...
- void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold)
- {
- scheduledFuture = executorService.schedule(this, leakDetectionThreshold, TimeUnit.MILLISECONDS);
- }
- /** {@inheritDoc} */
- @Override
- public void run()
- {
- isLeaked = true;
- finalvar stackTrace = exception.getStackTrace();
- finalvar trace = new StackTraceElement[stackTrace.length - 5];
- System.arraycopy(stackTrace, 5, trace, 0, trace.length);
- exception.setStackTrace(trace);
- LOGGER.warn("Connection leak detection triggered for {} on thread {}, stack trace follows", connectionName, threadName, exception);
- }
- void cancel()
- {
- scheduledFuture.cancel(false);
- if (isLeaked) {
- LOGGER.info("Previously reported leaked connection {} on thread {} was returned to the pool (unleaked)", connectionName, threadName);
- }
- }
- }
复制代码 总结
泄漏连接是指在使用完数据库连接后未及时归还连接池的连接。泄漏连接的主要危害包括连接池耗尽、应用性能下降、数据库资源浪费以及潜在的连接失效风险。泄漏连接的产生缘故原由通常包括未精确关闭连接、未处理非常或长变乱等。
Druid 和 HikariCP 两大常用连接池提供了相应的泄漏连接检测机制。Druid 通过DestroyConnectionThread周期性检测未归还的连接,并在超时后关闭这些连接。如果logAbandoned为 true,还会打印未归还连接的详细信息。HikariCP 则通过leakDetectionThreshold参数开启连接泄漏检测。当连接在指定时间内未被归还时,HikariCP 会触发ProxyLeakTask,打印连接泄漏信息。
在开发和测试环境中,建议开启连接泄漏检测功能,以便尽早发现题目并进行修复。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |