qidao123.com技术社区-IT企服评测·应用市场

标题: 如何定位 Druid & HikariCP 连接池的连接泄漏题目? [打印本页]

作者: 北冰洋以北    时间: 2025-3-31 08:20
标题: 如何定位 Druid & HikariCP 连接池的连接泄漏题目?
背景

最近碰到一个 case,一个 Java 应用无法获取新的数据库连接,日志中出现了以下错误:
  1. com.alibaba.druid.pool.GetConnectionTimeoutException: wait millis 5001, active 20, maxActive 20, creating 0
  2.         at com.alibaba.druid.pool.DruidDataSource.getConnectionInternal(DruidDataSource.java:1894)
  3.         at com.alibaba.druid.pool.DruidDataSource.getConnectionDirect(DruidDataSource.java:1502)
  4.         at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1482)
  5.         at com.alibaba.druid.pool.DruidDataSource.getConnection(DruidDataSource.java:1463)
复制代码
active 等于 maxActive,说明连接池中的连接已耗尽。
分析报错时间段的数据库连接情况,发现数据库的连接数(Threads_connected)显著增加,但活跃线程数(Threads_running)较低且平稳。活跃线程数低且平稳意味着没有慢查询占用连接。但连接数增加明显,说明连接未被及时释放回连接池。
对于这种在肯定时间内没有进行任何利用,但又未及时归还到连接池的连接,其实有个专用名词,即泄漏连接(Leaked Connection)。
下面,我们聊聊泄漏连接的相干题目,包括:
泄漏连接的危害

泄漏连接大概引发以下题目:
泄漏连接的产生缘故原由

泄漏连接通常由以下缘故原由导致:
1. 长变乱或长连接。
变乱长时间未提交或连接长时间未释放。
2. 未关闭连接。
在使用完连接后,未调用close()方法将连接归还到连接池。如,
  1. Connection conn = dataSource.getConnection();
  2. // 执行数据库操作
  3. // 忘记调用 conn.close();
复制代码
3. 非常未处理。
在数据库利用过程中发生非常,导致连接未正常关闭。如,
  1. Connection conn = null;
  2. try {
  3.     conn = dataSource.getConnection();
  4.     // 执行数据库操作
  5.     thrownew RuntimeException("模拟异常");
  6. } catch (SQLException e) {
  7.     e.printStackTrace();
  8. } finally {
  9.     if (conn != null) {
  10.         try {
  11.             conn.close(); // 异常发生后,可能不会执行到此处
  12.         } catch (SQLException e) {
  13.             e.printStackTrace();
  14.         }
  15.     }
  16. }
复制代码
Druid 中如何定位泄漏连接

在 Druid 连接池中,可以通过以下参数开启未归还连接的检测:
需要留意的是,logAbandoned 仅在 removeAbandoned 为 true 时生效。也就是说,Druid 连接池不支持仅打印,但不接纳超时未归还连接的功能。
实现细节

在从连接池获取连接时,如果removeAbandoned为 true,则会记录连接的堆栈信息和创建时间,用于检测未归还连接。
  1. public DruidPooledConnection getConnectionDirect(long maxWaitMillis) throws SQLException {
  2.         ...
  3.         for (; ; ) {
  4.             DruidPooledConnection poolableConnection;
  5.             try {
  6.                 poolableConnection = getConnectionInternal(maxWaitMillis);
  7.             } catch (GetConnectionTimeoutException ex) {
  8.                 ...
  9.             }
  10.             ...
  11.             if (removeAbandoned) {
  12.                 // 记录堆栈信息,方便调试,找出未及时关闭连接的代码位置
  13.                 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); 
  14.                 poolableConnection.connectStackTrace = stackTrace;
  15.                 // 设置连接的connectedTimeNano为当前时间
  16.                 poolableConnection.setConnectedTimeNano();
  17.                 poolableConnection.traceEnable = true;
  18.                 // 将连接加入活跃连接列表,用于后续的未归还连接检测。
  19.                 activeConnectionLock.lock();
  20.                 try {
  21.                     activeConnections.put(poolableConnection, PRESENT);
  22.                 } finally {
  23.                     activeConnectionLock.unlock();
  24.                 }
  25.             }
  26.             ...
  27.             return poolableConnection;
  28.         }
  29.     }
