数据库连接池爆满:连接泄漏三种典型场景与定位

本文是 Spring Boot 生产配置实战 系列的第 2 篇 叙事框架:现象 → 排查过程 → 根因 → 修复 → 预防

问题现象

某日上午 10:05,告警群突然炸了——order-fulfillment 服务的三个 HikariCP 连接池使用率全部突破 90%,活跃连接数 29/30,pending 请求 156 个,超时率 12.3%。

告警群排查全过程

值班小A登录服务器一看,CPU 才 32%,但 load average 已经 18.73。大量进程在等 IO——确切地说,是在等数据库连接。

监控告警:连接池活跃度

值班小A登录服务器一看,CPU 才 32%,但 load average 已经 18.73。大量进程在等 IO——确切地说,是在等数据库连接。

top 系统资源

有意思:CPU 空闲 58%,load 却飙到 18。不是计算密集型,是 IO 阻塞型。

排查过程

第一步:看连接

三个数据源——订单库(:3306)、库存库(:3307)、物流库(:3308)——全部 ESTABLISHED 占满。

netstat 连接状态

每个池 maximumPoolSize=10,三个池一共 30 个连接全被占用。一个不剩。

第二步:看日志

翻应用日志,看到大量连接获取超时异常,以及 HikariCP 的 leak-detection-threshold 触发警告:

应用日志异常

Caused by: java.sql.SQLTransientConnectionException: HikariPool-1 (order-ds) - Connection is not available, request timed out after 10000ms (total=10, active=10, idle=0, waiting=52)

还有 HikariCP 自身的泄漏检测:

WARN  - Connection leak detection triggered for connection 192.168.10.50:3306/order-db
Stack trace follows:
java.lang.Exception: Connection leak detection: connection has been active for at least 15000ms
    at cn.opencao.order.service.OrderService.createOrder(OrderService.java:32)

泄漏检测已经触发了 47 次,分别落在 createOrder(32 次)、updateInventory(12 次)、createLogistics(3 次)三个方法上——正好是三个不同的泄漏场景。

第三步:jstack 看线程栈

用 jstack 看线程分布,三种不同的阻塞模式:

jstack 线程分析

108 个 RUNNABLE 线程中有大量卡在 socketRead0——连接从池中借出后,在 executeQuery 阶段就卡住了,说明连接压根没被归还。

45 个 TIMED_WAITING 线程卡在 Thread.sleep——这是事务内调了外部 HTTP 接口,事务一直不提交,连接一直占着。

15 个 WAITING 线程卡在 HikariPool.getConnection——这是后续请求排队等连接。

第四步:JMX 看运行时指标

通过 Actuator 看每个连接池的实时指标:

HikariCP 运行时状态

order-ds:      active=10/10  pending=52  timeout=4%
inventory-ds:  active=10/10  pending=48  timeout=5%
logistics-ds:  active=9/10   pending=56  timeout=3%

三个池全部爆满,156 个线程在排队。三处泄漏同时发作,形成叠加效应。

根因分析

泄漏场景一:未关闭 JDBC 资源(createOrder)

泄漏1: 未关闭 JDBC 资源

public void createOrder() {
    Connection conn = null;
    Statement stmt = null;
    ResultSet rs = null;
    try {
        conn = dataSource.getConnection();
        stmt = conn.createStatement();
        rs = stmt.executeQuery(query);
        while (rs.next()) {
            // 业务处理
        }
    } catch (SQLException e) {
        throw new RuntimeException("订单创建失败", e);
    }
    // ❌ 没有 finally 块关闭连接!
    // 查询异常 → 直接抛 → 连接永远不归还
}

最原始的写法——手动管理 JDBC 连接但忘了 close。当 executeQuerynext() 抛出异常时,connstmtrs 全部悬空,连接池不知道连接已可回收。

HikariCP 默认没有 leakDetectionThreshold,这类泄漏会在连接池默默积累,直到池满才发现。

泄漏场景二:事务内远程调用阻塞(updateInventory)

泄漏2: 事务内远程调用

@Transactional
public void updateInventory(String skuId, int quantity) {
    // 步骤1: 数据库扣减库存 — 获取连接
    jdbcTemplate.update(sql, quantity, skuId, quantity);

    // 步骤2: 同步调用外部 WMS 系统 — 阻塞 2-3 秒!
    // @Transactional 事务不提交,连接一直被占用
    restTemplate.postForObject(wmsUrl, request, String.class);
    // 只有方法返回时事务才提交,连接才归还
}

@Transactional 在方法入口开启事务(从池中借出连接),方法返回时才提交事务(归还连接)。如果在事务中间调了外部 HTTP 接口,连接会一直被占用直到 HTTP 响应返回。

一个请求等 2-3 秒,10 个连接最多扛 3-5 个并发。高峰期 TPS 远超这个数。

泄漏场景三:异常路径未归还连接(createLogistics)

泄漏3: 异常路径未归还连接

