我不会起名字322· 后端 / 算法 / 数据库
📘 技术栈 | 🧩 力扣 Hot100 | 🐹 Go 项目 | 🟥 Redis | 🐬 MySQL
文章目录
- MySQL 连接数被打满:too many connections 的 3 类来源与 5 个参数
- 一、先分清:连接数不是「有多少人在用」
- 二、3 类来源
- 来源 1:连接泄漏(应用拿了不还)
- 来源 2:慢查询长期占住连接
- 来源 3:连接池配置把连接数放大了
- 三、5 个参数
- 四、已经连不上了怎么办(救急 3 招)
- 五、4 步排查顺序
- 六、总结
MySQL 连接数被打满:too many connections 的 3 类来源与 5 个参数
应用日志突然开始刷同一条错误:
Error 1040: Too many connections连mysql -uroot -p都登不进去、重启应用能好一阵子、过一会儿又满——这是 MySQL 连接数耗尽最典型的样子。更难受的是:当你想登录上去查原因时,你自己也连不上(MySQL 给SUPER权限用户预留的 1 个连接,在 8.0 之前是救命稻草)。
这篇文章把连接耗尽的 3 类来源拆开,给 5 个参数的正确配法,以及一套「已经连不上时怎么救」的动作。
一、先分清:连接数不是「有多少人在用」
MySQL 的连接状态要看两个数,它们的含义完全不同:
SHOWGLOBALSTATUSLIKE'Threads_connected';-- 当前建立的连接总数SHOWGLOBALSTATUSLIKE'Threads_running';-- 真正在执行语句的连接数SHOWGLOBALVARIABLESLIKE'max_connections';-- 上限Threads_connected高、Threads_running低 → 大量连接建立了但没在干活(Sleep),典型是连接池持有不释放或wait_timeout太大;- 两个都高 → 真的有那么多并发在执行,可能是慢查询堆积或者业务量确实上来了;
Threads_running长期超过 CPU 核数的几倍 → 数据库已经在「排队」了,加连接数只会更糟。
连接数不等于并发能力。MySQL 的吞吐由 CPU/IO 决定,几百个并发线程同时跑只会让上下文切换和锁等待吃掉一切。看到Threads_running飙到几百,第一反应应该是「为什么这些查询这么慢」,而不是「把max_connections调大」。
二、3 类来源
来源 1:连接泄漏(应用拿了不还)
最常见的一类:代码里从连接池借了连接,异常路径上没有归还;或者用了长连接却在网络闪断后没有重建机制,导致池子里堆了一堆死连接。
识别方法——看 Sleep 状态的连接分布:
SELECTuser,host,command,timeASsleep_seconds,COUNT(*)AScntFROMinformation_schema.processlistWHEREcommand='Sleep'GROUPBYuser,host,commandHAVINGsleep_seconds>300ORDERBYcntDESCLIMIT20;如果某个应用机器的连接有几十上百条Sleep且time是几千秒,基本就是泄漏(连接池以为它们还活着,MySQL 也以为客户端还在)。
Go 侧最常见的两个写法问题:
// ❌ 错误:Query 之后没有 Close rows,连接不会归还池子rows,err:=db.Query("SELECT id FROM user LIMIT 10")iferr!=nil{returnerr}// 忘记 rows.Close(),也没有迭代到底 → 连接一直被占着forrows.Next(){// ...}// ✅ 正确:defer 关闭,并且迭代完整rows,err:=db.QueryContext(ctx,"SELECT id FROM user LIMIT 10")iferr!=nil{returnerr}deferrows.Close()// ⭐ 关键forrows.Next(){// ...}iferr:=rows.Err();err!=nil{// ⭐ 迭代中途出错也要检查returnerr}// ❌ 错误:事务拿了不提交/不回滚tx,_:=db.Begin()_,_=tx.Exec("UPDATE ...")ifsomeErr!=nil{returnsomeErr// 直接 return,事务没回滚 → 连接被占住直到超时}tx.Commit()// ✅ 正确:defer 兜底回滚(Commit 之后再 Rollback 会返回 ErrTxDone,无害)tx,err:=db.BeginTx(ctx,nil)iferr!=nil{returnerr}defertx.Rollback()// ⭐ 无论成败都执行,Commit 之后是空操作if_,err:=tx.ExecContext(ctx,"UPDATE ...");err!=nil{returnerr}returntx.Commit()来源 2:慢查询长期占住连接
一条跑了 300 秒的 SQL,会占住一条连接 300 秒。并发上来时,连接就是这样被「吃掉」的。
-- 看正在执行、且跑得最久的连接SELECTid,user,host,db,command,time,state,LEFT(info,120)ASsql_textFROMinformation_schema.processlistWHEREcommand!='Sleep'ANDtime>10ORDERBYtimeDESCLIMIT20;这一类不能靠调大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 侧的标准配法:
packagemainimport("database/sql""time"_"github.com/go-sql-driver/mysql")funcnewDB(dsnstring,maxOpenint)(*sql.DB,error){db,err:=sql.Open("mysql",dsn)iferr!=nil{returnnil,err}// ⭐ 三个参数必须配套设置db.SetMaxOpenConns(maxOpen)// 最大打开连接数(含在用+空闲)db.SetMaxIdleConns(maxOpen/2)// 最大空闲连接数db.SetConnMaxLifetime(30*time.Minute)// 连接最长存活时间,要小于 MySQL 的 wait_timeoutdb.SetConnMaxIdleTime(5*time.Minute)// 空闲连接最长保留时间(Go 1.15+)returndb,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 |
运行时修改(立即生效,重启失效):
SETGLOBALmax_connections=1000;SETGLOBALmax_user_connections=800;SETGLOBALwait_timeout=900;SETGLOBALinteractive_timeout=900;-- ⭐ 别忘了这一个配置文件(重启后仍生效,必须同步改):
[mysqld] max_connections = 1000 max_user_connections = 800 wait_timeout = 900 interactive_timeout = 900 thread_cache_size = 64⚠注意:max_connections调大后,每个连接都要占用内存(per-threadbuffer: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 秒的连接SELECTCONCAT('KILL ',id,';')FROMinformation_schema.processlistWHEREcommand='Sleep'ANDtime>600;-- 把结果复制出来执行⚠ 生产上先确认这些连接确实来自应用连接池(不是备份、不是长事务),杀错会让业务报错。
③ 从操作系统层面限流。如果连登录都做不到,只能临时停掉部分应用实例,或者在网络层限制到 3306 的新连接,等 MySQL 缓过来再恢复。
五、4 步排查顺序
第 1 步:区分是「连接被占着」还是「真的并发高」。
SHOWGLOBALSTATUSLIKE'Threads_connected';SHOWGLOBALSTATUSLIKE'Threads_running';connected高、running低 → 来源 1 或来源 3(连接占着不干活);
两个都高 → 来源 2(慢查询),先去查慢 SQL,别急着加连接数。
第 2 步:按用户/主机聚合,定位是谁。
SELECTuser,SUBSTRING_INDEX(host,':',1)ASip,COUNT(*)AScntFROMinformation_schema.processlistGROUPBYuser,ipORDERBYcntDESCLIMIT20;一眼就能看出是哪个应用实例(哪个 IP)在大量建连,结合实例数 × 池上限估算是否超配。
第 3 步:查 Sleep 时长分布和长事务。
-- Sleep 超过 5 分钟的连接SELECTCOUNT(*)FROMinformation_schema.processlistWHEREcommand='Sleep'ANDtime>300;-- 执行超过 30 秒的语句SELECTid,time,LEFT(info,120)FROMinformation_schema.processlistWHEREcommand!='Sleep'ANDtime>30ORDERBYtimeDESCLIMIT10;前者指向连接泄漏/超时配置,后者指向慢查询。
第 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(保留连接机制相同)。