数据库连接池耗尽故障排查实战

一、故障现象

数据库连接池耗尽是生产环境中常见且严重的故障,典型现象包括:

  • 应用侧:接口超时、请求堆积、大量 ConnectionPoolTimeoutException
  • 数据库侧:连接数接近上限、新连接建立失败、慢查询增多
  • 监控告警:连接池使用率 > 90%、活跃连接数异常、等待队列堆积
1
2
3
4
5
6
典型错误日志:
Caused by: java.sql.SQLTransientConnectionException:
HikariPool-1 - Connection is not available, request timed out after 30000ms.

Caused by: com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator$1:
Connection acquisition timeout after 30000 ms

二、故障影响

影响维度 具体表现
业务影响 接口超时、用户请求失败、交易中断
系统影响 线程池阻塞、内存增长、CPU 升高
数据库影响 连接数爆满、锁等待、性能下降
连锁反应 服务雪崩、级联故障、数据不一致

三、快速诊断流程

3.1 诊断流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
                ┌─────────────────┐
│ 发现连接池告警 │
└────────┬────────┘

┌─────────────────┐
│ 检查连接池状态 │
└────────┬────────┘

┌──────────────┴──────────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 连接数是否超限?│ │ 是否有连接泄漏?│
└────────┬────────┘ └────────┬────────┘
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 检查数据库连接数 │ │ 检查长事务/未关闭│
└────────┬────────┘ └────────┬────────┘
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ 慢查询/锁等待? │ │ 代码修复 + 重启 │
└────────┬────────┘ └─────────────────┘

┌─────────────────┐
│ 扩容/优化/限流 │
└─────────────────┘

3.2 应用侧诊断命令

1
2
3
4
5
6
7
8
9
10
11
12
13
# 1. 查看应用连接池指标(以 HikariCP 为例)
curl -s http://localhost:8080/actuator/metrics/hikaricp.connections.active
curl -s http://localhost:8080/actuator/metrics/hikaricp.connections.pending
curl -s http://localhost:8080/actuator/metrics/hikaricp.connections.max

# 2. 查看 JVM 线程状态
jstack <PID> | grep -A 30 "pool-" | head -100

# 3. 查找等待数据库连接的线程
jstack <PID> | grep -B 5 "HikariPool" | grep -A 5 "WAITING"

# 4. 查看连接池配置
java -jar /opt/app/app.jar --spring.config.location=file:/opt/app/application.yml

3.3 数据库侧诊断命令

MySQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-- 查看当前连接数
SHOW STATUS LIKE 'Threads_connected';
SHOW VARIABLES LIKE 'max_connections';

-- 查看连接来源
SELECT host, db, user, command, state, time, info
FROM information_schema.processlist
ORDER BY time DESC
LIMIT 20;

-- 查看各用户连接数
SELECT user, host, COUNT(*) as conn_count
FROM information_schema.processlist
GROUP BY user, host
ORDER BY conn_count DESC;

-- 查看锁等待
SELECT * FROM information_schema.innodb_lock_waits;
SELECT * FROM performance_schema.data_lock_waits;

-- 查看长事务
SELECT * FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 60;

PostgreSQL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 查看当前连接数
SELECT count(*) FROM pg_stat_activity;
SHOW max_connections;

-- 查看连接详情
SELECT pid, usename, datname, client_addr, application_name,
state, wait_event_type, wait_event, query, query_start
FROM pg_stat_activity
ORDER BY query_start
LIMIT 20;

-- 查看锁等待
SELECT * FROM pg_locks WHERE NOT granted;

-- 查看长事务
SELECT pid, now() - pg_stat_activity.query_start AS duration, query
FROM pg_stat_activity
WHERE (now() - pg_stat_activity.query_start) > interval '1 minute'
ORDER BY duration DESC;

四、常见原因分析

4.1 连接泄漏(最常见)

现象:连接数持续增长,不释放

原因

  • 代码中未正确关闭 Connection/Statement/ResultSet
  • 异常处理不当,finally 块未执行关闭
  • 使用 try-with-resources 不规范

排查方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 错误示例 - 连接泄漏
public void leakExample() throws SQLException {
Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users");
// 忘记关闭资源!
}

// 正确示例 - try-with-resources
public void properExample() throws SQLException {
try (Connection conn = dataSource.getConnection();
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("SELECT * FROM users")) {
while (rs.next()) {
// 处理结果
}
} // 自动关闭资源
}

HikariCP 泄漏检测

1
2
3
4
5
6
7
# application.yml
spring:
datasource:
hikari:
leak-detection-threshold: 60000 # 60 秒未归还则记录日志
connection-timeout: 30000
max-lifetime: 1800000

