背景

最近碰到一个 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)。

下面,我们聊聊泄漏连接的相关问题,包括:

  1. 泄漏连接的危害。
  2. 泄漏连接的产生原因。
  3. Druid 中如何定位泄漏连接。
  4. HikariCP 中如何定位泄漏连接。

泄漏连接的危害

泄漏连接可能引发以下问题:

  1. 连接池耗尽:泄漏的连接会持续占用连接池中的资源,导致可用连接逐渐减少,最终耗尽连接池。

  2. 应用性能下降:当连接池中的连接被耗尽时,新的数据库操作无法获取连接,导致请求阻塞或失败,这可能导致应用程序无法正常运行。

  3. 数据库资源浪费:泄漏的连接会占用数据库的连接资源,可能导致数据库的连接数达到上限。

  4. 连接失效风险:长时间未释放的连接无法通过连接池的 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;
}

该方法的处理流程如下:

  1. 遍历当前活跃连接(activeConnections),检查每个连接的使用时间。连接的使用时间等于当前时间减去连接的借出时间(即borrow时刻的时间戳)。
  2. 如果某个连接的使用时间超过了removeAbandonedTimeoutMillis,则将其加入 abandonedList。
  3. 遍历 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,打印连接泄漏信息。

在开发和测试环境中,建议开启连接泄漏检测功能,以便尽早发现问题并进行修复。

如何定位 Druid & HikariCP 连接池的连接泄漏问题?的更多相关文章

  1. HttpClient连接池的连接保持、超时和失效机制

    HTTP是一种无连接的事务协议,底层使用的还是TCP,连接池复用的就是TCP连接,目的就是在一个TCP连接上进行多次的HTTP请求从而提高性能.每次HTTP请求结束的时候,HttpClient会判断连 ...

  2. commons-pool与commons-pool2连接池(Hadoop连接池)

    commons-pool和commons-pool2是用来建立对象池的框架,提供了一些将对象池化必须要实现的接口和一些默认动作.对象池化之后可以通过pool的概念去管理其生命周期,例如对象的创建,使用 ...

  3. JDBC连接池-自定义连接池

    JDBC连接池 java JDBC连接中用到Connection   在每次对数据进行增删查改 都要 开启  .关闭  ,在实例开发项目中 ,浪费了很大的资源 ,以下是之前连接JDBC的案例 pack ...

  4. 同过增强Connection类[重写了close的方法]实现的从连接池取出连接并放回连接的简单的实现流程

    package tk.dong.connection.util; import java.io.IOException;import java.io.InputStream;import java.i ...

  5. 通常每个套接字地址(协议/网络地址/端口)只允许使用一次。 数据库连接不释放测试 连接池 释放连接 关闭连接 有关 redis-py 连接池会导致服务器产生大量 CLOSE_WAIT 的再讨论以及一个解决方案

    import pymysqlfrom redis import Redisimport time h, pt, u, p, db = '192.168.2.210', 3306, 'root', 'n ...

  6. JDBC连接池-C3P0连接

    JDBC连接池-C3P0连接 c3p0连接池的学习英语好的看英文原版      c3p0 - JDBC3 Connection and Statement Pooling 使用c3p0连接池  三种方 ...

  7. java原生程序redis连接(连接池/长连接和短连接)选择问题

    最近遇到的连接问题我准备从重构的几个程序(redis和mysql)长连接和短连接,以及连接池和单连接等问题用几篇博客来总结下. 这个问题的具体发生在java原生程序和redis的交互中.这个问题对我最 ...

  8. psycopg2.pool – Connections pooling / psycopg2.pool – 连接池 / postgresql 连接池

    创建新的PostgreSQL连接可以是一个昂贵的操作.这个模块提供了一些纯Python类直接在客户端应用程序实现简单的连接池.      class psycopg2.pool.AbstractCon ...

  9. 使用DBCP连接池对连接进行管理

    //需要引用的jar包有4个,分别是commons-pool2-2.4.2.jar.commons-dbcp2-2.1.1.jar.mysql-connector-java-5.1.42-bin.ja ...

  10. 网络协议 finally{ return问题 注入问题 jdbc注册驱动问题 PreparedStatement 连接池目的 1.2.1DBCP连接池 C3P0连接池 MYSQL两种方式进行实物管理 JDBC事务 DBUtils事务 ThreadLocal 事务特性 并发访问 隔离级别

    1.1.1 API详解:注册驱动 DriverManager.registerDriver(new com.mysql.jdbc.Driver());不建议使用 原因有2个: >导致驱动被注册2 ...

随机推荐

  1. .NET Core:架构、特性和优势详解

    .NET Core:架构.特性和优势详解 在软件开发领域,保持领先地位至关重要.随着技术以指数级的速度发展,开发人员不断寻求高效.可扩展且多功能的解决方案来应对现代挑战..NET Core 就是这样一 ...

  2. Mac上安装mongoDB详细教程

    Mac OSX 平台安装 MongoDB MongoDB 提供了 OSX 平台上 64 位的安装包,你可以在官网下载安装包. 下载地址:https://www.mongodb.com/download ...

  3. Tomcat启用manager管理

    1.修改远程访问限制: 修改webapps/manager/META-INF/context.xml和webapps/host-manager/META-INF/context.xml,在<Co ...

  4. Vanity Intermediate 统配符提权

    nmap扫描 ┌──(root㉿kali)-[~] └─# nmap -p- -A 192.168.167.234 Starting Nmap 7.94SVN ( https://nmap.org ) ...

  5. C# Winform 实现静态变量属性的值变了,触发事件,类似WPF的双向绑定

    在C# WinForms中,虽然没有像WPF那样内置的双向绑定机制,但你可以通过事件和属性封装来实现类似的功能.具体来说,你可以在静态属性的set访问器中触发一个自定义事件,然后在需要的地方订阅这个事 ...

  6. DeepSeek-R1 技术全景解析:从原理到实践的“炼金术配方” ——附多阶段训练流程图与核心误区澄清

    字数:约3200字|预计阅读时间:8分钟 (调试着R1的API接口,看着控制台瀑布般流淌的思维链日志)此刻我仿佛看到AlphaGo的棋谱在代码世界重生--这是属于推理模型的AlphaZero时刻. D ...

  7. 告别 DeepSeek 系统繁忙,七个 DeepSeek 曲线救国平替入口,官网崩溃也能用!

    前言 DeepSeek作为一款备受瞩目的国产大模型,以其强大的功能和卓越的性能赢得了众多用户的青睐.然而,随着用户量的激增,DeepSeek官网近期频繁遭遇服务器繁忙甚至崩溃的问题,给广大用户带来了不 ...

  8. ABB喷涂机器人控制柜维护保养

    ABB喷涂机器人的管理与维护保养目的是减少机器人的故障率和停机时间,充分利用机器人这一生产要素,最大限度地提高产效率.喷涂机器人维修与保养在企业生产中尤为重要,直接影响到系统的寿命,必须精心维护. A ...

  9. Java8 stream 提取对象 List 中的某一字段生成新的 List

    //输出List StudentInfo.printStudents(studentList); //从对象列表中提取一列(以name为例) List<String> nameList = ...

  10. SpringBoot 2.x 接入非标准SSE格式大模型流式响应实践 🚀

    近期DeepSeek等国产大模型热度持续攀升,其关注度甚至超过了OpenAI(被戏称为CloseAI).在SpringBoot3.x环境中,可以使用官方的Spring AI轻松接入,但对于仍在使用JD ...