那天凌晨3点,告警短信把整组人炸醒了。
监控大屏上,一个核心服务的RT从平时的80ms飙到了30秒,所有接口全部超时。看Tomcat线程池——200个线程全部在跑,没有一个空闲。再看数据库——MySQL连接数只有12个,远没到上限151。数据库不慢,服务却崩了。
运维第一反应是重启。重启后好了10分钟,又崩。再重启,又是10分钟。
接下来3天的排查,最后锁定的问题只有一行配置。
排查第一天:看起来只是"慢"
从日志入手,错误信息很明确:
java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms. at com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator.call(HikariPool.java:734)
翻译成人话:获取数据库连接等了30秒还没拿到,直接抛异常。
这是 HikariCP 连接池的典型报错——连接池里的连接不够用了,新的请求排队等了 connection-timeout 的时长(默认30秒),还是没等到,报错退出。
第一天的排查方向:是不是有慢SQL把连接占住了?查了MySQL慢查询日志,确实有一条统计SQL跑了3秒——原因是某个表昨天新加了200万行数据,负责统计的索引没建。
加了索引后,SQL从3秒降到50ms。重启服务,以为解决了。
凌晨4点,又崩了。一模一样的报错。
排查第二天:200多个线程全卡在一个地方
第二天做了线程dump。不看不知道,一看吓一跳:
"http-nio-8080-exec-187" #187 daemon prio=5 os_prio=0 tid=... waiting on condition java.lang.Thread.State: TIMED_WAITING at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:358) - waiting on <0x00000006c0d4e8f0> "http-nio-8080-exec-188" #188 daemon prio=5 os_prio=0 tid=... waiting on condition java.lang.Thread.State: TIMED_WAITING at com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:358) - waiting on <0x00000006c0d4e8f0>
从 #187 到 #200+,所有线程都在 HikariPool.getConnection() 上排队。Tomcat的200个线程池全部被这些等待连接的线程占满了,新来的请求根本没线程处理,直接被拒绝。
问题变成了:为什么连接池里的连接不够用?
看了一下配置——问题初步浮出水面:
# 当前配置(就是Spring Boot的默认值,一直没改过) spring.datasource.hikari.maximum-pool-size=10 spring.datasource.hikari.connection-timeout=30000
10个连接。这个服务的正常QPS是100,每个数据库查询平均50ms,理论上 10 / 0.05 = 200 QPS 上限,100 QPS 应该够用。
但问题在于:当出现一个3秒的慢查询时,整个池瞬间就崩了。
第三天:终于找到了根因——不是1个参数的锅,是3个默认值联手搞的事
算一笔账,你就能明白为什么"10个连接+30秒超时"是生产环境的定时炸弹:
灾难推演:假设QPS=100,10个连接。当1条SQL从50ms突变为3秒(缺索引/数据量暴涨/锁等待),有效吞吐量从 10/0.05=200 QPS 骤降到 10/3=3.3 QPS。每秒钟有 97 个请求在排队等连接。每个请求最多等 connection-timeout=30秒才报错。30秒 × 97个/秒 = 2910个请求在Tomcat线程池里排队。而Tomcat默认只有200个线程——线程池先于连接池被击穿,整个服务假死。
重新审视 HikariCP 的默认值,有3个参数是专门为开发机设计的,不是为生产环境设计的:
参数 |
默认值 |
开发机的逻辑 |
生产环境的现实 |
maximum-pool-size |
10 |
本地就一个用户,10个连接绰绰有余 |
生产QPS 100+,一条慢SQL就让10个连接全部占满 |
connection-timeout |
30000(30秒) |
本地请求少,等久一点无所谓 |
30秒超时意味着线程池被排队线程占满,其他正常请求全被拒绝 |
idle-timeout |
600000(10分钟) |
连接池小,回收快慢无所谓 |
空闲连接10分钟才回收,连接长时间不用可能已被数据库端断开,下次使用直接报错 |
单个看,每个默认值都有它的道理。但三个组合在一起,高并发场景下的效果是乘法级的灾难:
- maximum-pool-size=10 → 吞吐天花板极低
- connection-timeout=30000 → 出问题时线程堵30秒才退出,堵住的线程又占着Tomcat线程池
- idle-timeout=600000 → MySQL端 wait_timeout 如果设了较短值(比如某些云数据库默认60秒),连接在池里闲置10分钟早就被数据库关了,取出来用就报 CommunicationsException
三个参数的Production-Ready配置
1. maximum-pool-size:不是越大越好,但要算清楚
先看数据库能承受多少连接:
-- 查数据库最大连接数 SHOW VARIABLES LIKE 'max_connections'; -- 查当前已用连接数 SHOW STATUS LIKE 'Threads_connected';
假设MySQL最大连接数151,系统账号和监控占20个,剩下131个给业务。如果线上有4个应用实例,每个实例的连接池大小上限 = 131 / 4 ≈ 32。
然后再用业务公式验证:
# 公式:连接池大小 = 峰值QPS × 平均数据库查询RT(s) × 1.5 # 示例:峰值QPS=200,平均SQL RT=50ms=0.05s # → 200 × 0.05 × 1.5 = 15 个连接 # 但必须保留慢查询buffer:再预留30% # → 15 × 1.3 ≈ 20 个连接 spring.datasource.hikari.maximum-pool-size=20
一个经验值:单实例一般配 20~50,不要超过数据库最大连接数 ÷ 实例数的 80%。10 是绝对不够的。
2. connection-timeout:30秒太长了,3秒就够了
你在生产环境等过一个30秒的请求吗?用户早就关页面了。
connection-timeout 的逻辑是"等多久还没拿到连接就抛异常"。30秒意味着:
- 用户等了30秒才看到报错(他10秒前就已经关了页面)
- 这30秒里Tomcat线程一直被占用,堵着后面的正常请求
- 如果30秒内有3000个请求进来,3000个线程全堵着——Tomcat线程池瞬间爆炸
# 设置3秒超时——如果3秒拿不到连接,说明连接池大概率已经崩了 # 快速失败比长时间等待要好得多 spring.datasource.hikari.connection-timeout=3000
快速失败原则:3秒拿不到连接就报错,用熔断/降级兜底,而不是让所有请求堵30秒然后把整个服务拖死。
3. idle-timeout 和 max-lifetime:别让你的连接被数据库偷偷关了
最后一个坑:HikariCP里的连接闲置太久,数据库那边早就断开了,拿出来用就报错。
# MySQL默认 wait_timeout = 28800(8小时),一般不改 # 但很多云数据库RDS的默认值不同,有些只有60秒 # HikariCP 的 max-lifetime 必须比数据库的 wait_timeout 短 # 一般设 30分钟(1800000ms) spring.datasource.hikari.max-lifetime=1800000 # 30分钟,默认值OK # idle-timeout 要小于 max-lifetime # 设置5分钟——连接5分钟没用就回收,不让数据库端先断开 spring.datasource.hikari.idle-timeout=300000 # 5分钟 # 额外加 keepalive ——定期ping保持连接活跃(HikariCP 4.0+ 支持,最低间隔30秒) spring.datasource.hikari.keepalive-time=60000 # 每60秒ping一次
一张表记住所有配置
参数 |
默认值(坑) |
生产建议值 |
为什么 |
maximum-pool-size |
10 |
20~50 |
按 QPS×RT×1.5 计算 |
connection-timeout |
30000 |
3000 |
快速失败,堵30秒会拖死线程池 |
idle-timeout |
600000 |
300000 |
5分钟回收,防止数据库端先断 |
max-lifetime |
1800000 |
1800000 |
默认OK,但必须小于MySQL wait_timeout |
keepalive-time |
0(关闭) |
60000 |
每60秒ping保活 |
写在最后
HikariCP 本身是一个非常好的连接池——启动快、开销低、性能顶级。问题不在于它,在于默认值是为"本地开发"这个场景设置的。Spring Boot 把它作为默认连接池时,并没有把默认值改成生产级别的。
这件事的教训不只是"记得改配置"——上线前检查清单里,连接池配置必须是必查项。10个连接在本地跑得飞起,到了生产就是一颗不知道什么时候爆炸的定时炸弹。
你的项目里 HikariCP 的这几个参数改过了吗?还在用默认值的话,下次高并发可能就不是排查3天的问题了。