4.2 慢查询导致连接占用

现象:连接数正常,但活跃连接占比高,大量连接处于执行状态

排查方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- MySQL 慢查询日志
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';

-- 查看当前执行的慢查询
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 5
ORDER BY time DESC;

-- PostgreSQL
SELECT pid, query, now() - query_start AS duration
FROM pg_stat_activity
WHERE state = 'active' AND now() - query_start > interval '5 seconds';

解决方案

  • 优化慢查询(添加索引、改写 SQL)
  • 设置查询超时:statement_timeout
  • 增加连接池大小(临时方案)

4.3 事务过长

现象:连接持有时间长,事务未提交

排查方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- MySQL 长事务
SELECT trx_id, trx_state, trx_started, trx_mysql_thread_id,
trx_query, trx_operation_state, trx_tables_in_use,
trx_tables_locked, trx_lock_structs, trx_lock_rows,
trx_isolation_level
FROM information_schema.innodb_trx
WHERE TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 30;

-- PostgreSQL 长事务
SELECT pid, usename, datname, client_addr,
now() - xact_start AS transaction_duration,
state, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL
AND now() - xact_start > interval '1 minute'
ORDER BY transaction_duration DESC;

解决方案

  • 拆分大事务
  • 避免在事务中调用外部 API
  • 设置事务超时

4.4 连接池配置不当

现象:连接池大小设置过小,无法应对流量高峰

排查方法

1
2
3
4
5
6
7
8
9
10
# 检查当前配置
spring:
datasource:
hikari:
maximum-pool-size: 10 # 是否过小?
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
connection-test-query: SELECT 1

推荐配置(参考 HikariCP 官方建议):

1
2
3
4
5
6
7
8
9
10
11
12
spring:
datasource:
hikari:
# 连接池大小 = CPU 核心数 * 2 + 1 到 CPU 核心数 * 2 + 磁盘数
maximum-pool-size: 20 # 根据实际负载调整
minimum-idle: 10 # 保持最小空闲连接
connection-timeout: 30000 # 30 秒获取连接超时
idle-timeout: 600000 # 10 分钟空闲超时
max-lifetime: 1800000 # 30 分钟最大生命周期
connection-timeout: 30000
validation-timeout: 5000
leak-detection-threshold: 60000

4.5 数据库连接数上限

现象:应用连接池正常,但数据库拒绝新连接

排查方法

1
2
3
4
5
6
7
8
-- MySQL
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Max_used_connections';
SELECT @@max_connections, @@max_user_connections;

-- PostgreSQL
SHOW max_connections;
SELECT * FROM pg_stat_activity WHERE datname = current_database();

解决方案

  • 调整数据库最大连接数
  • 使用连接池中间件(PgBouncer、ProxySQL)
  • 应用层限流

五、应急响应方案

5.1 紧急恢复步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 步骤 1:确认故障范围
# - 单应用还是多应用?
# - 单数据库实例还是集群?

# 步骤 2:临时扩容(如可能)
# MySQL
mysql -e "SET GLOBAL max_connections = 1000;"

# 步骤 3:杀死长事务/慢查询
# MySQL
mysql -e "KILL <thread_id>;"

# PostgreSQL
psql -c "SELECT pg_terminate_backend(<pid>);"

# 步骤 4:重启应用(最后手段)
systemctl restart application

# 步骤 5:限流降级
# 启用熔断器,限制数据库请求

5.2 连接池临时调整

1
2
3
4
5
# 临时增加连接池大小(需重启应用)
spring:
datasource:
hikari:
maximum-pool-size: 50 # 临时调大
1
2
3
4
// 动态调整(部分连接池支持)
HikariConfig config = new HikariConfig();
config.setMaximumPoolSize(50);
HikariDataSource ds = new HikariDataSource(config);

5.3 数据库侧应急

1
2
3
4
5
6
7
8
9
10
11
12
-- MySQL 紧急增加连接数
SET GLOBAL max_connections = 1000;
-- 注意:需要重启才能永久生效

-- 杀死所有空闲连接
SELECT CONCAT('KILL ', id, ';')
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 300;

-- PostgreSQL 限制普通用户连接
ALTER DATABASE mydb WITH CONNECTION LIMIT 50;
-- 保留 superuser 连接用于排查

六、监控告警配置

6.1 关键监控指标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 连接池使用率
hikaricp_connections_active / hikaricp_connections_max

# 连接等待队列
hikaricp_connections_pending

# 连接获取延迟
hikaricp_connections_acquire_seconds

# 数据库连接数
mysql_global_status_threads_connected / mysql_global_variables_max_connections

