
1. 线上事故复盘:连接泄漏、雪崩与默认配置的坑
凌晨两点告警群炸了,核心交易接口 RT 从 50ms 直接拉到 30s+。日志里全是 Connection is not available, request timed out after 30000ms。重启应用能缓一阵,十分钟后直接打回原形。DBA 那边一看,MySQL max_connections 已经见底,一堆 Sleep 状态的僵尸连接赖着不走。
典型的连接泄漏引发的级联雪崩。很多团队一上来就怪 HikariCP 不稳定,但真正的问题往往出在对默认配置缺乏敬畏,以及对连接生命周期的管理失控。
别信开箱即用,生产环境必须手动接管。 默认配置在开发环境跑得挺欢,放到高并发场景就是定时炸弹:
connectionTimeout=30000太长了。微服务链式调用里,上游网关或调用方会因为这个 30s 的死等堆积大量线程,Tomcat/Netty 线程池瞬间被打满,紧接着就是 OOM 或 CPU 飙高。线上等 30s 基本等于放弃治疗,业务侧根本扛不住。maximumPoolSize=10是默认值,但很多项目一上线就盲目改到 50、100 甚至 200。池子大了没用,数据库的 CPU、IO 和网络连接数是有硬顶的。连接数一旦超过 DB 承载阈值,上下文切换和锁争用会让性能断崖式下跌。- 默认不开泄漏检测。业务线程拿到连接后,如果因为未捕获异常、死锁,或者单纯忘了在
finally里关连接,池子就会把它当成"正在使用"。请求量一上来,可用连接肉眼可见地变少,最后彻底枯竭。
2. 底层机制:HikariCP 为什么快
调参不能靠猜,得知道它底层怎么跑连接。HikariCP 能号称最快,核心就是甩掉了传统 ArrayBlockingQueue 的重锁,搞了一套无锁快速路径 + 轻量级挂起的设计。
它内部维护连接的核心是个叫 ConcurrentBag 的容器。这玩意儿不是标准的 JUC 队列,而是基于 ThreadLocal 和 CAS 的混合体:
- FastPath(本地缓存) :线程第一次借连接,会先翻自己的
ThreadLocal。如果里面有,直接拿走,全程无锁,速度极快。 - Shared List(CAS 共享池) :本地没命中,就去共享列表里扫。这里用的是
AtomicReference配合CAS做状态更新,避开了ReentrantLock的线程调度开销。 - 等待机制 :如果池子真的空了,Hikari 不会用重型队列让线程排队竞争,而是直接
LockSupport.park()把调用线程挂起。等有连接归还时,再unpark唤醒。这种"生产者直递消费者"的方式,极大减少了队列抖动和上下文切换。
连接状态流转也很干脆:NOT_IN_USE → IN_USE → 归还后重置 autoCommit 和事务上下文,放回共享池。驱逐连接靠的是 maxLifetime 定时器,而且内部自带随机抖动(jitter),防止大批连接同一时间点集中销毁重建,避开"惊群效应"。
摸清这套机制后,调优的思路就清晰了:核心是降低竞争热度,减少线程挂起频率,让池子大小刚好接住业务峰值就行。
3. 参数怎么给:别套公式,看业务模型
网上流传的各种池大小计算公式,只能当初始参考值。真正的配置得靠压测和业务特征反推。
maxLifetime:必须小于 DB 侧的超时时间
MySQL 默认 wait_timeout 是 8 小时,但很多云厂商或运维规范会改成 600s 甚至更低。HikariCP 这边如果设成 0(不回收)或者比 DB 端长,就会频繁遇到 Communications link failure。线上建议直接压到 DB wait_timeout 的 70% 左右,配合内置的抖动,一般设 20 分钟(1200000ms)比较稳妥。长事务或批量任务多的场景可以稍微放宽,但别硬刚 DB 的底线。
connectionTimeout:快速失败比干等重要
这个参数决定线程在池子里等连接最多等多久。线上千万别留 30s。一般业务能容忍的排队时间也就 2~3 秒。超过这个时间拿不到连接,说明池子真打满了或者后端 DB 已经卡死,这时候直接抛异常走熔断降级,比让线程挂着把整个 JVM 拖下水强得多。生产环境通常给 3000ms 就够,配合 Sentinel 或 Resilience4j 做降级。
keepaliveTime 与池容量
新版 Hikari 默认 keepaliveTime 是 0(不探活)。云网络环境波动大,建议显式开启,设个 30000ms。如果 JDBC 驱动版本较新(支持 Connection.isValid()),就别再配 connectionTestQuery=SELECT 1 了,驱动自带的探活更高效。
至于池大小,有个经验值:IO 密集型应用,池子大小 ≈ CPU核心数 × 0.8 × (1 + IO等待系数/计算系数)。数据库操作 IO 等待远大于计算,系数通常在 10~20 之间。一台 4C 机器算下来大概 50 左右。但这只是起点,压测时盯着连接池的 Active 和 Pending 指标看。如果 Pending 持续大于 0,说明池子小了或者超时太短;如果 Active 长期顶满但 DB CPU 没跑满,那绝对是慢 SQL 或者锁等待在拖后腿,这时候加池子没用,得查业务。
4. 主动防御:泄漏检测、慢 SQL 拦截与止血机制
光调参是防守,得把探测和拦截的网织起来。
泄漏检测(LeakDetectionThreshold)
Hikari 自带这个功能,原理是包装一层 Connection 代理,借出时记下时间戳和调用栈。超过设定阈值没还回来,就打印 WARN。配置项就一行:
yaml
spring:
datasource:
hikari:
leak-detection-threshold: 5000
注意,线上千万别默认开着。因为它每次借连接都会 new Throwable() 抓堆栈,CPU 和内存开销不小。平时关着,排查问题时通过配置中心动态开 10~20 分钟抓个现场就行。日志里会直接打出 Leaked connection created at...,顺藤摸瓜就能找到没关连接的业务代码。
慢 SQL 拦截
Hikari 只负责给连接,不解析 SQL。得靠代理层。我们团队常用 datasource-proxy 包一层,再对接 Micrometer 埋点:
java
@Bean
public DataSource dataSource(DataSourceProperties props) {
DataSource rawDs = DataSourceBuilder.create()
.url(props.getUrl()).username(props.getUsername())
.password(props.getPassword()).driverClassName(props.getDriverClassName()).build();
return ProxyDataSourceBuilder.create(rawDs)
.name("TradeDS")
.logQueryBySlf4j(SlowQueryLogLevel.WARN, 1000) // 超 1s 告警
.asJson()
.build();
}
代理层能把 SQL 模板、耗时、参数分布吐出来。结合 Prometheus 抓 io.datasourcesproxy.query.execution.seconds,在 Grafana 配个 P95 > 2s 的告警规则,慢 SQL 基本无处遁形。
兜底 Kill 机制
慢 SQL 把连接池拖死的时候,得有强制释放的手段。但直接 KILL QUERY 风险太大,容易引发数据不一致。推荐分层做:
- 客户端优先:配置 JDBC 的
socketTimeout或Connection.setNetworkTimeout(15000)。超时后驱动层直接切断网络流,连接安全回滚释放。 - 服务端巡检:定时任务扫
information_schema.processlist,捞TIME > 10且状态是Sending data或Locked的连接,先记审计日志,再温和地KILL。 - 中间件层:如果用了 ProxySQL 或 MySQL 8.0+,直接配置资源组或限流规则,让慢查询自动排队,别让它占着核心连接池不放。
代码层面,所有 DB 操作老老实实走 @Transactional + try-with-resources,把兜底机制留给极端异常场景。
5. 线上监控、线程池匹配与排查路子
日常维稳,重点盯 Prometheus 暴露出来的几个核心指标,不用全看,抓关键就行:
hikaricp_connections_active 代表正在干活的连接数。健康状态应该长期在池子总容量的 60%~80% 之间浮动。如果长期贴着上限跑,要么是 SQL 执行慢,要么是池子确实偏小,得结合 DB 端看锁等待。
hikaricp_connections_idle 是空闲连接。如果这个值持续低于 minimumIdle 的配置,说明连接不够用或者正在发生泄漏。池子应该随时保持一定余量,别让新请求来直接触发新建连接的 IO 开销。
hikaricp_connections_pending 是排队等连接的请求数。这个指标最敏感,只要大于 0 持续几秒,就得拉响警报。通常意味着连接池被打满,或者 connectionTimeout 设得太短。
hikaricp_connections_usage_seconds 是连接占用时长。看 P95 和 P99,如果长尾突然飙升,基本是网络抖动或者某个慢查询卡住了连接。
和 Web 线程池怎么配?
Tomcat/Undertow 的线程池跟 DB 连接池是两码事。很多项目把 Tomcat max-threads 开到 200,DB 池只给 20,结果 180 个线程全卡在 borrowConnection 上白白消耗内存。两者得遵循背压原则:Web 线程池负责接流量,遇到 DB 调用尽量异步化(比如 JDK 21 的虚拟线程或者 CompletableFuture),别阻塞主线程。DB 池保持适中(20~60 之间),靠限流器控制入池速率。业务侧的并发压力要能被连接池挡住,而不是直接压垮 DB。
故障排查 Checklist
线上真出了连接池告警,别一上来就重启。按这个顺序走,基本能定位:
- 先看 Grafana:
active、pending曲线是不是异常冲高?usage耗时有没有长尾? - 翻应用日志:搜
leak detected、SocketTimeout、Communications link failure,看有没有连接池本身的报错。 - 抓线程栈:
jstack <pid> | grep -A 30 "HikariPool"。重点找state=WAITING或TIMED_WAITING且栈底在com.zaxxer.hikari...的线程,看它们卡在哪个业务方法。 - 查 DB 端:
SHOW FULL PROCESSLIST或连 performance_schema,看有没有大量LOCK WAIT、长时间Sleep或Sending data的连接。 - 对照代码:拿抓到的堆栈或慢 SQL,回查业务逻辑。重点看有没有漏关
ResultSet、Statement,或者事务边界拉得太长。 - 压测回放:修复后别直接上生产,用同样的流量模型打一遍,确认
pending归零,RT 回到基线再发版。
写在最后
连接池这东西,本质是在有限的数据库连接资源、系统线程数和业务吞吐量之间找平衡。Spring Boot 和 HikariCP 把底层基建做得很扎实,但线上能不能稳,全靠配置建模准不准、监控透不透明、降级策略狠不狠。把黑盒拆成白盒,靠数据说话,少凭感觉调参,这才是生产环境该有的样子。
🎁 福利时间
如果你正在备战面试或者想要学习其他知识,给大家推荐一个宝藏知识库,作者整理了一些列 Java 程序员需要掌握的核心知识,有需要的自取不谢。
知识库地址:https://farerboy.com/
