连接池不是越大越好
连接池最常见的误用是把它当成“并发上限调节旋钮”:接口慢了就把池子从 20 调到 100。结果往往是数据库 CPU 打满、锁等待变长、整体 QPS 反而下降。
原因很简单:一个数据库连接在后端对应一个线程和一份内存,数据库能真正并行执行的会话数是有限的,通常和核数同量级。池子开到 200,200 个会话在数据库里排队抢 CPU 和 latch,应用的等待时间只会更长。连接池的作用是复用连接、限制并发,不是提升数据库的处理能力。
容量规划要按“全集群总量”算:实例数 × 每实例池大小。20 个实例每个配 30,就是 600 个连接,而 MySQL 的 max_connections 往往只有 500,第 501 个连接开始直接报 Too many connections,而且报错的通常是最后一个扩容的实例,看起来像“新机器有问题”。
HikariCP 的几个关键参数
| 参数 | 默认 | 建议与原因 |
|---|---|---|
maximumPoolSize |
10 | 按数据库核数与实例数反推,不是越大越好 |
minimumIdle |
等于最大池 | 保持固定大小,避免频繁建连与销毁 |
connectionTimeout |
30s | 改到 1–3s,快速失败优于长时间挂起 |
maxLifetime |
30min | 必须小于数据库 wait_timeout 和中间层空闲超时 |
leakDetectionThreshold |
关闭 | 设成 20s 左右,帮助发现未归还的连接 |
connectionTimeout 是最值得改的一个。默认 30 秒意味着请求会卡在获取连接上,用户端早超时了,线程还占着不放,最终把 Tomcat 线程池也拖满。改成 2 秒后,快速失败至少能让上游熔断,问题不会层层放大。
maxLifetime 和 wait_timeout 的关系是“通信链路失败”的高发原因:MySQL 默认 8 小时断开空闲连接,中间还可能有负载均衡的 5 分钟空闲超时。连接池认为连接还活着,取出来用时才发现已被对端关闭,报错 Communications link failure。把 maxLifetime 设得比所有空闲超时都短,并打开 keepaliveTime,这类问题基本消失。
spring:
datasource:
url: jdbc:mysql://10.0.3.11:3306/shop?useSSL=false&serverTimezone=Asia/Shanghai
username: shop_rw
password: ${DB_PASSWORD}
hikari:
pool-name: order-db-pool
maximum-pool-size: 16
minimum-idle: 16
connection-timeout: 2000
idle-timeout: 300000
max-lifetime: 1200000
keepalive-time: 60000
leak-detection-threshold: 20000
连接泄漏与获取超时
“获取连接超时”有两个完全不同的根因,处理方式相反:
连接泄漏:借了没还。典型来源是手写的 JDBC 代码忘了 close、Connection 被塞进静态变量、或者异常分支里没有 finally。现代框架基本都帮你关闭,但 @Transactional 方法里做长时间 HTTP 调用会“合法地”占着连接——事务不结束,连接就不归还。
连接不够用:池子确实小了,或者下游太慢。区别方法是看活跃连接数与获取等待时间:活跃数长期等于最大值且等待队列非空,是容量问题;活跃数不高但偶尔打满,更像泄漏或长事务。
// 错误:事务里做远程调用,连接被占住几秒
@Transactional
public void createOrder(OrderCommand command) {
orderMapper.insert(command.toOrderDO());
String token = remotePayClient.prepare(); // 网络调用可达数秒
orderMapper.updateToken(command.getOrderId(), token);
}
// 正确:远程调用移到事务外,事务只包住数据库操作
public void createOrder(OrderCommand command) {
String token = remotePayClient.prepare();
orderTransactionalService.save(command, token);
}
另外,@Transactional 的自调用失效、嵌套事务传播级别设置不当,也会让连接持有时间远超预期。REQUIRES_NEW 在外层事务未结束时开新事务,会额外占用一个连接,高并发下很容易把池子翻倍消耗。
排查步骤
# 1. 当前池的实时状态:待获取、活跃、空闲
curl -s localhost:8080/actuator/metrics/hikaricp.connections.pending
curl -s localhost:8080/actuator/metrics/hikaricp.connections.active
curl -s localhost:8080/actuator/metrics/hikaricp.connections.acquire
# 2. 数据库侧的会话分布,按来源主机聚合
mysql -h 10.0.3.11 -e "
SELECT SUBSTRING_INDEX(host, ':', 1) AS client, COUNT(*) AS conn
FROM information_schema.processlist
GROUP BY client ORDER BY conn DESC LIMIT 10;"
# 3. 看有没有长事务卡住连接
mysql -h 10.0.3.11 -e "
SELECT trx_id, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS secs, trx_mysql_thread_id
FROM information_schema.innodb_trx ORDER BY secs DESC LIMIT 10;"
第 2 步能直接定位“哪个实例占的连接最多”,第 3 步能看出连接是被事务卡住的。如果 innodb_trx 里有个事务跑了几百秒,先别调池子,去看那条 SQL。
再补一个必查项:连接池的指标要接入监控并配告警。hikaricp.connections.pending 持续大于 0、acquire 的 P99 超过 100ms,就是容量或慢 SQL 的早期信号,比等用户投诉要早得多。
常见坑
每个实例都按单机压测值配置。 单实例 30 个连接跑得很好,20 个实例就是 600,数据库直接拒绝。容量要按总量算。
池子大 + 慢 SQL 组合。 慢查询占住连接,池子越大堆积越多,最后数据库 CPU 100%,整个集群雪崩。
maxLifetime 大于 wait_timeout。 空闲一段时间后必然出现链路失效,重启后恢复,形成“每天早高峰第一波请求失败”的规律性故障。
读写分离共用一个池配置。 从库通常承载更多读请求,主库连接数应该更少更精,两者混在一个池里,主库被读流量拖慢。
忽略数据库的 max_connections 与代理层限制。 用了数据库中间件之后,真正的上限在中间件侧,应用的池大小之和超过它,一样会被拒。
没有 pool-name。 多数据源的应用里,日志和指标全是 HikariPool-1,出问题时根本分不清是哪个库。
评论