Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL连接数被打满:too many connections

MySQL连接数被打满:too many connections的3类来源与5个参数

作者:我不会起名字322

本文深入剖析MySQL出现too many connections错误的3类核心原因:连接泄漏、慢查询长期占用连接、连接池配置不当导致连接数被放大,重点解析5个关键参数的同配置原则,并提供连不上时的3种应急方案及4步标准化排查流程,需要的朋友可以参考下

应用日志突然开始刷同一条错误:

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';  -- 上限

连接数不等于并发能力。 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 解决——那只会让更多慢查询同时跑,把数据库彻底拖垮。正确做法:

  1. 先杀掉明显的异常长查询(KILL <id>);
  2. 用 EXPLAIN 分析为什么慢(索引、扫描行数);
  3. 给应用侧加语句超时(max_execution_time,8.0 的 MAX_EXECUTION_TIME hint,或连接池层面的 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_connections151(5.7/8.0)全局最大连接数按「应用实例数 × 池上限 × 1.25」估算,别盲目调到几千
max_user_connections0(不限)单个用户的最大连接数⭐ 强烈建议设,防止一个应用吃光所有连接
wait_timeout28800(8 小时)非交互式连接的空闲超时生产建议 600~1800 秒
interactive_timeout28800交互式连接的空闲超时⭐ 必须与 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 的峰值。

六、总结

  1. Threads_connected 和 Threads_running 要分开看:前者高是连接被占着(泄漏/池配置),后者高是慢查询堆积。两种问题的处理方式完全相反。
  2. 3 类来源:① 连接泄漏(rows.Close()、tx.Rollback() 忘了写);② 慢查询长期占住连接(加语句超时、优化 SQL);③ 连接池配置放大(应用实例数 × 每个实例的池上限 才是总连接数)。
  3. 5 个参数:max_connections、max_user_connections(防一个应用吃光)、wait_timeout、interactive_timeout(⭐ 必须与前者同时设)、thread_cache_size。
  4. 应用侧配对:SetConnMaxLifetime 必须小于 MySQL 的 wait_timeout,否则会出现「连接已被服务端关闭」的幽灵连接。
  5. 调大 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的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文