那次P99延迟暴涨,让我彻底重新理解了数据库连接池
去年双十一,我们线上出了一次诡异的故障。事后复盘,我发现了问题的根源——一个我之前一直忽略的、看似理所当然的配置:数据库连接池大小。这篇文章,我就用这次踩坑经历,和大家聊聊我对连接池的重新认识。不讲那些网上烂大街的"连接池很重要",只讲我亲身验证过的、可能和你认知完全相反的东西。
故障回顾:一切正常,延迟暴涨
事情是这样的。那天凌晨两点,我被报警吵醒:P99延迟从正常的30ms飙升到800ms。登录服务器看了看,CPU、内存、磁盘IO都正常。网络流量也没异常。数据库机器状态也OK。奇怪了。业务量没有大的波动,代码也没有发布,怎么就突然慢了呢?我用show processlist看了一下数据库连接——只有20多个连接在跑,远没到我们配置的上限200。问题不在数据库本身。那在哪?我回头看了看应用的连接池配置,突然发现一个我没注意过的参数:minimumIdle。我们的配置里,minimumIdle = 50。然后我又查了一下监控——连接池里有70多个空闲连接。等等……minimumIdle是50,怎么会有70多个空闲连接?原来,HikariCP会在负载低的时候逐步创建连接,直到达到minimumIdle。而凌晨两点这种低峰期,连接池已经"热身"完毕,50个最小连接已经就位。但是,问题就出在这50个最小连接上。
你可能误解了连接池的工作方式
很多人(包括之前的我)觉得连接池就是:设置一个最大连接数,业务用多少取多少,不用就归还。很简单。但实际上,连接池的工作方式比你想象的复杂得多。HikariCP(我们用的连接池)有个默认行为:它会保持minimumIdle数量的活跃连接,即使这些连接完全不被使用。这50个连接,在凌晨两点的低负载下,平均每个连接每分钟只被使用1-2次。这意味着什么?每个连接在两次使用之间的空闲时间很长,但这些连接依然保持着和数据库的活跃状态。问题来了:MySQL有个参数叫wait_timeout,默认是8小时。但如果一个连接8小时没有任何查询,MySQL就会主动关闭这个连接。但是HikariCP会检测连接,如果发现连接死了,会立刻创建一个新的。等等,这样的话,应该自动恢复才对啊?问题在哪?问题在于:连接检测和重建不是零成本的。当一个连接失效后,下一个请求拿到这个"死连接",尝试使用时会发现连接已断开。HikariCP会丢弃这个连接,从池中取出另一个——如果池中没有可用的有效连接,它会创建一个新的。但问题在于,如果短时间内大量连接同时失效(比如MySQL重启,或者网络抖动),连接池会进入"连接重建风暴"——同时创建大量新连接,数据库压力骤增。这,就是我们那天P99延迟暴涨的真正原因。
真正的问题:连接数和业务规模的错配
我后来和DBA聊,他说了一句让我印象深刻的话:你们那个服务,平均并发就20-30个连接就够了。但你们配了200最大连接,50个minimumIdle——这意味着,即使只有5个并发请求,数据库也要维护50个活跃连接。每个数据库连接都要消耗资源:内存、CPU、文件描述符。更重要的是——MySQL连接在等待时会占用buffer空间,这个空间是预分配的。一个MySQL连接,默认会占用4MB-10MB的buffer(取决于你的配置)。50个连接,就是200MB-500MB的预分配内存。但如果这50个连接大部分时间都在"空转"——这就是巨大的浪费。而且,更容易被人忽略的是:连接是一种"会话",每个会话都有维护成本。MySQL要管理这些会话的状态,要处理它们的心跳,要追踪它们的事务状态……50个空转连接,和5个真正干活的连接,MySQL的工作量差了10倍。
我是怎么修复的
其实很简单,我做了两件事:
1. 把minimumIdle调低,甚至设为0
对于大多数业务场景,minimumIdle完全不需要那么高。低负载时,让连接池自然收缩到只有真正需要的数量。高负载时,连接池会弹性扩容。我把minimumIdle从50改成了5。
2. 把maximumPoolSize重新评估
我查了线上真实的并发数据,发现我们业务的峰值并发从来没超过80。而数据库能承受的连接数,远比80多得多。所以我需要更大的池?不,我犯了个错误——很多人以为连接池越大性能越好,这是错的。实际上,在数据库端,如果并发连接数超过了某个阈值,性能反而会下降。因为数据库需要管理更多的会话,需要更多的锁竞争,需要更多的上下文切换。我查了一些资料,结合我们DBA的建议,把maximumPoolSize从200调到了50。调完之后的效果:平均连接数从40+降到了10左右、P99延迟从800ms恢复到了正常的25ms、数据库CPU使用率反而更低了。
几个你可能不知道的连接池真相
结合这次踩坑经历,我总结了几个可能和你们认知不同的点:
真相1:连接池不是越大越好
很多人觉得"反正数据库支持几百个连接,我多配点更稳妥"。错。连接池过大的危害:数据库端连接数过多,锁竞争加剧、每个连接的内存开销累积起来很大、数据库要维护更多会话状态,开销增加、连接故障时的重建风暴更严重。正确的做法是:让连接池大小匹配你真实的最大并发量。如果你的业务最大并发是50,那就配50左右的池大小。
真相2:minimumIdle不应该是"越大越好"
很多人认为minimumIdle设高一点可以"预热",让连接池随时ready。但实际上:如果你的业务有明显的高低峰,低谷期的大量空闲连接就是浪费、空闲连接会持续占用数据库资源、长时间空闲的连接反而更容易出问题(MySQL的wait_timeout、网络中断等)。我的建议是:把minimumIdle设为你正常负载的25%左右,或者直接设为0让连接池完全弹性。
真相3:连接池的健康检测不一定靠谱
很多人以为HikariCP有连接检测就不会有问题。但实际上:连接检测有timeout,如果数据库响应慢,检测也会超时、检测本身也是一种开销、在连接池耗尽时,检测请求也会占用一个连接。更更好的做法是:从架构层面避免连接池耗尽。比如合理超时设置、熔断降级、读写分离等。
真相4:你可能不需要那么高并发
这是个架构问题。如果你的并发量高到需要200个数据库连接——你应该考虑的是分库分表、读写分离、或者引入缓存层,而不是单纯增加连接池大小。200个连接打到单库,性能一定比50个连接+缓存层差得多。
我现在的连接池配置
最后分享下我现在的配置逻辑(我们用的是Spring Boot + HikariCP):
spring: datasource: hikari: maximum-pool-size: 60 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000 connection-test-query: SELECT 1
几点说明:maximum-pool-size: 60 — 基于我们的真实峰值并发评估、minimum-idle: 5 — 低谷期保持少量连接,避免资源浪费、connection-timeout: 30s — 获取连接超时时间,设太长会积累更多等待线程、idle-timeout: 10min — 空闲连接存活时间、max-lifetime: 30min — 连接最大生命周期,到期前主动更换。当然,这个配置不是万能公式。你们一定要根据自己的真实业务负载来调整。
总结
这次故障让我意识到,数据库连接池是个"看起来简单但暗坑很多"的组件。很多人(包括之前的我)都觉得它就是个连接管理,不会有大问题。但实际上:配置不当会导致性能剧烈波动、连接数和业务规模不匹配会造成严重资源浪费、不理解它的行为模式,就会在故障时手足无措。我的建议是:不要抄别人的配置,要理解原理,然后根据自己业务调整。下次遇到延迟暴涨,别只盯着SQL和索引——看看你的连接池,可能问题就在那。祝各位的数据库都稳稳当当。