ERROR 1040 (HY000): Too many connections 这个错误,直接调大 max_connections 通常只是把问题推后。真正的解法是先搞清谁在占连接、占着在干什么。
三种完全不同的占满类型,解法相反:
| 类型 | 特征 | 根因 | 解法 |
|---|---|---|---|
| 连接泄漏 | 连接数缓慢单调上升,不回落 | 代码拿了连接没释放 | 修代码;调大只是拖延 |
| 瞬时高峰 | 连接数尖峰后回落 | 流量突增或批量任务 | 限流、连接池、错峰执行 |
| 长连接堆积 | 部分连接长期 Sleep 但 Time 很大 | 应用配了超长空闲时间 | 调小 wait_timeout 或应用侧回收 |
第一步:确认是哪种类型
-- 当前连接数与历史峰值
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Max_used_connections';
SHOW VARIABLES LIKE 'max_connections';
-- 采样两次看趋势(间隔 30 秒)
SHOW STATUS LIKE 'Threads_connected';
-- sleep 30 后再查一次
-- 被拒绝的连接次数(累加说明反复打满)
SHOW STATUS LIKE 'Aborted_connects';- 两次采样持续上升 = 连接泄漏(最需要改代码)
- 两次采样波动但峰值高 = 瞬时高峰
- Aborted_connects 持续增长 = 客户端连不上(配置或网络问题,不是连接数问题)
- Max_used / max_connections 长期 > 0.8 = 处于危险水位
第二步:看 processlist(关键是 Command 与 Time)
-- 只看非 Sleep 的连接,按时间倒序
SELECT id, user, host, db, command, time, state,
LEFT(info, 80) AS query
FROM information_schema.processlist
WHERE command <> 'Sleep'
ORDER BY time DESC
LIMIT 20;
-- 先看状态分布
SELECT command, state, COUNT(*) AS n
FROM information_schema.processlist
GROUP BY command, state
ORDER BY n DESC;
-- 按来源 IP 分组(找出哪个实例占最多)
SELECT SUBSTRING_INDEX(host, ':', 1) AS client, COUNT(*) AS n
FROM information_schema.processlist
GROUP BY client ORDER BY n DESC;| Command | 含义 | 要不要担心 |
|---|---|---|
| Query | 正在执行 SQL | 要担心——看 time 是否很长 |
| Sleep | 空闲等命令 | 看 time 是否特别长(可能泄漏) |
| Locked | 等表锁 | 要担心——有锁竞争 |
| Waiting for table metadata lock | 等元数据锁 | 非常要担心——长事务或未提交的 DDL |
| Copying to tmp table | 在写临时表 | 查询太重,缺索引 |
| Sending data | 在发数据 | 可能是大结果集 |
| Killed | 被杀但没退出 | 等它退出,必要时 kill |
第三步:连接泄漏的三种典型代码
类型 1:忘了 close
// 错误:拿到 rows 后若某分支提前 return 而没 defer,就泄漏
rows, err := db.Query("SELECT ...")
if err != nil {
return err
}
// 若这里中间有 return,连接不释放
defer rows.Close()
// 正确:紧跟着 defer
rows, err := db.Query("SELECT ...")
if err != nil {
return err
}
defer rows.Close()
for rows.Next() { ... }类型 2:事务没提交也没回滚
// 错误:出错时直接 return,事务没回滚,连接带着未提交事务被占住
tx, _ := db.Begin()
_, err = tx.Exec("UPDATE ...")
if err != nil {
return err // ← 事务没回滚!
}
tx.Commit()
// 正确:defer 兜底(已 Commit 时再 Rollback 是空操作)
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
if _, err = tx.Exec("UPDATE ..."); err != nil {
return err
}
return tx.Commit()类型 3:连接池参数配错
db, _ := sql.Open("mysql", dsn)
db.SetMaxOpenConns(20) // 默认 0 = 无限!
db.SetMaxIdleConns(10)
db.SetConnMaxLifetime(time.Hour) // 强制回收,防中间设备断连留死连接
db.SetConnMaxIdleTime(10 * time.Minute)SetMaxOpenConns默认 0 表示无限制——这是最容易踩的坑,高并发时开出几百连接把 MySQL 打满SetConnMaxLifetime必须设:不设的话,中间防火墙断掉的死连接会一直留在池里- MaxOpenConns 不是越大越好:超过 MySQL 的 max_connections 就直接报 1040
- 经验值:单实例 MaxOpenConns ≈ max_connections / 应用实例数 × 0.7
第四步:瞬时高峰怎么判断
-- 按秒看查询峰值(需 performance_schema)
SELECT DATE_FORMAT(event_time,'%H:%i:%s') AS sec, COUNT(*) AS n
FROM performance_schema.events_statements_history
GROUP BY sec ORDER BY n DESC LIMIT 10;
-- 看谁在短时间内发了大量查询
SELECT user, COUNT(*) AS queries
FROM performance_schema.events_statements_history
WHERE event_time > NOW() - INTERVAL 1 MINUTE
GROUP BY user ORDER BY queries DESC;瞬时高峰的典型来源:
- 定时任务叠加:多个 cron 同一分钟触发(备份 + 报表 + 清理),各自开连接
- 批量接口:一次请求开 N 个并发查询
- 缓存集体失效:缓存过期瞬间大量请求同时打 DB
- 慢查询堆积:一个慢查询占住连接,后续请求排队,连接数越堆越高
第五步:除了调 max_connections 更该做什么
调大 max_connections 有硬上限(受 open_files_limit 与内存限制)。每个连接占线程栈约 256KB-1MB,调太大会 OOM。
-- 检查现有上限
SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'open_files_limit';
-- 系统侧
1. ulimit -n
-- 临时调(重启失效,用于止血)
SET GLOBAL max_connections = 300;
-- 永久调:my.cnf [mysqld] max_connections=300
-- 同时确认 open_files_limit 足够(每个连接约 2 个 fd)但更根本的四件事:
| 措施 | 解决的问题 | 优先级 |
|---|---|---|
| 应用侧设 MaxOpenConns 上限 | 把「无限开连接」变成「有界」 | 最高 |
| 加索引消除慢查询 | 缩短慢查询占住连接的时间 | 最高 |
| 设 wait_timeout 回收空闲连接 | Sleep 连接堆积 | 高 |
| 读写分离 / 加缓存 | 减少打到主库的查询量 | 中 |
| 连接池中间件(ProxySQL) | 多实例统一管连接 | 按需 |
-- 回收长期空闲连接
SHOW VARIABLES LIKE 'wait_timeout'; -- 默认 28800 秒(8 小时)
SET GLOBAL wait_timeout = 600; -- 改为 10 分钟
-- 找出长时间 Sleep 的(先确认不是应用连接池里的)
SELECT id, user, host, time FROM information_schema.processlist
WHERE command='Sleep' AND time > 3600;
-- 确认后可 KILL <id>;wait_timeout 设太小有副作用:应用连接池里的空闲连接会被 MySQL 单方面断开,应用再拿来用就报错。要么应用侧配
ConnMaxLifetime小于wait_timeout,要么就别乱调。
第六步:应急止血 vs 根因修复
| 场景 | 应急(5 分钟内) | 根因 |
|---|---|---|
| 连接数瞬间打满、站点 502 | 调大 max_connections 到 300,或 kill 最长 Sleep | 查慢查询日志 |
| 持续泄漏、越用越满 | 重启应用进程(释放所有连接) | 修连接未关的代码 |
| 锁等待导致全堵 | 找出元数据锁持有者并 kill | 查未提交事务 / 长 DDL |
| 定时任务并发 | 临时错开执行时间 | 重排调度或改队列 |
总结
- 先分清三种类型:泄漏(单调上升)、高峰(尖峰回落)、堆积(长 Sleep),解法完全相反
- processlist 看 Command 与 Time,别只看数量——20 个 Query 比 200 个 Sleep 更危险
- Go/Java 的池子默认常常无上限,
SetMaxOpenConns不设等于给 MySQL 埋雷 - 调大 max_connections 有内存天花板:每连接 256KB-1MB 栈,300 连接可能吃掉几百 MB
- 根治靠三件事:连接数上限、消除慢查询、合理回收空闲