症状生产环境某接口高峰期偶发 HTTP 500重试后正常。日志里只有一行could not acquire connection within timeout没有业务异常堆栈。运维说数据库服务器 CPU 和内存都正常。背景Spring Boot 2.7 MyBatis-Plus HikariCP默认连接池PostgreSQL 数据库。日均请求量约 20 万偶发 500 的时间点集中在每天 10:00 和 14:00 前后——恰好是业务高峰。排查过程第一步确认错误类型查 Kibana 日志过滤 500 错误找到完整异常java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.连接池超时不是 SQL 本身的问题。第二步看连接池当前配置# application.yml问题版本spring:datasource:hikari:maximum-pool-size:10connection-timeout:30000idle-timeout:600000max-lifetime:1800000maximum-pool-size: 10——只有 10 个连接。这是 HikariCP 的默认值从未被修改过。第三步分析请求并发量高峰期接口的 QPS 约80~120。每个请求的平均数据库操作耗时约200ms。粗算120 req/s × 0.2s 24 个并发连接。10 个连接根本不够用。连接池被打满后续请求等待 30 秒超时直接 500。但问题没这么简单——为什么平时不报错只在高峰出现第四步找慢查询打开慢查询日志log_min_duration_statement 500发现高峰期有一类查询耗时飙升到2~3 秒-- 问题 SQL执行计划做了全表扫描SELECT*FROMordersWHEREuser_id?ANDstatusIN(1,2,3)ANDcreated_at?ORDERBYcreated_atDESCLIMIT20;用EXPLAIN ANALYZE检查Seq Scan on orders (cost0.00..48234.56 rows1823 width312) Filter: ((user_id 10086) AND (status ANY ({1,2,3}::integer[])) AND ...) Rows Removed by Filter: 892341 Planning Time: 1.2 ms Execution Time: 2847.4 ms全表扫描过滤掉了 89 万行。user_id上根本没有索引。真正的根因链高峰期并发上升 → 命中慢查询无索引全表扫描 → 单个请求占用连接时间从 200ms 拉长到 3s → 同时需要的连接数 120 × 3 360 10 → 连接池耗尽 → 超时 500平时并发低慢查询偶尔出现但不叠加所以不触发。修复方案第一步加索引治本-- 针对查询条件建联合索引CREATEINDEXCONCURRENTLY idx_orders_user_status_createdONorders(user_id,status,created_atDESC);CONCURRENTLY关键字确保建索引时不锁表生产环境必加。加完索引后再EXPLAIN ANALYZEIndex Scan using idx_orders_user_status_created on orders Index Cond: ((user_id 10086) AND (status ANY ({1,2,3}::integer[]))) Filter: (created_at 2024-01-01) Planning Time: 0.8 ms Execution Time: 1.3 ms ← 从 2847ms 降到 1.3ms第二步调整连接池配置治标spring:datasource:hikari:# 核心最大连接数# 公式参考connections (core_count * 2) effective_spindle_count# 4 核服务器 SSD → (4 * 2) 1 9取整为 20留余量maximum-pool-size:20# 最小空闲连接建议等于 maximum-pool-size避免频繁创建销毁minimum-idle:20# 连接超时等待连接的最长时间超过直接报错不要设太长否则线程堆积connection-timeout:5000# 从 30s 改为 5s快速失败# 连接最长存活时间略小于数据库的 wait_timeoutmax-lifetime:1200000# 20 分钟# 空闲连接超时仅 minimum-idle maximum-pool-size 时生效idle-timeout:300000# 5 分钟# 连接健康检查 SQLPostgreSQL 用 SELECT 1connection-test-query:SELECT 1# 连接泄露检测超过此时间未归还连接则打印警告leak-detection-threshold:5000关于连接池大小的常见误区更大不等于更好。连接数过多会导致数据库端上下文切换开销增大反而变慢。HikariCP 官方推荐的公式connections (core_count × 2) effective_spindle_count。第三步开启连接泄露检测设置leak-detection-threshold后若某段代码拿了连接但没有归还事务未提交/回滚、连接未关闭会在日志里打印WARN HikariPool - Connection leak detection triggered for com.example.service.OrderService.getOrderList(), stack trace follows...顺藤摸瓜我们还发现了一处事务忘记声明Transactional导致的连接未释放// ❌ 问题代码手动获取连接后没有 try-finally 保证关闭publicListOrdergetOrdersManual(){ConnectionconndataSource.getConnection();// 拿了连接// ... 业务逻辑 ...// 如果中间抛异常conn.close() 不会执行conn.close();}// ✅ 正确用 try-with-resources 保证关闭publicListOrdergetOrdersFixed(){try(ConnectionconndataSource.getConnection()){// ... 业务逻辑 ...}// 自动关闭}第四步加监控告警防复发暴露 HikariCP 的 Actuator 指标management:endpoints:web:exposure:include:health,metrics关键指标# 连接池当前活跃连接数 hikaricp_connections_active{poolHikariPool-1} # 等待获取连接的线程数这个飙升就是危险信号 hikaricp_connections_pending{poolHikariPool-1} # 连接获取超时次数大于 0 就应该告警 hikaricp_connections_timeout_total{poolHikariPool-1}在 Grafana/Prometheus 中对connections_pending 3设置告警在问题影响用户前就能感知。举一反三连接池问题的常见触发模式场景表现解法慢 SQL 突增高峰期偶发超时加索引优化 SQLN1 查询接口越来越慢改为 JOIN 或批量查询事务粒度过大连接长时间不释放缩短事务范围避免事务内做 HTTP 调用连接泄露连接数只增不减leak-detection-threshold定位后修复数据库重启后连接失效服务重启后才恢复配置keepaliveTime或健康检查总结这次问题表面上是连接池配置不合理但根本原因是慢查询。连接池被打满通常是结果而不是原因——遇到这类问题先查慢查询再调连接池最后加监控。排查路径总结500 错误 → 确认是连接超时还是业务异常 → 连接超时 → 看慢查询日志 → 有慢查询 → EXPLAIN ANALYZE → 加索引 → 没慢查询 → 检查连接泄露 → leak-detection-threshold → 都正常 → 调大连接池 加监控生产环境调整连接池配置前务必先在压测环境验证避免调参不当适得其反。
接口偶发 500,日志里没有异常?连接池被打满了
症状生产环境某接口高峰期偶发 HTTP 500重试后正常。日志里只有一行could not acquire connection within timeout没有业务异常堆栈。运维说数据库服务器 CPU 和内存都正常。背景Spring Boot 2.7 MyBatis-Plus HikariCP默认连接池PostgreSQL 数据库。日均请求量约 20 万偶发 500 的时间点集中在每天 10:00 和 14:00 前后——恰好是业务高峰。排查过程第一步确认错误类型查 Kibana 日志过滤 500 错误找到完整异常java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 30000ms.连接池超时不是 SQL 本身的问题。第二步看连接池当前配置# application.yml问题版本spring:datasource:hikari:maximum-pool-size:10connection-timeout:30000idle-timeout:600000max-lifetime:1800000maximum-pool-size: 10——只有 10 个连接。这是 HikariCP 的默认值从未被修改过。第三步分析请求并发量高峰期接口的 QPS 约80~120。每个请求的平均数据库操作耗时约200ms。粗算120 req/s × 0.2s 24 个并发连接。10 个连接根本不够用。连接池被打满后续请求等待 30 秒超时直接 500。但问题没这么简单——为什么平时不报错只在高峰出现第四步找慢查询打开慢查询日志log_min_duration_statement 500发现高峰期有一类查询耗时飙升到2~3 秒-- 问题 SQL执行计划做了全表扫描SELECT*FROMordersWHEREuser_id?ANDstatusIN(1,2,3)ANDcreated_at?ORDERBYcreated_atDESCLIMIT20;用EXPLAIN ANALYZE检查Seq Scan on orders (cost0.00..48234.56 rows1823 width312) Filter: ((user_id 10086) AND (status ANY ({1,2,3}::integer[])) AND ...) Rows Removed by Filter: 892341 Planning Time: 1.2 ms Execution Time: 2847.4 ms全表扫描过滤掉了 89 万行。user_id上根本没有索引。真正的根因链高峰期并发上升 → 命中慢查询无索引全表扫描 → 单个请求占用连接时间从 200ms 拉长到 3s → 同时需要的连接数 120 × 3 360 10 → 连接池耗尽 → 超时 500平时并发低慢查询偶尔出现但不叠加所以不触发。修复方案第一步加索引治本-- 针对查询条件建联合索引CREATEINDEXCONCURRENTLY idx_orders_user_status_createdONorders(user_id,status,created_atDESC);CONCURRENTLY关键字确保建索引时不锁表生产环境必加。加完索引后再EXPLAIN ANALYZEIndex Scan using idx_orders_user_status_created on orders Index Cond: ((user_id 10086) AND (status ANY ({1,2,3}::integer[]))) Filter: (created_at 2024-01-01) Planning Time: 0.8 ms Execution Time: 1.3 ms ← 从 2847ms 降到 1.3ms第二步调整连接池配置治标spring:datasource:hikari:# 核心最大连接数# 公式参考connections (core_count * 2) effective_spindle_count# 4 核服务器 SSD → (4 * 2) 1 9取整为 20留余量maximum-pool-size:20# 最小空闲连接建议等于 maximum-pool-size避免频繁创建销毁minimum-idle:20# 连接超时等待连接的最长时间超过直接报错不要设太长否则线程堆积connection-timeout:5000# 从 30s 改为 5s快速失败# 连接最长存活时间略小于数据库的 wait_timeoutmax-lifetime:1200000# 20 分钟# 空闲连接超时仅 minimum-idle maximum-pool-size 时生效idle-timeout:300000# 5 分钟# 连接健康检查 SQLPostgreSQL 用 SELECT 1connection-test-query:SELECT 1# 连接泄露检测超过此时间未归还连接则打印警告leak-detection-threshold:5000关于连接池大小的常见误区更大不等于更好。连接数过多会导致数据库端上下文切换开销增大反而变慢。HikariCP 官方推荐的公式connections (core_count × 2) effective_spindle_count。第三步开启连接泄露检测设置leak-detection-threshold后若某段代码拿了连接但没有归还事务未提交/回滚、连接未关闭会在日志里打印WARN HikariPool - Connection leak detection triggered for com.example.service.OrderService.getOrderList(), stack trace follows...顺藤摸瓜我们还发现了一处事务忘记声明Transactional导致的连接未释放// ❌ 问题代码手动获取连接后没有 try-finally 保证关闭publicListOrdergetOrdersManual(){ConnectionconndataSource.getConnection();// 拿了连接// ... 业务逻辑 ...// 如果中间抛异常conn.close() 不会执行conn.close();}// ✅ 正确用 try-with-resources 保证关闭publicListOrdergetOrdersFixed(){try(ConnectionconndataSource.getConnection()){// ... 业务逻辑 ...}// 自动关闭}第四步加监控告警防复发暴露 HikariCP 的 Actuator 指标management:endpoints:web:exposure:include:health,metrics关键指标# 连接池当前活跃连接数 hikaricp_connections_active{poolHikariPool-1} # 等待获取连接的线程数这个飙升就是危险信号 hikaricp_connections_pending{poolHikariPool-1} # 连接获取超时次数大于 0 就应该告警 hikaricp_connections_timeout_total{poolHikariPool-1}在 Grafana/Prometheus 中对connections_pending 3设置告警在问题影响用户前就能感知。举一反三连接池问题的常见触发模式场景表现解法慢 SQL 突增高峰期偶发超时加索引优化 SQLN1 查询接口越来越慢改为 JOIN 或批量查询事务粒度过大连接长时间不释放缩短事务范围避免事务内做 HTTP 调用连接泄露连接数只增不减leak-detection-threshold定位后修复数据库重启后连接失效服务重启后才恢复配置keepaliveTime或健康检查总结这次问题表面上是连接池配置不合理但根本原因是慢查询。连接池被打满通常是结果而不是原因——遇到这类问题先查慢查询再调连接池最后加监控。排查路径总结500 错误 → 确认是连接超时还是业务异常 → 连接超时 → 看慢查询日志 → 有慢查询 → EXPLAIN ANALYZE → 加索引 → 没慢查询 → 检查连接泄露 → leak-detection-threshold → 都正常 → 调大连接池 加监控生产环境调整连接池配置前务必先在压测环境验证避免调参不当适得其反。