public void createLogistics(Order order) {
    Connection conn = null;
    try {
        conn = dataSource.getConnection();
        // ... 业务处理
        if (!addressValidator.isValid(order.getAddress())) {
            throw new IllegalArgumentException("地址不在配送范围");
        }
    } catch (Exception e) {
        log.error("创建物流单失败", e);
        throw e;  // ❌ 直接抛异常,没有 finally 释放连接!
    }
    // ❌ 连接从池中借出但从未归还
}

这个更隐蔽——大部分请求走正常路径时 conn 会被正确关闭,但地址校验失败这个异常路径上,catch 里直接 throw,没有 finally 块来释放连接。每触发一次这种异常,就漏掉一个连接。

三处叠加效应

三个泄漏同时存在于同一个服务的不同方法中:

场景 泄漏速率 每个泄漏占连接时间
未关闭资源 100% 泄漏 直到 maxLifetime(10min)
事务内远程调用 请求级阻塞 2-3 秒/次
异常路径未归还 异常时泄漏 直到 leakDetectionThreshold(15s)

连接池一共 30 个连接。正常请求每秒 200+ TPS,三个泄漏加起来,每 10 秒漏掉一堆连接。2 小时后池满。

修复方案

修复一:try-with-resources 自动关闭

try (Connection conn = dataSource.getConnection();
     Statement stmt = conn.createStatement();
     ResultSet rs = stmt.executeQuery(sql)) {
    // 业务处理
} // 自动 close,连接归还到池

修复二:事务内异步化远程调用

public void updateInventory(String skuId, int quantity) {
    jdbcTemplate.update(sql, quantity, skuId, quantity);
    // MQ 异步通知 WMS,不阻塞事务
    rabbitTemplate.convertAndSend("wms.sync", new WmsSyncEvent(skuId, quantity));
}

去掉 @Transactional 只保数据操作,远程调用改成 MQ 异步投递。

修复三:finally 保证归还

} catch (Exception e) {
    log.error("创建物流单失败", e);
    closeQuietly(conn, ps, null);  // 异常路径也释放
    throw e;
}

完整 Git diff:

Git diff 完整修复

部署修复

修复与验证

重新部署 v2.1 后,三个 Pod 依次滚动更新上线。

验证结果

部署后连接池全面恢复:

修复后验证

  • activeConnections 从 29 降到 6
  • pending 从 156 降到 0
  • 超时次数归零
  • 错误率归零
  • p99 响应恢复

避坑建议

1. 永远用 try-with-resources

Java 7+ 提供的 try-with-resources 是防御连接泄漏的第一道防线。手动 finally { close() } 容易遗漏,特别是在多个资源嵌套时。

2. @Transactional 里别调远程

事务连接是宝贵的池资源。事务内做 HTTP 调用、RPC、sleep——统统不要。事务只负责数据库操作,外部交互放外面或走 MQ 异步。

3. 配置 leakDetectionThreshold

spring:
  datasource:
    hikari:
      leak-detection-threshold: 15000  # 连接借出超过 15 秒触发警告

连接泄漏的黄金信号。开启后,被占用的连接超时会在日志输出完整的 stack trace,精确定位泄漏代码行。

4. Actuator + Prometheus 监控连接池

management:
  endpoints:
    web:
      exposure:
        include: metrics,prometheus

需要监控的关键指标:

指标 告警阈值 说明
hikaricp.connections.active > 80% maxPoolSize 连接利用率
hikaricp.connections.pending > 0 有请求在排队等连接
hikaricp.connections.timeout > 0 连接获取超时(紧急)

5. 代码审查关注连接生命周期

Review 清单:

  • [ ] JDBC 操作是否用了 try-with-resources?
  • [ ] @Transactional 方法内有无外部调用?
  • [ ] 异常路径是否确保资源释放?
  • [ ] 自定义线程池的连接是否在 finally 中归还?

附:完整命令清单

系统连接排查

ss -ant 'dport = :3306'                 # 查看某端口连接数
ss -ant 'dport = :3307 or dport = :3308' # 多端口
ss -ant | awk '{print $6}' | sort | uniq -c | sort -rn  # 连接状态统计

应用日志排查

grep 'Connection is not available' app.log | head
grep 'Connection leak detection' app.log | head
grep -c 'Connection leak detection' app.log              # 泄漏触发次数

JVM 线程分析

jstack <pid> | grep 'http-nio' | wc -l                  # 活跃线程数
jstack <pid> | grep 'java.lang.Thread.State' | sort | uniq -c | sort -rn  # 线程状态分布
jstack <pid> | grep -A 30 'http-nio-8080-exec-1'        # 看具体线程栈

HikariCP 运行时指标

curl -s http://localhost:8080/actuator/metrics/hikaricp.connections.active
curl -s http://localhost:8080/actuator/metrics/hikaricp.connections.pending
curl -s 'http://localhost:8080/actuator/metrics/hikaricp.connections.active?tag=pool:order-ds'

连接池配置检查

grep -rn 'leak-detection-threshold\|maximum-pool-size\|connection-timeout' src/main/resources/

📖 完整版带可复现 Demo → opencao.cn 📺 公众号「Ai拆代码的曹操」 🌟 知识星球「源阅会」(82877104)