HikariCP connectionTimeout 频繁触发,如何量化区分「连接真泄漏」vs「被慢 SQL 占满」?
该问题的正文与回答。
背景
我负责一个订单写入服务:Spring Boot 3.2 + JDK 17 + HikariCP 5.1 + MySQL 8.0,单实例 8C16G,前置 4 实例。核心链路是「接收下单 → 写主库 → 发事件」。HikariCP 关键配置:maximumPoolSize=20、connectionTimeout=3000ms、leakDetectionThreshold=60000ms。
现象
白天平稳(QPS ~800,P50 20ms),晚高峰(QPS ~2000,P50 30ms)时约 1% 的请求在 3s 后抛 SQLTransientConnectionException: Connection is not available, request timed out after 3000ms。同时 leakDetectionThreshold 没有触发,日志里始终看不到 Apparent connection leak detected。
已尝试的排查与结果
- 高峰期抓
SHOW PROCESSLIST:最慢一条是带 IN 子查询的批量 UPDATE,耗时 2.8s;但没有看到连接长时间处于Sleep状态。 - 把
maximumPoolSize从 20 提到 50:超时比例反而从 1% 升到 4%——我怀疑是慢 SQL 把池占满后,更多并发进来一起排队,越扩越堵。 - 把
leakDetectionThreshold降到 30000ms 复现晚高峰:依然没有泄漏告警。 - 用 JFR 抓 CPU + 锁竞争:没看到明显的
synchronized热点。
我卡住的点 「连接拿了没还(真泄漏)」和「连接被慢 SQL 合法占用、池被占满导致新请求等不到(伪超时)」这两种情况,表象都是 connectionTimeout,但处理路径完全不同:前者要查代码里的 try-with-resources / finally close / 异常分支漏 close,后者要治慢 SQL 或重估池大小。我需要一个能直接判别当前属于哪种的可观测手段。
期望得到的答案形态
- 一个可操作的判别方法:看 HikariCP 的哪个内部指标 / JMX MBean / 日志字段,怎么读数能区分「pending 队列堆积(池被慢 SQL 占满)」vs「连接持有时间异常长(真泄漏)」;最好带具体的 MBean 名或 SQL/命令。
- 「慢 SQL 占满池」场景下的一组推荐参数组合(
connectionTimeout/leakDetectionThreshold/maximumPoolSize/maxLifetime)和理由,为什么不是越大越好。 - 如果社区有人踩过完全一样的坑,欢迎贴当时的现象、最终解法和复盘链接。
【2026-10-24 补充:后来定位到的真因 + 一个反直觉的判别口径】
后来用 Arthas trace com.zaxxer.hikari.pool.ProxyConnection close 抓了 200 次调用,发现大约 35% 的超时请求对应的连接,close() 根本没被调用——不是慢 SQL 占满,是代码里一个 catch (SQLException e) 分支漏了 finally { conn.close() },异常路径直接 return 了。leakDetectionThreshold=60000ms 没触发是因为那些连接在 60s 内被 GC 的 ReferenceQueue 回收后 HikariCP 的 ConnectionPool#drainPool 才归还,不是真正的"泄漏"而是"延迟归还"。
一个比 leakDetectionThreshold 更早的判别口径(后来验证有效):
-- MySQL 侧:区分"真持有"vs"已 kill 但驱动未归还"
SELECT
COUNT(*) AS active_sessions,
SUM(CASE WHEN `Time` > 60 AND `Command` = 'Sleep' THEN 1 ELSE 0 END) AS true_idle_leak,
SUM(CASE WHEN `Time` > 60 AND `Command` != 'Sleep' THEN 1 ELSE 0 END) AS true_slow_query
FROM information_schema.PROCESSLIST
WHERE `USER` = 'app_user' AND `ID` != CONNECTION_ID();
true_idle_leak > 0:连接在 MySQL 侧已经是 Sleep 但应用侧没 close → 真泄漏(治代码)true_slow_query > 0:连接在 MySQL 侧还在跑 → 慢 SQL 占满(治 SQL / 池大小)- 两个都 = 0 但
connectionTimeout仍触发 → 驱动层僵尸窗口(MAX_EXECUTION_TIMEkill 后驱动未归还,参见我另一篇 id=1371 的问题)
这个口径比 leakDetectionThreshold 早 30~60s 出信号,因为它直接看 MySQL 侧的会话状态,不依赖 JVM 侧的 GC 回收时机。