摘要:从一次"应用并发上来后数据库响应变慢10倍"的性能故障出发,深度剖析MySQL连接池配置不当导致的性能问题。通过连接池排队、连接耗尽、以及连接创建开销的真实案例,揭秘为什么max_connections设置过小会成为瓶颈、HikariCP连接池的最佳实践、以及如何监控连接池状态。配合时序图展示连接等待流程,给出不同并发场景下的连接数计算公式。
💥 翻车现场
周二下午,哈吉米的应用上线了。
压测结果(单用户):
接口:/api/order/create
响应时间:50ms ✅
数据库查询时间:5ms
压测结果(100并发):
接口响应时间:500ms ❌(慢了10倍)
数据库查询时间:5ms(没变)
问题:数据库查询时间没变,为什么接口变慢了?
哈吉米:"数据库查询才5ms,怎么接口要500ms?"
查看监控:
应用监控:
- 活跃线程:100
- 数据库连接池等待时间:495ms ← 问题在这
数据库监控:
- 当前连接数:10
- 查询响应时间:5ms
哈吉米:"连接池等待了495ms?为什么?"
查看配置:
# application.yml
spring:
datasource:
hikari:
maximum-pool-size: 10 # 最大连接数:10
minimum-idle: 5
问题分析:
场景:
1. 100个请求并发进来
2. 每个请求需要1个数据库连接
3. 连接池只有10个连接
4. 前10个请求立即获取连接,执行查询(5ms)
5. 后90个请求等待连接释放(排队)
时间线:
T0: 100个请求进来
T1: 前10个请求获取连接,开始查询
T5: 前10个请求查询完成,释放连接
T6: 后10个请求获取连接,开始查询
T11: 后10个请求查询完成,释放连接
...
T495: 最后10个请求查询完成
平均等待时间:495ms / 2 ≈ 250ms
平均响应时间:250ms(等待) + 5ms(查询) = 255ms
哈吉米:"卧槽,连接池太小了,大家在排队等连接!"
南北绿豆和阿西噶阿西来了。
南北绿豆:"连接池是性能的关键,配置不当会导致严重的性能问题!"
阿西噶阿西:"来,我给你讲讲连接数的配置原理。"
🤔 为什么连接数过小会慢?
连接池的工作原理
阿西噶阿西在白板上画了一个图。
连接池(最大10个连接):
┌─────────────────────────────┐
│ 连接池(HikariCP) │
│ │
│ [conn1] [conn2] [conn3] │
│ [conn4] [conn5] [conn6] │
│ [conn7] [conn8] [conn9] │
│ [conn10] │
│ │
│ 等待队列: │
│ [请求11] [请求12] [请求13] │
│ [请求14] ... [请求100] │ ← 90个请求在等待
└─────────────────────────────┘
连接等待的时序图
sequenceDiagram
participant Req1 as 请求1
participant Req11 as 请求11
participant Pool as 连接池(10个连接)
participant DB as MySQL
par 前10个请求立即获取连接
Req1->>Pool: 1. 获取连接
Pool->>Req1: 2. 返回conn1 ✅
Req1->>DB: 3. 查询(5ms)
DB->>Req1: 4. 返回数据
Req1->>Pool: 5. 归还conn1
end
Req11->>Pool: 6. 获取连接(T0时刻)
Note over Pool: 连接池已满,等待
Note over Req11: 阻塞等待连接释放...
Note over Req1: 5ms后释放连接
Pool->>Req11: 7. 返回conn1(T5时刻)
Note over Req11: 等待时间:5ms
Req11->>DB: 8. 查询(5ms)
DB->>Req11: 9. 返回数据
Note over Req11: 总耗时:5ms(等待) + 5ms(查询) = 10ms
南北绿豆:"看到了吗?连接数不够,请求会排队等待连接,导致响应时间暴增。"
🎯 连接数应该设置多少?
计算公式
阿西噶阿西:"连接数的设置有公式。"
公式:
连接数 = 并发请求数 × 单次查询时间 / 查询间隔
简化公式(QPS维度):
连接数 = 峰值QPS × 平均查询时间
示例:
峰值QPS:1000
平均查询时间:0.01秒(10ms)
连接数 = 1000 × 0.01 = 10
但要考虑峰值和突发:
推荐连接数 = 10 × 2 = 20
HikariCP官方推荐
HikariCP作者的建议:
连接数 = ((核心数 × 2) + 磁盘数)
示例:
CPU:8核
磁盘:1个SSD
连接数 = (8 × 2) + 1 = 17
理由:
- CPU密集:核心数即可
- IO密集(数据库查询):核心数 × 2
- 磁盘IO:+1
不同场景的推荐值
| 场景 | QPS | 平均查询时间 | 推荐连接数 |
|---|---|---|---|
| 低并发 | 100 | 10ms | 10-20 |
| 中并发 | 1000 | 10ms | 20-50 |
| 高并发 | 10000 | 10ms | 100-200 |
| 超高并发 | 100000 | 10ms | 1000+ |
注意:
连接数不是越多越好:
1. MySQL有最大连接数限制(max_connections)
2. 每个连接占用内存(约256KB)
3. 连接过多,MySQL调度开销大
推荐:
- 应用连接池:20-100
- MySQL max_connections:500-1000
🎯 MySQL max_connections的设置
查看和设置
-- 查看当前最大连接数
SHOW VARIABLES LIKE 'max_connections';
+------------------+-------+
| Variable_name | Value |
+------------------+-------+
| max_connections | 151 | ← 默认151
+------------------+-------+
-- 查看当前连接数
SHOW STATUS LIKE 'Threads_connected';
+-------------------+-------+
| Variable_name | Value |
+-------------------+-------+
| Threads_connected | 125 | ← 当前125个连接
+-------------------+-------+
-- 临时设置(重启后失效)
SET GLOBAL max_connections = 500;
-- 永久设置(my.cnf)
[mysqld]
max_connections = 500
max_connections设置不当的问题
场景1:设置过小
max_connections = 100
10台应用服务器:
每台连接池:20个连接
总连接数:10 × 20 = 200
问题:
200 > 100(max_connections)
→ 部分应用连接不上
→ ERROR 1040: Too many connections ❌
场景2:设置过大
max_connections = 10000
问题:
1. 每个连接占用内存:256KB
2. 总内存:10000 × 256KB = 2.5GB
3. 连接过多,MySQL调度开销大
4. 性能反而下降 ❌
推荐配置
应用数量 × 每个应用的连接池大小 < MySQL max_connections × 0.8
示例:
10台应用服务器
每台连接池:50
MySQL max_connections = 10 × 50 / 0.8 = 625
推荐设置:
max_connections = 1000(留余量)
🎯 HikariCP连接池配置
核心参数
spring:
datasource:
hikari:
# 最大连接数
maximum-pool-size: 50
# 最小空闲连接数
minimum-idle: 10
# 连接超时时间(获取连接的超时)
connection-timeout: 30000 # 30秒
# 空闲连接存活时间
idle-timeout: 600000 # 10分钟
# 连接最大存活时间
max-lifetime: 1800000 # 30分钟
# 连接测试查询(验证连接是否有效)
connection-test-query: SELECT 1
参数详解
maximum-pool-size(最大连接数):
作用:连接池最多能创建多少个连接
设置:
- 根据QPS和查询时间计算
- 不要超过MySQL max_connections的80%
示例:
峰值QPS=1000,查询时间=10ms
maximum-pool-size = 1000 × 0.01 × 2 = 20
minimum-idle(最小空闲连接):
作用:连接池维持的最少空闲连接数
设置:
- 通常设置为maximum-pool-size的20-50%
- 避免突发流量时临时创建连接
示例:
maximum-pool-size = 50
minimum-idle = 10(20%)
connection-timeout(连接超时):
作用:等待连接的最大时间
场景:
1. 请求获取连接
2. 连接池已满,等待
3. 等待30秒后,仍没有空闲连接
4. 抛异常:HikariPool - Connection is not available
推荐:
- 短连接:3-10秒
- 长连接:10-30秒
🎯 监控连接池状态
监控关键指标
@Component
public class HikariPoolMonitor {
@Autowired
private HikariDataSource dataSource;
@Scheduled(fixedDelay = 10000) // 每10秒
public void monitor() {
HikariPoolMXBean poolMXBean = dataSource.getHikariPoolMXBean();
int totalConnections = poolMXBean.getTotalConnections(); // 总连接数
int activeConnections = poolMXBean.getActiveConnections(); // 活跃连接数
int idleConnections = poolMXBean.getIdleConnections(); // 空闲连接数
int waitingThreads = poolMXBean.getThreadsAwaitingConnection(); // 等待连接的线程数
log.info("连接池状态 - 总:{}, 活跃:{}, 空闲:{}, 等待:{}",
totalConnections, activeConnections, idleConnections, waitingThreads);
// 告警
if (waitingThreads > 0) {
log.warn("有{}个线程在等待连接,连接池可能不够", waitingThreads);
}
if (activeConnections == totalConnections) {
log.warn("连接池已满负荷运行");
}
}
}
监控指标
| 指标 | 正常值 | 异常值 | 说明 |
|---|---|---|---|
| 活跃连接数 | < 最大连接数的80% | >= 最大连接数 | 连接不够 |
| 空闲连接数 | > 0 | = 0 | 连接全部被占用 |
| 等待线程数 | 0 | > 0 | 有请求在等待连接 |
| 连接获取时间 | < 10ms | > 100ms | 连接池压力大 |
🎯 连接池配置的最佳实践
实践1:根据并发量设置
# 低并发(QPS < 100)
hikari:
maximum-pool-size: 10
minimum-idle: 5
# 中并发(QPS 100-1000)
hikari:
maximum-pool-size: 50
minimum-idle: 10
# 高并发(QPS > 1000)
hikari:
maximum-pool-size: 200
minimum-idle: 50
实践2:连接超时设置合理
hikari:
connection-timeout: 10000 # 10秒
# 如果10秒还拿不到连接,说明:
# 1. 连接池太小
# 2. 数据库查询太慢
# 3. 出现了慢查询或死锁
实践3:监控和告警
// Prometheus监控
@Bean
public MeterBinder hikariMetrics(HikariDataSource dataSource) {
return new HikariCPMetrics(dataSource);
}
// Grafana配置告警:
// - 活跃连接数 > 最大连接数的80%
// - 等待线程数 > 0
// - 连接获取时间 > 100ms
实践4:多数据源独立连接池
// 主库连接池(写)
@Bean
@Primary
public DataSource masterDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://master:3306/db");
config.setMaximumPoolSize(50); // 写操作多
return new HikariDataSource(config);
}
// 从库连接池(读)
@Bean
public DataSource slaveDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://slave:3306/db");
config.setMaximumPoolSize(100); // 读操作更多
return new HikariDataSource(config);
}
🎯 MySQL端的max_connections
应用连接池 vs MySQL max_connections
关系:
应用连接池总数 < MySQL max_connections × 0.8
示例:
10台应用服务器
每台连接池:50
总连接数:10 × 50 = 500
MySQL max_connections >= 500 / 0.8 = 625
推荐设置:
max_connections = 1000
查看MySQL连接状态
-- 当前连接数
SHOW STATUS LIKE 'Threads_connected';
-- 历史最大连接数
SHOW STATUS LIKE 'Max_used_connections';
-- 连接使用率
SELECT
@@max_connections AS max_conn,
(SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Threads_connected') AS current_conn,
ROUND((SELECT VARIABLE_VALUE FROM performance_schema.global_status
WHERE VARIABLE_NAME = 'Threads_connected') / @@max_connections * 100, 2) AS usage_pct;
+----------+--------------+-----------+
| max_conn | current_conn | usage_pct |
+----------+--------------+-----------+
| 500 | 423 | 84.60 | ← 使用率84.6%,接近上限
+----------+--------------+-----------+
🎓 面试标准答案
题目:为什么MySQL连接数过小会导致查询变慢?
答案:
原因:
-
连接池排队
- 并发请求 > 连接池大小
- 请求等待空闲连接
- 等待时间 >> 查询时间
-
示例:
- 100并发请求
- 连接池只有10个连接
- 前10个立即执行(5ms)
- 后90个排队等待(平均250ms)
- 总响应时间:250ms + 5ms = 255ms
-
连接创建开销
- 如果连接池太小,频繁创建/销毁连接
- 创建连接耗时:50-200ms
- 影响性能
如何设置:
公式:
连接数 = 峰值QPS × 平均查询时间 × 2
或
连接数 = (CPU核心数 × 2) + 磁盘数
推荐值:
- 低并发:10-20
- 中并发:20-50
- 高并发:50-200
注意:
- 应用连接池总数 < MySQL max_connections × 0.8
- 监控等待线程数(> 0说明连接不够)
- 连接不是越多越好(MySQL有上限)
题目:HikariCP的核心参数如何配置?
答案:
核心参数:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| maximum-pool-size | 20-100 | 根据并发量设置 |
| minimum-idle | maximum的20-50% | 保持一定空闲连接 |
| connection-timeout | 10000-30000ms | 等待连接的超时时间 |
| idle-timeout | 600000ms(10分钟) | 空闲连接存活时间 |
| max-lifetime | 1800000ms(30分钟) | 连接最大存活时间 |
配置示例:
spring:
datasource:
hikari:
maximum-pool-size: 50
minimum-idle: 10
connection-timeout: 10000
idle-timeout: 600000
max-lifetime: 1800000
监控指标:
- 活跃连接数
- 等待线程数
- 连接获取时间
🎉 结束语
一周后,哈吉米把连接池调大到50。
哈吉米:"连接池从10改成50后,100并发的响应时间从500ms降到60ms!"
南北绿豆:"对,连接池是性能的关键,配置不当会严重影响性能。"
阿西噶阿西:"记住:连接数要根据并发量设置,太小会排队,太大浪费资源。"
哈吉米:"还要监控等待线程数,大于0说明连接池不够。"
南北绿豆:"对,提前发现问题,比性能下降后再优化强100倍!"
记忆口诀:
连接数过小排队等,请求阻塞响应慢
根据QPS和查询时间算,峰值乘以时间再乘二
HikariCP推荐公式,核心数乘二加磁盘
应用连接池总数,不超MySQL的八成
监控等待线程数,大于零要调大连接池