跳转至

案例:连接数打满,Too many connections 报错

场景背景

  • 时间:2026-06-18 周四 10:30
  • 现象:业务报错:ERROR 1040 (HY000): Too many connections
  • 数据库:MySQL 8.0.32,业务库 jumpmax_connections = 151
  • 环境:电商大促预热期间,流量突增 3 倍
  • 目标:快速恢复业务,找到连接数暴涨原因

生活化比喻:数据库连接数就像餐厅座位。平时 100 个座位,来 80 个客人,很宽松。大促来了 300 人,但门口还排着 100 个没走的(连接没释放),新客人进不来,业务报错 "Too many connections"。


排查步骤

1. 紧急检查当前连接状态(30 秒)

dbskiter --database=jump monitor health

关键输出

连接数使用率: 100.0% (151/151)
活跃线程: 3
空闲线程: 148

线程状态分布:
  - Sleep: 148 个(连接休眠,未释放)
  - Query: 2 个
  - Waiting for table lock: 1 个

关键发现:151 个连接中,148 个是 Sleep 状态。这意味着连接池或应用程序没有正确关闭连接,导致大量空闲连接占满连接池。


2. 查看连接来源和耗时(1 分钟)

dbskiter --database=jump diagnose connections

输出

连接诊断:
====================================
总连接数: 151/151 (100%)
活跃查询: 3
休眠连接: 148

按用户分组:
  - app_user: 142 个(94.0%)
  - monitor: 5 个
  - root: 2 个
  - backup_user: 1 个
  - repl: 1 个

按 host 分组:
  - 10.0.1.50: 45 个(应用服务器 A)
  - 10.0.1.51: 48 个(应用服务器 B)
  - 10.0.1.52: 49 个(应用服务器 C)

休眠连接详情(Top 10):
  - 连接 ID 8823: Sleep, 1800s, app_user@10.0.1.50
  - 连接 ID 8824: Sleep, 1798s, app_user@10.0.1.50
  - ...
  - 148 个连接休眠时间超过 30 秒

关键发现: 1. 142 个连接来自 app_user(应用连接池账号) 2. 3 台应用服务器各 45-49 个连接,分布均匀 3. 休眠时间都超过 1800 秒(30 分钟),明显没释放


3. 检查连接配置和超时设置(30 秒)

dbskiter --database=jump diagnose sql "
SHOW VARIABLES LIKE 'wait_timeout';
SHOW VARIABLES LIKE 'interactive_timeout';
SHOW VARIABLES LIKE 'max_connections';
"

输出

Variable_name          Value
wait_timeout           28800       (8 小时)
interactive_timeout    28800       (8 小时)
max_connections        151

问题定位:wait_timeout = 28800(8 小时)。应用程序的连接池配置最大连接数 50,但连接池里的连接不会主动关闭,数据库侧要等 8 小时才回收。如果应用请求突增,连接池创建新连接,但旧连接不释放,很快就会打满。


4. 应用层排查(配合开发)

通过 dbskiter 的连接信息,让开发检查应用配置:

# 应用服务器 A (10.0.1.50) 连接数 45 个
# 检查应用连接池配置(HikariCP / Druid / c3p0)

典型问题配置

# application.yml(错误示例)
spring.datasource.hikari:
  maximum-pool-size: 50        # 最大 50 个连接
  minimum-idle: 50             # 最小空闲 50 个(等于最大,连接池保持满额)
  idle-timeout: 600000        # 10 分钟才回收空闲连接
  connection-timeout: 30000   # 获取连接等待 30 秒
  max-lifetime: 0             # 不限制连接生命周期(永不过期)

问题: 1. minimum-idle = maximum-pool-size = 50:连接池始终保持 50 个连接,即使没请求也不释放 2. max-lifetime = 0:连接永不过期,数据库 wait_timeout 8 小时,连接池不主动关闭 3. 3 台服务器 × 50 个连接 = 150 个,加上监控和备份连接,刚好 151 个打满


解决方案

紧急恢复(立即生效)

方案 A:杀休眠连接(临时释放,让业务先恢复)

# 查看休眠超过 60 秒的连接
dbskiter --database=jump diagnose sql "
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 60;
"

# 批量杀掉休眠连接(注意:确认是应用连接池的空闲连接后执行)
dbskiter --database=jump diagnose sql "
SELECT GROUP_CONCAT(CONCAT('KILL ', id) SEPARATOR '; ')
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 60 AND user = 'app_user';
"
# 然后执行生成的 KILL 语句

方案 B:临时调大 max_connections(应急)

dbskiter --database=jump diagnose sql "
SET GLOBAL max_connections = 300;
"
# 注意:重启后失效,需要写入 my.cnf