复制代码
什么时间会检测连接是否超时呢?

这个实际上是在DestroyConnectionThread的周期使命中进行的,在上一篇文章中,我们提到过DestroyConnectionThread按照肯定的时间间隔(由 timeBetweenEvictionRunsMillis 参数决定,默以为 60秒)调用shrink(true, keepAlive)方法,销毁连接池中的过期连接。其实,除了 shrink 方法,它还会调用removeAbandoned()来关闭那些超时未归还的连接。
  1. public class DestroyTask implements Runnable {
  2.     public DestroyTask() {
  3.     }

  4.     @Override
  5.     public void run() {
  6.         shrink(true, keepAlive);

  7.         if (isRemoveAbandoned()) {
  8.             removeAbandoned();
  9.         }
  10.     }

  11. }
复制代码
下面,我们看看removeAbandoned()具体的实现细节。
  1. public int removeAbandoned() {
  2.     int removeCount = 0;
  3.     // 如果当前没有活跃连接(activeConnections 为空),则直接返回
  4.     if (activeConnections.size() == 0) {
  5.         return removeCount;
  6.     }

  7.     long currrentNanos = System.nanoTime();

  8.     List<DruidPooledConnection> abandonedList = new ArrayList<DruidPooledConnection>();

  9.     activeConnectionLock.lock();
  10.     try {
  11.         Iterator<DruidPooledConnection> iter = activeConnections.keySet().iterator();
  12.         // 遍历活跃连接
  13.         for (; iter.hasNext(); ) {
  14.             DruidPooledConnection pooledConnection = iter.next();
  15.             // 如果连接正在运行(isRunning()),则跳过
  16.             if (pooledConnection.isRunning()) {
  17.                 continue;
  18.             }
  19.             // 计算连接的使用时间(timeMillis),即当前时间减去连接的借出时间。
  20.             long timeMillis = (currrentNanos - pooledConnection.getConnectedTimeNano()) / (1000 * 1000);
  21.             // 如果连接的使用时间超过了 removeAbandonedTimeoutMillis,则将其从活跃连接列表中移除,并加入 abandonedList
  22.             if (timeMillis >= removeAbandonedTimeoutMillis) {
  23.                 iter.remove();
  24.                 pooledConnection.setTraceEnable(false);
  25.                 abandonedList.add(pooledConnection);
  26.             }
  27.         }
  28.     } finally {
  29.         activeConnectionLock.unlock();
  30.     }
  31.     // 遍历 abandonedList,对每个未归还的连接调用 JdbcUtils.close() 关闭连接
  32.     if (abandonedList.size() > 0) {
  33.         for (DruidPooledConnection pooledConnection : abandonedList) {
  34.             ...
  35.             JdbcUtils.close(pooledConnection);
  36.             pooledConnection.abandond();
  37.             removeAbandonedCount++;
  38.             removeCount++;
  39.             // 如果 logAbandoned 为 true,则记录未归还连接的详细信息
  40.             if (isLogAbandoned()) {
  41.                 StringBuilder buf = new StringBuilder();
  42.                 buf.append("abandon connection, owner thread: ");
  43.                 buf.append(pooledConnection.getOwnerThread().getName());
  44.                 buf.append(", connected at : ");
  45.                 ...
  46.                 }

  47.                 LOG.error(buf.toString());
  48.             }
  49.         }
  50.     }

  51.     return removeCount;
  52. }
复制代码
该方法的处理流程如下:
HikariCP 中如何定位泄漏连接

在 HikariCP 连接池中,可以通过以下参数开启连接泄漏检测:
当出现泄漏连接时,HikariCP 日志中会打印以下信息
  1. Connection leak detection triggered for com.mysql.cj.jdbc.ConnectionImpl@5dd31d98 on thread com.example.HikariCPTest.main(), stack trace follows
  2. java.lang.Exception: Apparent connection leak detected
  3.         at com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:100)
  4.         at com.example.HikariCPTest.main(HikariCPTest.java:27)
  5. ...
复制代码
实现细节

