那天凌晨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 prio5 os_prio0 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 prio5 os_prio0 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-size10 spring.datasource.hikari.connection-timeout3000010个连接。这个服务的正常QPS是100每个数据库查询平均50ms理论上 10 / 0.05 200 QPS 上限100 QPS 应该够用。但问题在于当出现一个3秒的慢查询时整个池瞬间就崩了。第三天终于找到了根因——不是1个参数的锅是3个默认值联手搞的事算一笔账你就能明白为什么10个连接30秒超时是生产环境的定时炸弹灾难推演假设QPS10010个连接。当1条SQL从50ms突变为3秒缺索引/数据量暴涨/锁等待有效吞吐量从 10/0.05200 QPS 骤降到 10/33.3 QPS。每秒钟有 97 个请求在排队等连接。每个请求最多等 connection-timeout30秒才报错。30秒 × 97个/秒 2910个请求在Tomcat线程池里排队。而Tomcat默认只有200个线程——线程池先于连接池被击穿整个服务假死。重新审视 HikariCP 的默认值有3个参数是专门为开发机设计的不是为生产环境设计的参数默认值开发机的逻辑生产环境的现实maximum-pool-size10本地就一个用户10个连接绰绰有余生产QPS 100一条慢SQL就让10个连接全部占满connection-timeout3000030秒本地请求少等久一点无所谓30秒超时意味着线程池被排队线程占满其他正常请求全被拒绝idle-timeout60000010分钟连接池小回收快慢无所谓空闲连接10分钟才回收连接长时间不用可能已被数据库端断开下次使用直接报错单个看每个默认值都有它的道理。但三个组合在一起高并发场景下的效果是乘法级的灾难maximum-pool-size10 → 吞吐天花板极低connection-timeout30000 → 出问题时线程堵30秒才退出堵住的线程又占着Tomcat线程池idle-timeout600000 → 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 # 示例峰值QPS200平均SQL RT50ms0.05s # → 200 × 0.05 × 1.5 15 个连接 # 但必须保留慢查询buffer再预留30% # → 15 × 1.3 ≈ 20 个连接 spring.datasource.hikari.maximum-pool-size20一个经验值单实例一般配 20~50不要超过数据库最大连接数 ÷ 实例数的 80%。10 是绝对不够的。2. connection-timeout30秒太长了3秒就够了你在生产环境等过一个30秒的请求吗用户早就关页面了。connection-timeout 的逻辑是等多久还没拿到连接就抛异常。30秒意味着用户等了30秒才看到报错他10秒前就已经关了页面这30秒里Tomcat线程一直被占用堵着后面的正常请求如果30秒内有3000个请求进来3000个线程全堵着——Tomcat线程池瞬间爆炸# 设置3秒超时——如果3秒拿不到连接说明连接池大概率已经崩了 # 快速失败比长时间等待要好得多 spring.datasource.hikari.connection-timeout3000快速失败原则3秒拿不到连接就报错用熔断/降级兜底而不是让所有请求堵30秒然后把整个服务拖死。3. idle-timeout 和 max-lifetime别让你的连接被数据库偷偷关了最后一个坑HikariCP里的连接闲置太久数据库那边早就断开了拿出来用就报错。# MySQL默认 wait_timeout 288008小时一般不改 # 但很多云数据库RDS的默认值不同有些只有60秒 # HikariCP 的 max-lifetime 必须比数据库的 wait_timeout 短 # 一般设 30分钟1800000ms spring.datasource.hikari.max-lifetime1800000 # 30分钟默认值OK # idle-timeout 要小于 max-lifetime # 设置5分钟——连接5分钟没用就回收不让数据库端先断开 spring.datasource.hikari.idle-timeout300000 # 5分钟 # 额外加 keepalive ——定期ping保持连接活跃HikariCP 4.0 支持最低间隔30秒 spring.datasource.hikari.keepalive-time60000 # 每60秒ping一次一张表记住所有配置参数默认值坑生产建议值为什么maximum-pool-size1020~50按 QPS×RT×1.5 计算connection-timeout300003000快速失败堵30秒会拖死线程池idle-timeout6000003000005分钟回收防止数据库端先断max-lifetime18000001800000默认OK但必须小于MySQL wait_timeoutkeepalive-time0关闭60000每60秒ping保活写在最后HikariCP 本身是一个非常好的连接池——启动快、开销低、性能顶级。问题不在于它在于默认值是为本地开发这个场景设置的。Spring Boot 把它作为默认连接池时并没有把默认值改成生产级别的。这件事的教训不只是记得改配置——上线前检查清单里连接池配置必须是必查项。10个连接在本地跑得飞起到了生产就是一颗不知道什么时候爆炸的定时炸弹。你的项目里 HikariCP 的这几个参数改过了吗还在用默认值的话下次高并发可能就不是排查3天的问题了。