3. 连接池与线程池调优:HikariCP参数调优、Tomcat线程池配置、连接泄漏排查与修复
连接池和线程池,说白了就是API服务的“血管”和“心脏”。血管堵了,心脏再强也白搭。我见过太多项目,代码写得花里胡哨,结果一压测就挂,查到最后全是连接池或线程池的配置问题。
今天咱们就把这两个池子彻底聊透。我会结合自己踩过的坑,告诉你HikariCP到底怎么配、Tomcat线程池怎么调、连接泄漏怎么抓。
核心观点:连接池和线程池的调优,不是调得越大越好。调大了,资源浪费;调小了,请求排队。关键是找到那个“刚刚好”的点。
3.1 HikariCP参数调优:别再用默认值了
HikariCP现在是Spring Boot的默认连接池,性能确实好。但默认值只适合开发环境,上了生产必须调。
我个人习惯,先看这几个核心参数:
| 参数名 | 默认值 | 我的建议 | 说明 |
|---|---|---|---|
maximumPoolSize |
10 | CPU核心数 × 2 + 有效磁盘数 | 最大连接数,不是越大越好 |
minimumIdle |
10 | 与maximumPoolSize相同 | 最小空闲连接,避免频繁创建 |
connectionTimeout |
30000ms | 5000ms | 等待连接的超时时间 |
idleTimeout |
600000ms | 300000ms | 空闲连接最大存活时间 |
maxLifetime |
1800000ms | 小于数据库连接超时时间 | 连接最大生命周期 |
maximumPoolSize 到底设多少?
我有个公式,用了好几年:
maximumPoolSize = (CPU核心数 × 2) + 有效磁盘数
举个例子,一台4核服务器,单块SSD:
maximumPoolSize = (4 × 2) + 1 = 9
为什么会这样?你想想看,数据库操作大部分时间在等待I/O。CPU核心数×2,基本能打满CPU。再加个磁盘数,是因为写日志、刷盘也需要时间。
小技巧:如果业务以读为主,可以适当调大10%-20%。如果写操作多,保持公式值就行。我在一个电商项目中,读多写少,把连接数从10调到18,QPS直接翻倍。
connectionTimeout 别设太大
默认30秒,太长了。用户等30秒才返回超时,体验极差。我一般设5秒,最多10秒。如果5秒拿不到连接,说明系统已经扛不住了,早点失败比死等强。
spring.datasource.hikari.connection-timeout=5000
spring.datasource.hikari.maximum-pool-size=9
spring.datasource.hikari.minimum-idle=9
spring.datasource.hikari.idle-timeout=300000
spring.datasource.hikari.max-lifetime=600000
3.2 Tomcat线程池配置:别让线程成为瓶颈
Tomcat线程池处理HTTP请求。配不好,要么线程不够用,请求排队;要么线程太多,上下文切换把CPU干满。
核心参数就这几个:
| 参数名 | 默认值 | 我的建议 | 说明 |
|---|---|---|---|
server.tomcat.threads.max |
200 | 100-200 | 最大工作线程数 |
server.tomcat.threads.min-spare |
10 | 20-50 | 最小空闲线程数 |
server.tomcat.accept-count |
100 | 与max一致或略小 | 请求队列长度 |
server.tomcat.max-connections |
8192 | 与线程数匹配 | 最大连接数 |
线程数不是越多越好
我记得有一次,运维把线程数调到500,以为能扛更多请求。结果CPU直接飙到99%,响应时间反而变长了。为什么?线程太多,上下文切换开销把CPU吃光了。
我一般这样算:
线程数 = CPU核心数 × (1 + 等待时间/计算时间)
如果业务是纯计算,线程数≈CPU核心数。如果业务有大量I/O等待(查数据库、调外部API),线程数可以放大。但别超过200,超过200基本就是浪费。
避坑指南:我曾经在一个项目中,把accept-count设得特别大,以为能缓冲请求。结果内存爆了。accept-count本质是个队列,队列太长,请求等太久,前端早就超时重试了,反而加重系统负担。
配置示例:
server.tomcat.threads.max=150
server.tomcat.threads.min-spare=30
server.tomcat.accept-count=100
server.tomcat.max-connections=10000
3.3 连接泄漏排查与修复:最头疼的问题
连接泄漏,说白了就是拿了连接不归还。数据库连接是有限资源,泄漏一个少一个。等到连接池耗尽,整个服务就瘫了。
怎么发现连接泄漏?
我常用的方法有3种:
- 监控连接池指标:HikariCP暴露了
active、idle、pending等指标。如果active长期接近maximumPoolSize,且pending大于0,基本就是泄漏了。 - 开启泄漏检测:HikariCP有个参数
leakDetectionThreshold,设成比如60000ms。连接占用超过60秒没归还,就打印警告日志。 - 看数据库端:MySQL的
show processlist,如果看到大量Sleep状态的连接,且长时间不释放,那就是泄漏。
spring.datasource.hikari.leak-detection-threshold=60000
修复泄漏的套路
连接泄漏的原因,90%是这几种:
- try-catch 里没关闭连接:异常分支忘了关。
- 连接被借出后,业务逻辑耗时太长:比如在事务里调外部API。
- 线程池里的线程挂了:线程异常退出,连接没归还。
修复方法很简单:
- 用 try-with-resources:Java 7+的特性,自动关闭连接。
- 事务里别做耗时操作:查完数据赶紧提交,别在事务里等外部响应。
- 设置连接超时:
connectionTimeout别太大,让连接早点失败。
实战案例:有一次线上告警,数据库连接数飙到500。我一看,有个接口在事务里调了第三方支付,第三方超时了,事务一直没提交,连接一直占着。修复方案:把第三方调用移到事务外面,问题解决。
3.4 连接池与线程池的协同调优
这两个池子不是孤立的。线程池里的每个线程,处理请求时都可能从连接池拿连接。如果线程数 > 连接数,有些线程就得等连接。
我画了张图,帮你理解它们的关系:
协同调优的原则:
- 线程数 ≥ 连接数:否则有些线程永远拿不到连接,浪费。
- 连接数 ≤ 数据库最大连接数:否则数据库会拒绝连接。
- 超时时间要匹配:连接池的
connectionTimeout要小于线程池的超时时间,让失败快速传递。
我的经验:一般先调连接池,再调线程池。连接池是瓶颈的概率更大。调完用压测工具(比如JMeter)跑一下,观察active和pending指标,直到两者都稳定在合理范围。
3.5 总结:调优清单
最后,给你一份我自己的调优清单,照着做基本不会出大问题:
- HikariCP:
maximumPoolSize按公式算,connectionTimeout设5秒,开启leakDetectionThreshold。 - Tomcat线程池:
max不超过200,min-spare设20-50,accept-count别太大。 - 连接泄漏:用
try-with-resources,事务里别做耗时操作,监控active指标。 - 协同:线程数≥连接数,连接数≤数据库上限,超时时间匹配。