# 慢查询数量
mysql_global_status_slow_queries

# 长事务数量
mysql_info_schema_innodb_trx_trx_state_count{trx_state="RUNNING"}

6.2 告警规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# Prometheus 告警规则
groups:
- name: database-connection
rules:
- alert: ConnectionPoolHighUsage
expr: hikaricp_connections_active / hikaricp_connections_max > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "连接池使用率过高"
description: "{{ $labels.instance }} 连接池使用率 {{ $value | humanizePercentage }}"

- alert: ConnectionPoolExhausted
expr: hikaricp_connections_pending > 0
for: 2m
labels:
severity: critical
annotations:
summary: "连接池耗尽"
description: "{{ $labels.instance }}{{ $value }} 个连接在等待"

- alert: DatabaseConnectionHigh
expr: mysql_global_status_threads_connected / mysql_global_variables_max_connections > 0.9
for: 5m
labels:
severity: warning
annotations:
summary: "数据库连接数过高"
description: "{{ $labels.instance }} 连接数使用率 {{ $value | humanizePercentage }}"

- alert: LongRunningTransaction
expr: mysql_info_schema_innodb_trx_trx_started_timestamp < time() - 300
for: 5m
labels:
severity: warning
annotations:
summary: "发现长事务"
description: "{{ $labels.instance }} 有事务运行超过 5 分钟"

6.3 Grafana 仪表盘

建议监控面板包含:

  • 连接池使用率趋势图
  • 活跃/空闲/等待连接数
  • 连接获取延迟 P95/P99
  • 数据库连接数 vs 上限
  • 慢查询数量趋势
  • 长事务列表

七、预防措施

7.1 代码层面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 1. 始终使用 try-with-resources
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement(sql)) {
// 业务逻辑
}

// 2. 设置合理的超时
@Configuration
public class DataSourceConfig {
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setConnectionTimeout(30000);
config.setMaxLifetime(1800000);
config.setLeakDetectionThreshold(60000);
return new HikariDataSource(config);
}
}

// 3. 避免在事务中调用外部服务
@Transactional
public void badExample() {
// 数据库操作
repository.save(entity);
// 错误:在事务中调用 HTTP 接口
restTemplate.getForObject("http://external/api", String.class);
}

// 4. 使用连接池健康检查
@Bean
public DataSourceHealthIndicator dataSourceHealthIndicator(DataSource dataSource) {
return new DataSourceHealthIndicator(dataSource);
}

7.2 配置层面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 推荐的生产环境配置
spring:
datasource:
hikari:
# 连接池大小
maximum-pool-size: 20
minimum-idle: 10

# 超时配置
connection-timeout: 30000
validation-timeout: 5000
idle-timeout: 600000
max-lifetime: 1800000

# 泄漏检测
leak-detection-threshold: 60000

# 连接测试
connection-test-query: SELECT 1

# 指标注册(用于监控)
register-mbeans: true

jpa:
properties:
hibernate:
# 语句超时(秒)
jdbc:
statement_fetch_size: 100
batch_versioned_data: true

7.3 架构层面

  1. 读写分离:将读请求分发到从库
  2. 分库分表:降低单库连接压力
  3. 连接池中间件:使用 PgBouncer/ProxySQL
  4. 服务降级:配置熔断器(Hystrix/Resilience4j)
  5. 限流保护:限制并发数据库请求数

八、故障复盘模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 数据库连接池耗尽故障复盘

## 故障信息
- 故障时间:YYYY-MM-DD HH:MM
- 恢复时间:YYYY-MM-DD HH:MM
- 持续时长:XX 分钟
- 影响范围:XX 服务、XX 用户

## 故障现象
(描述监控告警、用户反馈、错误日志等)

## 根本原因
(5 Why 分析法找出根本原因)

## 处理过程
1. HH:MM 发现告警
2. HH:MM 开始排查
3. HH:MM 定位原因
4. HH:MM 实施修复
5. HH:MM 验证恢复

## 改进措施
- [ ] 短期:立即修复的问题
- [ ] 中期:需要开发排期的优化
- [ ] 长期:架构层面的改进

## 经验教训
(值得总结的经验)

九、总结

数据库连接池耗尽故障的排查要点:

  1. 快速定位:区分是应用侧还是数据库侧问题
  2. 找准原因:连接泄漏、慢查询、长事务、配置不当
  3. 应急恢复:杀死长事务、临时扩容、重启应用
  4. 监控预防:建立完善的监控告警体系
  5. 代码规范:正确使用连接池,避免资源泄漏

记住:连接池不是越大越好,合理配置 + 规范代码 + 完善监控才是王道。