前两天刚处理完一桩线上事故,HikariCP连接池在业务高峰期突然大批量抛连接失败异常,服务间调用跟着雪崩,排查过程踩了不少坑,也把连接池这块的老底重新翻了一遍。这篇就完整记录一下从日志告警到根因定位再到方案落地的全过程,里面涉及的参数调优和排查思路,对正在用Spring Boot + MySQL这套组合的团队应该都有参考价值。
1. 故障现场:先从日志异常说起
1.1 业务影响与第一直觉
那天地面服务突然收到一批上游调用超时告警,紧接着就是我们自己的应用日志里刷出了大量异常,核心报错就两行:
HikariPool-1 - Exception during pool initialization. java.sql.SQLException: Connections could not be acquired from the underlying database!第一反应是数据库挂了或者网络出了问题,登录跳板机准备连MySQL看看,结果发现MySQL本身运行正常,load不高,连接数也没有爆掉,活跃会话数甚至比平时还低。这就很奇怪了——数据库明明活着,为什么连接池拿不到连接?
1.2 把异常信息拆开看
先别急着改配置,我们来把这条异常信息拆一拆。Connections could not be acquired from the underlying database是HikariCP在connectionTimeout时间内没能从底层数据库获取到可用连接时抛出的最终错误,它只是一个结果,真正的原因藏在更早的日志里。
所以排查的第一步不是盯着这条报错,而是向前翻日志,找更早的异常链。通常你会看到以下几类根因:
- 数据库侧主动断开了连接,但连接池还在用
- 连接池初始化时就失败,比如账号密码错、网络不通
- 连接请求超时,池子里没有空闲连接可分配
- 数据库连接数达到上限
- DNS解析问题导致TCP握手阶段就失败
我这次遇到的问题属于第一类和第四类的结合体,具体怎么回事后面细说。这里先给个结论:遇到连接池报错,先定位是哪一层断的,是网络层、MySQL层还是连接池层。
2. Hikari连接池的工作原理与关键参数
2.1 连接池在中间层做了什么
HikariCP作为目前Spring Boot 2.x默认的数据库连接池,干的事情说白了就是替你维护一批到MySQL的长连接,避免每次SQL请求都走一遍TCP建连、MySQL鉴权、连接初始化的流程。这个过程虽然只要几十毫秒,但在高并发场景下,反复建连的开销会被无限放大。
连接池内部维护了一个ConnectionBag,里面放着空闲连接和一个等待队列。当业务线程请求连接时,Hikari会按顺序做几件事:
- 检查当前总连接数是否小于
maximumPoolSize - 如果有空闲连接,从中取一个
- 如果没有空闲连接,尝试新建连接
- 如果总连接数已达上限,就进入等待队列,等待其他线程归还连接
- 等待超过
connectionTimeout毫秒,就抛出连接获取超时
这个流程听起来简单,但有一个关键点特别容易忽略:连接池里的连接不是永久的。Hikari会按照你的配置对连接进行生命周期管理,包括空闲超时回收、最长存活时间控制、连接有效性检测等。一旦这些配置和数据库侧的超时时间不匹配,就会出现"连接还躺在池子里,但数据库那边早就把它断了"的尴尬情况。
2.2 几个关键参数决定了连接池生死
我用表格把这次排查中涉及到的核心参数整理了一下,方便你对着自己的工程进行比对:
| 参数名 | 默认值 | 作用 | 失败时的影响 |
|---|---|---|---|
connectionTimeout | 30000ms | 等待获取连接的超时时间 | 等待超时直接抛SQLException,伴随大量业务报错 |
maximumPoolSize | 10 | 池中允许的最大连接数 | 连接被占满后,新的请求只能排队等超时 |
maxLifetime | 1800000ms | 连接最大存活时间 | 超过时间后Hikari会强制关闭并替换连接 |
idleTimeout | 600000ms | 空闲连接被回收的时间 | 空闲过久会被清理,如果业务闲时特别长,需要留意 |
minimumIdle | 与maximumPoolSize相同 | 池中维护的最小空闲连接数 | 设置过大=浪费数据库资源,过小=流量尖峰时建连压力大 |
validationTimeout | 5000ms | 连接有效性检测超时时间 | 检测超时会直接判定连接不可用 |
connectionTestQuery | 无 | 自定义检测SQL | 配了以后每次借用连接都会执行,有额外开销 |
这里重点说下maxLifetime。官方建议它应该比数据库或网络基础设施的wait_timeout短几秒,这是为了防止连接被数据库侧回收后,连接池还拿着一根已经断掉的连接继续用。我当时就是在这上面栽了跟头。
2.3 为什么"本来好好的"会突然失败
连接池这种设计有个天然的矛盾:它维护的是长连接,但数据库服务器一般都会配置连接超时机制,比如MySQL默认的wait_timeout是8小时,超过这个时间没有活动的空闲连接会被服务端直接关闭。连接池并不知道这条连接被断了,它仍然认为连接是可用的,下次请求一来,就拿着这根"尸体连接"去执行SQL,结果就是Communications link failure或者Connection is not available。
更隐蔽的是,如果你用了负载均衡或者云数据库,中间还可能有一层网关/LVS在做空闲连接回收,那实际的超时时间可能比MySQL的wait_timeout还短。这就是为什么很多团队把maxLifetime和wait_timeout调成一个值之后,问题反而更严重了——因为中间还有一层"暗雷"。
3. 一步步排查:从配置到数据库的完整链路
3.1 第一步:先核对连接池配置
我先把服务里的Hikari配置捞出来看了一遍,当时的配置大概是这样的:
spring: datasource: hikari: connection-timeout: 30000 maximum-pool-size: 20 minimum-idle: 5 max-lifetime: 1800000 idle-timeout: 600000光看配置其实并不觉得有什么问题,maxLifetime用的还是默认30分钟。但问题恰恰出在默认值上——我潜意识里认为默认值是经过大量生产验证的,不会有什么坑,却忽略了它和数据库侧参数的匹配关系。
3.2 第二步:查MySQL侧的"杀手变量"
接下来登录MySQL,检查超时相关变量:
SHOW GLOBAL VARIABLES LIKE '%timeout%';结果让我吃了一惊:
| 变量名 | 值 |
|---|---|
wait_timeout | 60s |
interactive_timeout | 60s |
net_read_timeout | 30s |
net_write_timeout | 60s |
数据库的wait_timeout居然被设置成了60秒,而连接池的maxLifetime是1800000毫秒(30分钟)。这意味着什么?MySQL在60秒内没有收到来自该连接的请求,就会主动断开,而连接池里的连接却还"自以为活着"——这种配置不炸才是怪事。
后来问了DBA才知道,这套MySQL是云厂商提供的RDS,前阵子做了一次参数优化,把超时时间调小了,方便释放空闲连接以节省资源。出发点没错,但完全没有和应用侧沟通,属于典型的"两方各自调参、互相不知道"的协作漏洞。
3.3 第三步:验证连接生命周期
为了更直观地确认连接是不是被MySQL掐断的,我做了一个简单的实验:从应用服务器上用mysql客户端连上去,建一个连接后不执行任何SQL,静置70秒后再执行SELECT 1,结果直接抛出了:
ERROR 2006 (HY000): MySQL server has gone away这就实锤了,MySQL确实在60秒左右就会回收空闲连接。同时我在MySQL上用SHOW PROCESSLIST观察,发现应用连接池创建的那些Sleep状态的连接,都是存活不到1分钟就被清掉了,然后连接池需要不断重建连接,高峰期一上来,建连速度跟不上请求速度,连接池就进入"假死"状态。
3.4 第四步:抓取实时连接状态
为了搞清楚连接请求为什么失败,我在应用服务器上试过tcpdump抓包,看到的现象是:应用每拿到一个连接,MySQL就发来一个FIN包,把连接断开。这在抓包里非常明显——TCP连接刚建立,握手包都还没暖热,服务端就主动挥手了。
这一步虽然是事后分析的补充佐证,但它能帮你把问题定性得更清楚:到底是网络设备断的,还是MySQL断的,还是连接池自己断的。不同层面的断连,后续解法完全不一样。
4. 根因确认与方案落地
4.1 问题真正出在哪里
到这里,根因就很清晰了:
- MySQL侧
wait_timeout=60s,远小于Hikari的maxLifetime=30min - 空闲连接被MySQL回收后,Hikari仍然将其视为可用连接
- 业务请求到来时分配到"死连接",SQL执行失败,连接池尝试重建连接
- 高峰期大量请求同时触发建连,短时间内连接数达到
maximumPoolSize上限,后续请求排队超时 - 最终表现为大量
Connection is not available和Connections could not be acquired异常
说白了,就是连接池认为该由它来管理连接的生命周期,数据库却在背后偷偷把连接掐了,两边没有商量好。
4.2 配置调整的具体操作
找到根因之后,方案其实并不复杂,核心原则就是让连接池的连接存活时间永远比数据库回收时间短。
我建议的调整方案如下:
spring: datasource: hikari: connection-timeout: 30000 maximum-pool-size: 20 minimum-idle: 10 max-lifetime: 55000 idle-timeout: 30000 validation-timeout: 3000 connection-test-query: SELECT 1注意几个关键决定:
max-lifetime设为55000ms,目的是比MySQL的60s短5秒,保证Hikari会在MySQL回收之前主动替换掉连接connection-test-query加上SELECT 1,让Hikari在每次借用连接前做一次轻量探活,防止用到已经被服务端断开的连接。这里有人可能会说Hikari本身支持JDBC4的isValid()检测,不需要配这个,但在特定驱动版本下我曾经遇到isValid()不生效的情况,加一条显式SQL更稳妥,代价是每次获取连接多一次轻量查询,在低延迟业务中完全可以接受idle-timeout改成30000ms,让空闲连接更早被清理,避免占用数据库连接数minimum-idle从5调到10,是因为这个服务有典型的流量尖峰特征,保留一定量的空闲连接能避免瞬时建连压力
这里有个细节值得展开:为什么我不直接把maxLifetime调成比60s更小,比如30s?因为maxLifetime太短会导致连接池频繁建连,反而增加数据库压力,理想值是在数据库超时时间的基础上留出5~10秒的安全余量。
4.3 上线后的验证与监控
改完配置之后,我并没有立刻上线,而是先在测试环境模拟了“空闲超过60秒再发起请求”的场景,确认连接能正常获取并执行SQL。随后灰度上线,观察了一个业务高峰周期,连接池监控数据恢复平稳,没有再出现连接获取超时的告警。
另外我把相关指标加到了监控大盘上,重点看这几项:
- 活跃连接数与空闲连接数的变化趋势
- 连接池等待获取连接的平均耗时
pending等待队列的深度- 新建连接的速率
这些指标在Spring Boot下可以通过micrometer暴露出来,配合Prometheus和Grafana就能很方便地看连接池运行状态。连接池这种底层组件,平时不声不响,一出问题就是灾难性的,所以监控一定要提前做好。
5. 连接失败常见原因速查与避坑技巧
5.1 一张表看清不同失败根因
这次之后,我把平时遇到过的连接池失效场景都整理了一遍,方便你排查时快速定位:
| 日志特征 | 可能原因 | 易混淆点 |
|---|---|---|
Connection is not available. Request timed out | 连接池被占满,等待超时 | 不一定是连接池配置问题,可能是慢SQL拖住了连接不释放 |
Connections could not be acquired | 数据库连接数到达上限或网络不通 | 有可能是数据库账号权限问题,也可能是防火墙拦截 |
MySQL server has gone away | 数据库侧已断开连接 | 注意区分服务端主动断连和客户端主动断开 |
Communications link failure | 网络异常、DNS解析失败或服务端宕机 | 不一定是MySQL本身问题,中间负载均衡设备也可能导致 |
Access denied for user | 账号密码错误或数据库权限不足 | 大概率不是连接池配置问题,别在这上面浪费时间 |
Too many connections | 数据库max_connections达到上限 | 有可能是连接池的maximumPoolSize合计超过数据库上限 |
5.2 排查时的小技巧
排这类问题有个屡试不爽的方法:先看连接的源头,再看连接的终结者。什么意思呢?就是先确认连接是哪来的、由谁创建、去哪执行什么SQL,再看是谁把它关掉的——是应用代码?是连接池超时回收?是数据库?还是中间的网络设备?
我常用的命令组合如下,分享给你:
# 1. 看MySQL当前连接分布 mysql> SHOW PROCESSLIST; # 2. 看MySQL连接数限制 mysql> SHOW VARIABLES LIKE 'max_connections'; # 3. 看MySQL当前的等待超时参数 mysql> SHOW VARIABLES LIKE 'wait_timeout'; # 4. 抓取到数据库端口的网络包,确认断连方向 tcpdump -i eth0 port 3306 -nn -A | grep -i "fin\|rst"另外,不要只盯连接池报错本身。很多时候连接池报错只不过是一个"放大器",真正的问题在业务代码或数据库侧。比如有一次我们遇到类似的问题,最后定位到是某个接口里出现了死锁导致连接持有时间过长,连接池被耗尽,表现也是Connection is not available,但它跟超时参数压根没关系。
5.3 运维层面的几点建议
把这次的经验沉淀一下,给正在维护线上服务的朋友几个建议:
先统一连接生命周期口径。连接池的maxLifetime必须比数据库和中间网络设备的空闲超时都短,这是铁律。团队内部最好建立一个参数对照表,每次调整数据库侧的wait_timeout或者接入新的中间件,都要同步检查应用侧连接池配置。
千万别省连接有效性检测。有些团队为了性能把connection-test-query去掉,觉得反正JDBC驱动自带isValid(),但你这个判断依赖驱动版本和数据库协议支持情况。生产环境我会保留显式的SELECT 1检测,尤其是用了各种云数据库、走代理网关的情况下,多一道保险多一分安心。
给连接池加监控和告警。连接池的等待时间、活跃连接数、创建连接速率这三个指标一定要盯。一旦出现等待时间持续走高,说明池子配置或业务SQL有问题,早发现早处理,比等到报错再排查要省力得多。
写在最后的经验
说实话,这次问题本身不算复杂,但它让我重新意识到一个道理:组件默认值在任何生产环境里都不值得盲目信任。HikariCP的默认maxLifetime是30分钟,看起来人畜无害,但一旦数据库侧参数改动,默认值就会变成隐患。
排查连接池问题的思路,最核心的还是那条链路思维:从应用出发,经过连接池,再到数据库,链路每一环都可能成为"凶手",不要只看日志末尾的报错就急着改参数,一定要把整条链路的配置都核对一遍再动手。希望这篇记录能帮你在遇到类似问题时少走一些弯路。