MySQL连接数被打满:too many connections的3类来源与5个参数
作者:我不会起名字322
应用日志突然开始刷同一条错误:
Error 1040: Too many connections
连 mysql -uroot -p 都登不进去、重启应用能好一阵子、过一会儿又满——这是 MySQL 连接数耗尽最典型的样子。更难受的是:当你想登录上去查原因时,你自己也连不上(MySQL 给 SUPER 权限用户预留的 1 个连接,在 8.0 之前是救命稻草)。
这篇文章把连接耗尽的 3 类来源拆开,给 5 个参数的正确配法,以及一套「已经连不上时怎么救」的动作。
一、先分清:连接数不是「有多少人在用」
MySQL 的连接状态要看两个数,它们的含义完全不同:
SHOW GLOBAL STATUS LIKE 'Threads_connected'; -- 当前建立的连接总数 SHOW GLOBAL STATUS LIKE 'Threads_running'; -- 真正在执行语句的连接数 SHOW GLOBAL VARIABLES LIKE 'max_connections'; -- 上限
Threads_connected高、Threads_running低 → 大量连接建立了但没在干活(Sleep),典型是连接池持有不释放或wait_timeout太大;- 两个都高 → 真的有那么多并发在执行,可能是慢查询堆积或者业务量确实上来了;
Threads_running长期超过 CPU 核数的几倍 → 数据库已经在「排队」了,加连接数只会更糟。
连接数不等于并发能力。 MySQL 的吞吐由 CPU/IO 决定,几百个并发线程同时跑只会让上下文切换和锁等待吃掉一切。看到 Threads_running 飙到几百,第一反应应该是「为什么这些查询这么慢」,而不是「把 max_connections 调大」。
二、3 类来源
来源 1:连接泄漏(应用拿了不还)
最常见的一类:代码里从连接池借了连接,异常路径上没有归还;或者用了长连接却在网络闪断后没有重建机制,导致池子里堆了一堆死连接。
识别方法——看 Sleep 状态的连接分布:
SELECT
user,
host,
command,
time AS sleep_seconds,
COUNT(*) AS cnt
FROM information_schema.processlist
WHERE command = 'Sleep'
GROUP BY user, host, command
HAVING sleep_seconds > 300
ORDER BY cnt DESC
LIMIT 20;
如果某个应用机器的连接有几十上百条 Sleep 且 time 是几千秒,基本就是泄漏(连接池以为它们还活着,MySQL 也以为客户端还在)。
Go 侧最常见的两个写法问题:
// ❌ 错误:Query 之后没有 Close rows,连接不会归还池子
rows, err := db.Query("SELECT id FROM user LIMIT 10")
if err != nil {
return err
}
// 忘记 rows.Close(),也没有迭代到底 → 连接一直被占着
for rows.Next() {
// ...
}
// ✅ 正确:defer 关闭,并且迭代完整
rows, err := db.QueryContext(ctx, "SELECT id FROM user LIMIT 10")
if err != nil {
return err
}
defer rows.Close() // ⭐ 关键
for rows.Next() {
// ...
}
if err := rows.Err(); err != nil { // ⭐ 迭代中途出错也要检查
return err
}
// ❌ 错误:事务拿了不提交/不回滚
tx, _ := db.Begin()
_, _ = tx.Exec("UPDATE ...")
if someErr != nil {
return someErr // 直接 return,事务没回滚 → 连接被占住直到超时
}
tx.Commit()
// ✅ 正确:defer 兜底回滚(Commit 之后再 Rollback 会返回 ErrTxDone,无害)
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback() // ⭐ 无论成败都执行,Commit 之后是空操作
if _, err := tx.ExecContext(ctx, "UPDATE ..."); err != nil {
return err
}
return tx.Commit()
来源 2:慢查询长期占住连接
一条跑了 300 秒的 SQL,会占住一条连接 300 秒。并发上来时,连接就是这样被「吃掉」的。
-- 看正在执行、且跑得最久的连接
SELECT id, user, host, db, command, time, state,
LEFT(info, 120) AS sql_text
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 10
ORDER BY time DESC
LIMIT 20;
这一类不能靠调大 max_connections 解决——那只会让更多慢查询同时跑,把数据库彻底拖垮。正确做法:
- 先杀掉明显的异常长查询(
KILL <id>); - 用
EXPLAIN分析为什么慢(索引、扫描行数); - 给应用侧加语句超时(
max_execution_time,8.0 的MAX_EXECUTION_TIMEhint,或连接池层面的context.WithTimeout)。
来源 3:连接池配置把连接数放大了
这是最容易被忽略、但数字最吓人的一类:
总连接数 ≈ 应用实例数 × 每个实例的连接池上限
举例:20 个 Pod,每个 Pod 的连接池 MaxOpenConns=50 → 理论上最多 1000 条连接。如果你的 max_connections 只有 500,那么在应用扩容或流量高峰时必然打满。
而且连接池的 MaxOpenConns 是每个实例的上限,不是全局的。所以正确的估算方式是:
应用侧 MaxOpenConns ≤ (max_connections × 0.8) / 应用实例数
(留 20% 给管理连接、备份、监控和其他业务。)
Go 侧的标准配法:
package main
import (
"database/sql"
"time"
_ "github.com/go-sql-driver/mysql"
)
func newDB(dsn string, maxOpen int) (*sql.DB, error) {
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
// ⭐ 三个参数必须配套设置
db.SetMaxOpenConns(maxOpen) // 最大打开连接数(含在用+空闲)
db.SetMaxIdleConns(maxOpen / 2) // 最大空闲连接数
db.SetConnMaxLifetime(30 * time.Minute) // 连接最长存活时间,要小于 MySQL 的 wait_timeout
db.SetConnMaxIdleTime(5 * time.Minute) // 空闲连接最长保留时间(Go 1.15+)
return db, nil
}SetConnMaxLifetime 一定要小于 MySQL 的 wait_timeout,否则会出现「应用以为连接还活着、MySQL 已经把它关了」的幽灵连接(报错 invalid connection / driver: bad connection)。
三、5 个参数
| 参数 | 默认值 | 作用 | 建议 |
|---|---|---|---|
max_connections | 151(5.7/8.0) | 全局最大连接数 | 按「应用实例数 × 池上限 × 1.25」估算,别盲目调到几千 |
max_user_connections | 0(不限) | 单个用户的最大连接数 | ⭐ 强烈建议设,防止一个应用吃光所有连接 |
wait_timeout | 28800(8 小时) | 非交互式连接的空闲超时 | 生产建议 600~1800 秒 |
interactive_timeout | 28800 | 交互式连接的空闲超时 | ⭐ 必须与 wait_timeout 同时设置才会生效(会话建立时按连接类型取值) |
thread_cache_size | 自动 | 缓存空闲线程,减少新建开销 | 连接频繁建立销毁时调大,如 50~100 |
运行时修改(立即生效,重启失效):
SET GLOBAL max_connections = 1000; SET GLOBAL max_user_connections = 800; SET GLOBAL wait_timeout = 900; SET GLOBAL interactive_timeout = 900; -- ⭐ 别忘了这一个
配置文件(重启后仍生效,必须同步改):
[mysqld] max_connections = 1000 max_user_connections = 800 wait_timeout = 900 interactive_timeout = 900 thread_cache_size = 64
⚠ 注意:max_connections 调大后,每个连接都要占用内存(per-thread buffer:sort_buffer_size、join_buffer_size、read_buffer_size 都是按连接分配的)。1000 个连接 × 几 MB 的 buffer 就是几个 G。调大之前先算内存,别把实例撑爆。
四、已经连不上了怎么办(救急 3 招)
按优先级排序:
① 用预留连接登录。 MySQL 为 SUPER / CONNECTION_ADMIN 权限用户保留了 1 个连接。如果你有这类账号,直接登进去;没有的话,用 --connect-timeout 反复重试,运气好能挤进去。
② 批量杀掉 Sleep 连接(治标但最快)。
-- 生成批量 KILL 语句:杀掉 Sleep 超过 600 秒的连接
SELECT CONCAT('KILL ', id, ';')
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 600;
-- 把结果复制出来执行
⚠ 生产上先确认这些连接确实来自应用连接池(不是备份、不是长事务),杀错会让业务报错。
③ 从操作系统层面限流。 如果连登录都做不到,只能临时停掉部分应用实例,或者在网络层限制到 3306 的新连接,等 MySQL 缓过来再恢复。
五、4 步排查顺序
第 1 步:区分是「连接被占着」还是「真的并发高」。
SHOW GLOBAL STATUS LIKE 'Threads_connected'; SHOW GLOBAL STATUS LIKE 'Threads_running';
connected 高、running 低 → 来源 1 或来源 3(连接占着不干活);
两个都高 → 来源 2(慢查询),先去查慢 SQL,别急着加连接数。
第 2 步:按用户/主机聚合,定位是谁。
SELECT user, SUBSTRING_INDEX(host, ':', 1) AS ip, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY user, ip ORDER BY cnt DESC LIMIT 20;
一眼就能看出是哪个应用实例(哪个 IP)在大量建连,结合实例数 × 池上限估算是否超配。
第 3 步:查 Sleep 时长分布和长事务。
-- Sleep 超过 5 分钟的连接 SELECT COUNT(*) FROM information_schema.processlist WHERE command='Sleep' AND time>300; -- 执行超过 30 秒的语句 SELECT id, time, LEFT(info,120) FROM information_schema.processlist WHERE command != 'Sleep' AND time > 30 ORDER BY time DESC LIMIT 10;
前者指向连接泄漏/超时配置,后者指向慢查询。
第 4 步:核对参数并压测验证。
确认 wait_timeout / interactive_timeout 是否成对设置、max_user_connections 是否合理、应用侧 MaxOpenConns 与实例数的乘积是否超过 max_connections × 0.8。改完之后用真实并发压一轮,观察 Threads_connected 的峰值。
六、总结
Threads_connected和Threads_running要分开看:前者高是连接被占着(泄漏/池配置),后者高是慢查询堆积。两种问题的处理方式完全相反。- 3 类来源:① 连接泄漏(
rows.Close()、tx.Rollback()忘了写);② 慢查询长期占住连接(加语句超时、优化 SQL);③ 连接池配置放大(应用实例数 × 每个实例的池上限 才是总连接数)。 - 5 个参数:
max_connections、max_user_connections(防一个应用吃光)、wait_timeout、interactive_timeout(⭐ 必须与前者同时设)、thread_cache_size。 - 应用侧配对:
SetConnMaxLifetime必须小于 MySQL 的wait_timeout,否则会出现「连接已被服务端关闭」的幽灵连接。 - 调大
max_connections不是万能解:每个连接都占 per-thread buffer,调大前先算内存;真正的解法是控制池大小 + 消除慢查询 + 修掉泄漏。
本文参数默认值以 MySQL 5.7/8.0 为准(max_connections 默认 151);KILL、PROCESSLIST、max_user_connections 等行为在各版本一致,8.0 起权限名从 SUPER 变为 CONNECTION_ADMIN(保留连接机制相同)。
以上就是MySQL连接数被打满:too many connections的3类来源与5个参数的详细内容,更多关于MySQL连接数被打满:too many connections的资料请关注脚本之家其它相关文章!