在从连接池获取连接后,系统会调用leakTaskFactory.schedule(poolEntry)启动一个 ProxyLeakTask 定时使命。该使命将在leakDetectionThreshold毫秒后触发run()方法,用于检测并打印连接泄漏信息。
  1. public Connection getConnection(final long hardTimeout) throws SQLException
  2.    {
  3.       suspendResumeLock.acquire();
  4.       finalvar startTime = currentTime();
  5.       try {
  6.          var timeout = hardTimeout;
  7.          do {
  8.             // 从连接池中获取空闲连接
  9.             var poolEntry = connectionBag.borrow(timeout, MILLISECONDS);
  10.             if (poolEntry == null) {
  11.                break; // We timed out... break and throw exception
  12.             }
  13.             finalvar now = currentTime();
  14.             // 若连接已被标记为驱逐 (evict) 或检测到无效,则关闭该连接
  15.             if (poolEntry.isMarkedEvicted() || (elapsedMillis(poolEntry.lastAccessed, now) > aliveBypassWindowMs && isConnectionDead(poolEntry.connection))) {
  16.                closeConnection(poolEntry, poolEntry.isMarkedEvicted() ? EVICTED_CONNECTION_MESSAGE : DEAD_CONNECTION_MESSAGE);
  17.                timeout = hardTimeout - elapsedMillis(startTime);
  18.             }
  19.             else {
  20.                ...
  21.                // 返回一个代理连接,并启动连接泄漏检测任务
  22.                return poolEntry.createProxyConnection(leakTaskFactory.schedule(poolEntry));
  23.             }
  24.          } while (timeout > 0L);
  25.      ...
  26.    }
复制代码
如果连接在leakDetectionThreshold时间内被归还(即调用了close()方法),系统会调用leakTask.cancel()取消定时使命,从而避免触发run()方法。
如果连接超时未归还,系统将执行 run() 方法,打印连接泄漏信息。
以下是 ProxyLeakTask 的具体实现。
  1. class ProxyLeakTask implements Runnable
  2. {
  3.   ...
  4.    ProxyLeakTask(final PoolEntry poolEntry)
  5.    {
  6.       this.exception = new Exception("Apparent connection leak detected");
  7.       this.threadName = Thread.currentThread().getName();
  8.       this.connectionName = poolEntry.connection.toString();
  9.    }
  10.    ...
  11.    void schedule(ScheduledExecutorService executorService, long leakDetectionThreshold)
  12.    {
  13.       scheduledFuture = executorService.schedule(this, leakDetectionThreshold, TimeUnit.MILLISECONDS);
  14.    }

  15.    /** {@inheritDoc} */
  16.    @Override
  17.    public void run()
  18.    {
  19.       isLeaked = true;

  20.       finalvar stackTrace = exception.getStackTrace();
  21.       finalvar trace = new StackTraceElement[stackTrace.length - 5];

  22.       System.arraycopy(stackTrace, 5, trace, 0, trace.length);

  23.       exception.setStackTrace(trace);
  24.       LOGGER.warn("Connection leak detection triggered for {} on thread {}, stack trace follows", connectionName, threadName, exception);
  25.    }

  26.    void cancel()
  27.    {
  28.       scheduledFuture.cancel(false);
  29.       if (isLeaked) {
  30.          LOGGER.info("Previously reported leaked connection {} on thread {} was returned to the pool (unleaked)", connectionName, threadName);
  31.       }
  32.    }
  33. }
复制代码
总结

泄漏连接是指在使用完数据库连接后未及时归还连接池的连接。泄漏连接的主要危害包括连接池耗尽、应用性能下降、数据库资源浪费以及潜在的连接失效风险。泄漏连接的产生缘故原由通常包括未精确关闭连接、未处理非常或长变乱等。
Druid 和 HikariCP 两大常用连接池提供了相应的泄漏连接检测机制。Druid 通过DestroyConnectionThread周期性检测未归还的连接,并在超时后关闭这些连接。如果logAbandoned为 true,还会打印未归还连接的详细信息。HikariCP 则通过leakDetectionThreshold参数开启连接泄漏检测。当连接在指定时间内未被归还时,HikariCP 会触发ProxyLeakTask,打印连接泄漏信息。
在开发和测试环境中,建议开启连接泄漏检测功能,以便尽早发现题目并进行修复。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 qidao123.com技术社区-IT企服评测·应用市场 (https://dis.qidao123.com/) Powered by Discuz! X3.4