数据库连接池调优实战——HikariCP 核心参数、连接泄漏排查、监控指标
数据库连接池调优实战——HikariCP 核心参数、连接泄漏排查、监控指标
适读人群:负责生产环境稳定性、遇到过连接池耗尽或超时问题的后端工程师 | 阅读时长:约16分钟 | 核心价值:掌握 HikariCP 的调优方法论,建立连接池的完整监控体系
"获取连接超时"背后的故事
大概三年前,我帮一个做 B2B 采购的团队做过一次性能排查。他们的问题很典型:每天下午 2 点到 4 点,系统会出现大量报错:
Unable to acquire JDBC Connection;
nested exception is java.sql.SQLTransientConnectionException:
HikariPool-1 - Connection is not available, request timed out after 30000ms其他时间完全正常,就这两小时特别。
我问他们下午 2 点到 4 点有什么特殊:每天这个时间段,采购部门会做大批量的数据导出,一个导出任务要跑 5-10 分钟,持有数据库连接不释放。正好赶上下午的业务高峰,连接池全部被导出任务占满,正常请求拿不到连接,超时。
解决方案有两个维度:一是把导出任务改为异步(先生成任务,异步执行,执行完通知下载),二是给连接池设置合理的参数,对长时间持有连接的任务做监控和告警。
今天我们系统讲一下 HikariCP 的参数调优和连接泄漏排查。
一、HikariCP 核心参数详解
HikariCP 是 Spring Boot 2.x+ 默认的连接池,以极简的代码和极高的性能著称。但"默认配置"在生产环境并不总是最优的。
1.1 连接数配置
spring:
datasource:
hikari:
# 连接池大小
minimum-idle: 5 # 最小空闲连接数(默认等于 maximum-pool-size)
maximum-pool-size: 20 # 最大连接数(默认 10)
# 超时配置
connection-timeout: 3000 # 获取连接的最大等待时间(ms),默认 30000
idle-timeout: 600000 # 空闲连接存活时间(ms),默认 600000(10分钟)
max-lifetime: 1800000 # 连接最大存活时间(ms),默认 1800000(30分钟)
# 连接有效性检测
connection-test-query: SELECT 1 # 验证连接是否有效的 SQL(MySQL 不需要设置)
keepalive-time: 60000 # 保活检查间隔(ms),HikariCP 5.0+
# 连接池名称(方便监控)
pool-name: OrderServiceHikariCP
# 连接泄漏检测(重要!)
leak-detection-threshold: 5000 # 连接持有超过 5 秒则输出警告日志1.2 连接数怎么设置
这是最常见的问题。很多人想当然地把连接数设置得很大(比如 200),认为"连接越多越快"。这是错的。
数据库连接的本质是服务端线程。MySQL 每个连接对应一个服务端线程,线程本身有内存开销(默认每个线程 256KB 栈内存 + 查询缓冲区),并且 CPU 调度多个线程有上下文切换开销。
连接数 = CPU 核数 × 2 + 有效磁盘数。这是 HikariCP 官方给出的公式,适合 OLTP 场景。如果你的数据库服务器是 8 核,公式给出的连接数大约是 17-20。
实际上,我见过很多团队把最大连接数设为 200,结果数据库服务器 CPU 因为线程调度开销反而更高,性能不升反降。
我的建议:从小开始,maximum-pool-size = CPU核数 × 2,用压测验证,根据实际 TPS 和响应时间指标调整,而不是凭感觉设大。
1.3 max-lifetime 为什么不能设太大
MySQL 默认的 wait_timeout 是 8 小时(28800 秒),超过这个时间没有活动的连接会被服务器关闭。
如果你的 max-lifetime 设置为 30 分钟,连接池在 30 分钟内会主动轮换连接,不会触发 MySQL 的超时关闭。
但如果 max-lifetime 设置超过 wait_timeout(或者没设置),MySQL 已经关闭了这个连接,但连接池还认为它是有效的,下次使用时会报 Communications link failure。
踩坑一:"The last packet sent to the server was X seconds ago"
现象:应用运行一段时间后(通常是半夜流量低谷过后),早上第一批请求报 Communications link failure。
原因:连接池里的空闲连接在低谷期超过了 MySQL 的 wait_timeout(8 小时),MySQL 关闭了连接,但连接池不知道。
解法:确保 max-lifetime < MySQL wait_timeout,推荐设置为 30 分钟(1800000ms),远小于 MySQL 默认的 8 小时。
二、连接泄漏:最难排查的问题
连接泄漏(Connection Leak)是指代码获取了数据库连接,但因为异常或逻辑错误没有正确释放,导致连接池连接数不断减少,最终耗尽。
2.1 开启泄漏检测
spring:
datasource:
hikari:
leak-detection-threshold: 5000 # 连接持有超过 5 秒输出警告开启后,如果一个连接被持有超过 5 秒(通常是正常业务不需要这么长时间的),HikariCP 会打出类似如下的日志:
WARN c.z.h.pool.ProxyLeakTask - Connection leak detection triggered for
com.example.service.OrderService.createOrder(OrderService.java:45),
stack trace follows:
java.lang.Exception: Apparent connection leak detected
at com.example.service.OrderService.createOrder(OrderService.java:45)
at ...日志里有完整的调用栈,定位到代码行,一目了然。
踩坑二:在循环里用 JdbcTemplate 执行大量更新
// 连接泄漏示例:不是真正的泄漏,但持有连接时间过长
@Transactional
public void batchUpdateOrders(List<Long> orderIds) {
for (Long orderId : orderIds) {
// 每次循环都在同一个事务内执行,事务持有连接不释放
// 如果 orderIds 有 10000 个,这个事务可能持续几分钟
jdbcTemplate.update("UPDATE orders SET status=2 WHERE id=?", orderId);
}
// 事务结束,连接释放
// 在这几分钟内,这个连接一直被占用
}
// 正确做法:用批量更新
@Transactional
public void batchUpdateOrders(List<Long> orderIds) {
// 批量 SQL,一次执行完成
String sql = "UPDATE orders SET status=2 WHERE id IN (" +
orderIds.stream().map(id -> "?").collect(Collectors.joining(",")) + ")";
jdbcTemplate.update(sql, orderIds.toArray());
}踩坑三:手动获取连接后忘记关闭
// 错误:手动获取连接但没有在 finally 里关闭
public void directJdbcOperation() throws SQLException {
DataSource ds = ...; // 获取 DataSource
Connection conn = ds.getConnection(); // 从连接池获取连接
try {
PreparedStatement ps = conn.prepareStatement("...");
ps.execute();
// 没有 conn.close()!连接泄漏!
} catch (SQLException e) {
// 异常时也没关闭连接
throw e;
}
}
// 正确:使用 try-with-resources 确保关闭
public void directJdbcOperation() throws SQLException {
DataSource ds = ...;
try (Connection conn = ds.getConnection();
PreparedStatement ps = conn.prepareStatement("...")) {
ps.execute();
}
// try 块结束,自动调用 conn.close(),归还到连接池
}三、监控指标:建立连接池的可观测性
3.1 暴露 HikariCP 监控指标
HikariCP 集成了 Micrometer,通过 Actuator 可以暴露连接池的实时指标:
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
metrics:
export:
prometheus:
enabled: true关键监控指标:
| 指标 | Micrometer Key | 告警阈值建议 |
|---|---|---|
| 活跃连接数 | hikaricp.connections.active | > max_pool_size × 0.8 |
| 等待获取连接数 | hikaricp.connections.pending | > 0(有等待就要关注) |
| 连接获取超时次数 | hikaricp.connections.timeout.total | > 0(出现超时立刻告警) |
| 连接获取耗时 | hikaricp.connections.acquire | P99 > 100ms |
| 连接使用耗时 | hikaricp.connections.usage | P99 > 2000ms(说明有长事务) |
// 在 Java 代码中读取连接池指标(用于自定义监控)
@Component
public class ConnectionPoolMonitor {
@Autowired
private MeterRegistry meterRegistry;
@Scheduled(fixedRate = 30000) // 每 30 秒检查一次
public void checkPoolHealth() {
// 获取活跃连接数
Gauge activeConnections = meterRegistry.find("hikaricp.connections.active")
.tag("pool", "OrderServiceHikariCP")
.gauge();
if (activeConnections != null) {
double active = activeConnections.value();
if (active > 16) { // 超过 max_pool_size 的 80%
log.warn("连接池使用率过高:活跃连接={}/20", (int)active);
// 触发告警通知
}
}
// 检查超时次数(用 Counter)
Counter timeoutCounter = meterRegistry.find("hikaricp.connections.timeout.total")
.counter();
if (timeoutCounter != null && timeoutCounter.count() > 0) {
log.error("连接获取超时!累计次数={}", (long)timeoutCounter.count());
}
}
}四、多数据源场景
生产中常见读写分离的多数据源配置,每个数据源有独立的连接池,参数要分别调优:
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.master.hikari")
public HikariDataSource masterDataSource() {
HikariConfig config = new HikariConfig();
config.setPoolName("MasterPool");
config.setMaximumPoolSize(20);
config.setLeakDetectionThreshold(5000);
return new HikariDataSource(config);
}
@Bean
@ConfigurationProperties("spring.datasource.slave.hikari")
public HikariDataSource slaveDataSource() {
HikariConfig config = new HikariConfig();
config.setPoolName("SlavePool");
config.setMaximumPoolSize(40); // 读库连接数可以更多
config.setReadOnly(true);
config.setLeakDetectionThreshold(5000);
return new HikariDataSource(config);
}
}连接池是数据库层面的"第一道防线",参数不对,再好的 SQL 优化也是白费。建立对连接池指标的实时监控,比事后救火要值钱得多。