推荐先执行方案 A 释放连接,再优化配置。单纯调大 max_connections 是治标不治本,连接数越多,内存消耗越大(每个连接约 256KB-1MB 线程栈)。


根治方案(应用层 + 数据库层)

步骤 1:优化应用连接池配置

# application.yml(正确示例)
spring.datasource.hikari:
  maximum-pool-size: 20        # 根据并发量调整,单节点 15-20 足够
  minimum-idle: 5              # 最小空闲连接,保持低水位
  idle-timeout: 120000          # 2 分钟无使用就释放空闲连接
  connection-timeout: 10000     # 获取连接最多等 10 秒
  max-lifetime: 1800000         # 连接最长存活 30 分钟,到期自动重建
  leak-detection-threshold: 60000  # 连接泄漏检测,60 秒未归还告警

关键调整: - maximum-pool-size:从 50 降到 20,3 台服务器 × 20 = 60,留充足余量 - minimum-idle:从 50 降到 5,业务低峰期自动释放空闲连接 - idle-timeout:从 10 分钟降到 2 分钟,快速释放不用连接 - max-lifetime:从永久改为 30 分钟,防止连接长期不释放

步骤 2:优化数据库超时参数

-- 临时设置(立即生效,重启丢失)
SET GLOBAL wait_timeout = 300;        -- 5 分钟无活动自动关闭
SET GLOBAL interactive_timeout = 300;  -- 同上

-- 永久设置(写入 my.cnf)
[mysqld]
wait_timeout = 300
interactive_timeout = 300
max_connections = 200                  -- 适度放大,留余量

注意:wait_timeout 要小于应用连接池的 max-lifetime,形成"双层保险"。 - 连接池 max-lifetime = 1800(30 分钟) - 数据库 wait_timeout = 300(5 分钟) - 逻辑:应用先关(30 分钟),如果应用没关,数据库 5 分钟后强制关

步骤 3:检查连接泄漏(代码层)

// 错误示例:连接没关闭
try {
    Connection conn = dataSource.getConnection();
    // ... 执行查询 ...
    // 忘记 conn.close()!
} catch (Exception e) {
    e.printStackTrace();
}

// 正确示例:try-with-resources 自动关闭
try (Connection conn = dataSource.getConnection();
     PreparedStatement ps = conn.prepareStatement(sql)) {
    // ... 执行查询 ...
} catch (Exception e) {
    // 自动关闭 conn 和 ps
}

验证效果

# 优化后重新检查
dbskiter --database=jump monitor health

预期结果

指标 优化前 优化后 变化
连接数使用率 100% (151/151) 15% (30/200) 正常
休眠连接 148 < 10 释放
活跃查询 3 正常范围 稳定
单服务器连接 50 20 合理

复盘要点

1. 连接数排查口诀

Too many connections → 看连接状态 → 看休眠连接数 → 看连接来源 → 看连接池配置 → 调超时/调连接池

2. 连接数核心公式

数据库最大连接数 >= 应用连接池最大连接数 × 应用服务器数量 + 预留连接

示例:
  应用连接池最大: 20
  应用服务器: 3 台
  监控 + 备份: 5
  预留: 20%

  需要: 20 × 3 + 5 = 65
  配置: 65 × 1.2 = 80

  max_connections 建议设为 80-100,不要 151 也不要 500

3. 连接池配置黄金法则

参数 推荐值 说明
maximum-pool-size CPU 核数 × 2 + 有效磁盘数 单节点通常 10-30
minimum-idle maximum-pool-size 的 20-30% 低峰期自动释放
idle-timeout 1-3 分钟 快速释放不用连接
max-lifetime 15-30 分钟 定期重建,防止连接老化
connection-timeout 5-10 秒 获取连接等待上限
wait_timeout(数据库) 2-5 分钟 小于 max-lifetime,形成双层保险

4. 常见误区

误区 正确做法
"max_connections 越大越好" 连接越多内存消耗越大,151 通常够用,先优化连接池
"连接池设置最大 50 安全" 连接池大小 ≠ 并发能力,合理的连接池是 10-20
"连接数满了就杀进程" 先分析是连接泄漏还是配置问题,杀进程治标不治本
"wait_timeout 不能调小" 调小到 5 分钟不影响正常业务,只杀空闲连接
"连接池不用关,保持常驻" 连接池要保持最小空闲,不是最大连接常驻

5. 连接数监控建议

# 每日检查连接数趋势
dbskiter --database=jump monitor health

# 连接数使用率建议告警阈值
# 警告: > 70%
# 严重: > 85%
# 紧急: > 95%

# 定期检查休眠连接比例
dbskiter --database=jump diagnose connections
# 如果 Sleep 连接 > 80%,说明连接池配置有问题

延伸阅读