MySQL连接数治理指南:应急恢复与长期优化策略
今日关键词:MySQL连接数、Too many connections、连接泄漏、max_connections、紧急排查
上周五下班前,监控群突然炸了——"数据库连不上了!所有接口都在报错!" 应用日志里,满屏都是 Too many connections。
那一瞬间,心跳直接飙到120。
之前我们聊过连接池设计如何预防这类问题,但预防归预防,真出了事怎么办?连接数爆满,是生产环境中最常见的紧急故障之一,处理不好就是全线业务停摆。
这里分享我的排查和处理过程,从"先让业务活下来"到"搞清楚为什么",希望遇到同类问题时,大家能少走弯路,从容应对。
一、Too many connections 的本质
先搞清楚这个报错的本质。
MySQL 有一个参数 max_connections,限制了同时能连多少个客户端。5.7和8.0默认是151个。当已建立的连接数达到这个上限,新的连接请求就会被拒绝,报 ERROR 1040 (HY000): Too many connections。
注意一个细节:root用户在本地通过socket连接时,MySQL会预留一个额外的连接位。所以业务连不上了,DBA用root还是能登进去排查。这个设计就是给紧急情况留的后门。
连接数打满不等于数据库挂了。进程还在,数据也没丢,只是"门关上了进不去"。搞清楚这一点,心态就稳了。
二、紧急评估:30秒内搞清楚状况
收到告警先别慌,三步快速判断局势。
第一步:MySQL 还活着吗
# 看进程ps aux | mysqld | grep -v grep# 看端口ss -tlnp | grep 3306# 看机器负载uptime
进程在、端口在、负载没爆,说明MySQL本身没挂,只是连接满了。这算好消息。
第二步:连了多少,连的谁
# 用socket直连,不走TCPmysql -u root -p -S /var/lib/mysql/mysql.sock -e "SHOW PROCESSLIST;" 2>/dev/null | wc -l
如果连socket都连不上(很少见),从操作系统层看:
# 看3306端口的连接总数ss -ant | grep :3306 | wc -l# 看每个IP的连接分布ss -ant | grep :3306 | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -rn
第三步:区分两种关键指标
SHOW STATUS LIKE 'Threads_connected';SHOW STATUS LIKE 'Threads_running';SHOW VARIABLES LIKE 'max_connections';
Threads_connected 是已建立的连接总数,包含睡觉的。Threads_running 是正在跑SQL的连接数。
这两个数的区别很关键。如果 Threads_connected 高但 Threads_running 低,说明大量连接在Sleep,杀了就行。如果 Threads_running 也高,说明有东西在跑慢查询,得找到它干掉。
三、快速止血:先让业务活过来
确认了情况,开始止血。以下操作有侵入性,执行前心里要有数。
杀掉睡眠连接
大多数情况下,连接数爆满的元凶是大量Sleep连接。这些连接建立后啥也没干,或者干完了没关,白白占着坑位。
-- 杀掉超过30秒的睡眠连接SELECT CONCAT('KILL ', ID, ';')FROM INFORMATION_SCHEMA.PROCESSLISTWHERE COMMAND = 'Sleep'AND TIME > 30;
拿到结果后复制出来执行。或者更暴力一点:
# 一行搞定mysql -u root -S /var/lib/mysql/mysql.sock -N -e "SELECT CONCAT('KILL ', ID, ';')FROM INFORMATION_SCHEMA.PROCESSLISTWHERE COMMAND = 'Sleep' AND TIME > 30;" | mysql -u root -S /var/lib/mysql/mysql.sock
第一次处理这个问题的时候,把所有Sleep连接都杀了,包括一些正常的。后来才知道要加 TIME > 30 的条件,刚建立几秒的Sleep连接可能是正常的——应用从连接池取出来准备用,还没来得及发SQL。
临时提高 max_connections
清理完还不够?把上限临时拉高:
-- 动态调整,不用重启SET GLOBAL max_connections = 500;
这个改法MySQL重启就失效了。想持久化的话用 SET PERSIST(8.0 )或者改my.cnf。但记住,这只是应急,不是解法。max_connections设太大,内存扛不住反而更惨。
杀掉慢查询
如果 Threads_running 很高,得找到那些跑得最久的查询:
SELECTID,USER,HOST,DB,COMMAND,TIME,STATE,LEFT(INFO, 100) AS QUERYFROM INFORMATION_SCHEMA.PROCESSLISTWHERE COMMAND != 'Sleep'ORDER BY TIME DESCLIMIT 10;
看到执行了300秒的查询?大概率就是它在捣乱。确认不影响业务后干掉:
KILL
注意区分:KILL 杀的是连接,KILL QUERY 只停查询不断连接。紧急情况下直接 KILL 更彻底。
四、根因分析:四个方向挨个查
血止住了,得搞清楚为什么会爆。连接数爆满不是病根,是症状。常见根因就四个。
根因一:慢查询堆积
这是最常见的原因。一条SQL执行30秒,这30秒内这个连接一直在干活,别的请求只能开新连接。如果慢查询持续涌入,连接数很快打满。
排查方法:
-- 通过performance_schema看哪些SQL最耗时SELECTDIGEST,COUNT_STAR,SUM_TIMER_WAIT / 1000000000000 AS SUM_SECONDS,A VG_TIMER_WAIT / 1000000000000 AS A VG_SECONDS,LEFT(SQL_TEXT, 200) AS SQL_SAMPLEFROM performance_schema.events_statements_summary_by_digestORDER BY SUM_TIMER_WAIT DESCLIMIT 10;
再看慢查询日志确认:
tail -100 /var/log/mysql/slow.log | grep -i "Query_time"
遇到过的一个案例:财务模块导出报表触发了一条跨五张表的JOIN查询,没走索引,单次执行3分钟。当天下午正好好几个财务在同时导出,连接直接打满。后来加了索引,执行时间从3分钟降到0.2秒,问题解决。
根因二:连接泄漏
这个最隐蔽。应用代码拿到连接后没关闭,连接一直占着不释放。少量泄漏看不出来,积累几天就爆了。
怎么判断是不是泄漏:
SELECTUSER,HOST,COUNT(*) AS CONN_COUNT,MAX(TIME) AS MAX_TIME,MIN(TIME) AS MIN_TIMEFROM INFORMATION_SCHEMA.PROCESSLISTWHERE COMMAND = 'Sleep'GROUP BY USER, HOSTHA VING COUNT(*) > 10ORDER BY CONN_COUNT DESC;
某个来源IP的Sleep连接数持续增长,而且 MAX_TIME 特别大(比如几千秒),基本就是泄漏了。
之前见过的一个案例:Ja va应用的异常处理分支里,catch块只记了日志,没关连接。正常请求没问题,一碰到异常就漏一个。平时流量小看不出来,攒了一周,周五下午高峰时爆了。
修复代码很简单,用 try-with-resources:
// 泄漏写法public void queryData() { Connection conn = dataSource.getConnection();// 异常时conn不会被关闭query(conn);}// 正确写法public void queryData() { try (Connection conn = dataSource.getConnection()) { query(conn);}}// 方法结束自动关闭,异常也会关
根因三:短连接冲击
PHP和部分Python应用默认用短连接——每次请求新建连接,用完就关。并发量一上来,MySQL要不停地做TCP握手、认证、分配线程,忙不过来连接就堆上了。
-- 看连接建立速率SHOW STATUS LIKE 'Connections';SHOW STATUS LIKE 'Aborted_connects';
Aborted_connects 持续涨,说明大量连接建立失败。结合 wait_timeout 的设置看:
SHOW VARIABLES LIKE 'wait_timeout';SHOW VARIABLES LIKE 'interactive_timeout';
这两个值如果设得很短(比如60秒),Sleep连接60秒就断,应用下次请求又得新建。频繁建连关连,MySQL压力山大。
一般建议 wait_timeout = 600(10分钟),别太短也别太长。
根因四:max_connections 确实太小
MySQL默认151,中等流量的生产环境根本不够用。
SHOW VARIABLES LIKE 'max_connections';SHOW STATUS LIKE 'Max_used_connections';
Max_used_connections 是MySQL启动以来的历史峰值。如果这个值已经很接近 max_connections,说明上限确实该调了。
但别拍脑袋调,得算。每个连接占大约2-4MB内存(thread_stack 各种buffer),服务器能撑多少连接取决于还剩多少内存:
可分配连接数 ≈ (总内存 - Buffer Pool - 系统预留) / 单连接内存
典型经验值:
| 服务器配置 | max_connections 建议 |
|---|---|
| 4核8G | 300~500 |
| 8核16G | 500~800 |
| 16核32G | 800~1500 |
调完记得观察一段时间,别调完就走。
五、一个诊断脚本搞定排查
上面的信息量不小,紧急情况下手忙脚乱容易漏。我写了一个脚本,一键收集所有关键信息:
#!/bin/bash# mysql_conn_diagnosis.sh# 用法:bash mysql_conn_diagnosis.shSOCKET="/var/lib/mysql/mysql.sock"OUTPUT="/tmp/mysql_conn_diag_$(date %Y%m%d_%H%M%S).txt"exec > >(tee "$OUTPUT") 2>&1echo "====== MySQL 连接数爆满诊断 ======"echo "时间: $(date)"echo ""echo "--- 1. 连接数概览 ---"mysql -u root -S "$SOCKET" -N -e "SELECT 'Threads_connected:', VARIABLE_VALUE FROM performance_schema.global_status WHERE variable_name='Threads_connected';SELECT 'Threads_running:', VARIABLE_VALUE FROM performance_schema.global_status WHERE variable_name='Threads_running';SELECT 'max_connections:', @@max_connections;SELECT 'Max_used_connections:', VARIABLE_VALUE FROM performance_schema.global_status WHERE variable_name='Max_used_connections';" 2>/dev/nullecho ""echo "--- 2. 连接来源分布 ---"mysql -u root -S "$SOCKET" -e "SELECT USER, SUBSTRING_INDEX(HOST,':',1) AS SRC_IP, COUNT(*) AS CNTFROM INFORMATION_SCHEMA.PROCESSLISTGROUP BY USER, SRC_IPORDER BY CNT DESC LIMIT 10;" 2>/dev/nullecho ""echo "--- 3. 执行时间最长的查询 ---"mysql -u root -S "$SOCKET" -e "SELECT ID, USER, TIME, STATE, LEFT(INFO,120) AS QUERYFROM INFORMATION_SCHEMA.PROCESSLISTWHERE COMMAND != 'Sleep'ORDER BY TIME DESC LIMIT 10;" 2>/dev/nullecho ""echo "--- 4. 未提交事务 ---"mysql -u root -S "$SOCKET" -e "SELECT trx_id, trx_state, trx_started, TIMESTAMPDIFF(SECOND, trx_started, NOW()) AS RUNNING_SEC, LEFT(trx_query, 100) AS QUERYFROM INFORMATION_SCHEMA.INNODB_TRXORDER BY trx_started LIMIT 10;" 2>/dev/nullecho ""echo "--- 5. 锁等待 ---"mysql -u root -S "$SOCKET" -e "SELECT COUNT(*) AS lock_wait_cnt FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_state='LOCK WAIT';" 2>/dev/nullecho ""echo "诊断完成,结果已保存到 $OUTPUT"
把这个脚本提前放在服务器上,出事的时候直接跑,不用现场想命令。
六、监控告警:别等爆了才知道
连接数爆满的可怕在于——等你发现的时候业务已经挂了。必须靠监控提前预警。
Prometheus MySQL Exporter
用 mysqld_exporter 采集MySQL指标,核心关注这几个:
# 连接使用率mysql_global_status_threads_connected / mysql_global_variables_max_connections# 活跃线程数mysql_global_status_threads_running# 连接拒绝速率rate(mysql_global_status_connection_errors_total[5m])
告警规则
# mysql_connection_alerts.ymlgroups:- name: MySQL连接数告警rules:- alert: MySQLConnectionsHighexpr: mysql_global_status_threads_connected / mysql_global_variables_max_connections > 0.8for: 5mlabels:severity: warningannotations:summary: "MySQL连接数超过80%"- alert: MySQLConnectionsCriticalexpr: mysql_global_status_threads_connected / mysql_global_variables_max_connections > 0.95for: 1mlabels:severity: criticalannotations:summary: "MySQL连接数即将打满,立即处理!"- alert: MySQLThreadsRunningHighexpr: mysql_global_status_threads_running > 20for: 5mlabels:severity: warningannotations:summary: "MySQL活跃连接数过高,可能存在慢查询"
80%告警、95%严重——给处理留出缓冲时间。别设到99%才叫,那时候已经来不及了。
业务级心跳
光看连接数还不够。习惯在Grafana面板上同时放三个指标:连接数趋势、活跃线程数、慢查询速率。三个一起看,才能判断是"连接没关"还是"SQL跑得慢"。
七、长期方案:参数 连接池 代码
止血和定位解决的是当下,长期不出问题得从三个层面做。
参数调优
-- 按服务器配置设合理的上限SET PERSIST max_connections = 800;-- 睡眠连接别占太久SET PERSIST wait_timeout = 600;SET PERSIST interactive_timeout = 600;
8.0的 SET PERSIST 很方便,改完立即生效且重启不丢。比改my.cnf安全。
上连接池
连接泄漏和短连接问题的根本解法是上连接池。Ja va用HikariCP:
spring:datasource:hikari:maximum-pool-size: 20minimum-idle: 5connection-timeout: 30000idle-timeout: 600000max-lifetime: 1800000
maximum-pool-size 的经验值是 (CPU核数 * 2) 磁盘数。8核机器配20左右差不多。别设太大,连接池太大会适得其反。
Python用内置的连接池:
from mysql.connector import poolingpool = pooling.MySQLConnectionPool(pool_name="app_pool",pool_size=10,host="localhost",database="mydb",user="app_user",password="xxx")def query(sql, params=None):conn = pool.get_connection()try:cursor = conn.cursor(dictionary=True)cursor.execute(sql, params or ())return cursor.fetchall()finally:cursor.close()conn.close()# 归还到池,不是真断开
ProxySQL 做连接复用
如果应用代码改不动(老系统),可以在数据库前面放ProxySQL做连接复用:
-- ProxySQL管理端mysql -h 127.0.0.1 -P 6032 -u admin -padmin-- 查看连接池状态SELECT * FROM stats_mysql_connection_pool;-- 调整后端连接上限UPDATE mysql_servers SET max_connections=100 WHERE hostgroup_id=10;LOAD MYSQL SERVERS TO RUNTIME;
ProxySQL能扛住几千个前端连接,后端只开几十个到MySQL,相当于一个连接"漏斗"。
八、五个真实场景速查
| 场景 | 特征 | 处理 |
|---|---|---|
| 促销突发流量 | 连接数几分钟内飙升 | 临时调高max_connections 杀Sleep连接 事后上连接池 |
| 慢查询堆积 | Threads_running高,有长时间执行的SQL | 找到慢SQL杀掉 加索引优化 |
| 连接泄漏 | Sleep连接数持续增长,MAX_TIME很大 | 修复代码 定时清理脚本兜底 |
| max_connections太小 | Max_used_connections接近上限 | 算好内存后调大上限 |
| ProxySQL自身瓶颈 | 通过ProxySQL连不上但直连正常 | 调整ProxySQL的pool_size和max_connections |
九、面试怎么答
面试官:MySQL报Too many connections,你的处理流程是什么?
面试官:max_connections设多大合适?
面试官:怎么判断是连接泄漏?
避坑清单
| 序号 | 坑点 | 后果 | 正确做法 |
|---|---|---|---|
| 1 | 根本不开监控,等业务报障才发现 | 业务已中断几分钟甚至几十分钟 | 配Prometheus告警,80%就预警 |
| 2 | max_connections盲目设到10000 | 内存爆了,CPU锁争用飙升,比连不上更惨 | 按内存算,留余量 |
| 3 | 杀Sleep连接不加时间条件 | 把正在等下一个请求的正常连接也杀了 | 加 TIME > 30 过滤 |
| 4 | 只止血不查根因 | 同样的问题反复发生 | 止血后必须分析是慢查询、泄漏还是配置问题 |
| 5 | 连接池size设太大 | 数据库压力反而更大,排队更严重 | 经验值:(CPU核数×2) 磁盘数 |
| 6 | wait_timeout设太短(比如30秒) | 连接频繁断开重建,MySQL握手开销大 | 一般600秒起步 |
| 7 | 应用代码不关连接,指望MySQL回收 | 连接泄漏积累到爆 | try-with-resources或finally关闭 |
| 8 | 改了max_connections不改my.cnf或不用SET PERSIST | MySQL重启后配置丢失 | 用SET PERSIST或改配置文件 |
| 9 | 出了事才想加监控 | 黄花菜都凉了 | 监控先行,上线前就配好 |
| 10 | 把连接数高等同于数据库挂了 | 盲目重启MySQL,可能丢数据 | 连接满不等于数据库挂,root能连就能救 |
总结
连接数爆满是"症状"不是"病根"。处理思路很简单:
止血 → 杀Sleep连接,临时调高max_connections
定因 → 慢查询?泄漏?短连接?上限太小?
治本 → 该加索引加索引,该修代码修代码,该上连接池上连接池
预防 → 监控告警先行,别等爆了才反应
记住一个优先级:先活下来,再查原因,最后治本。三个步骤都不能少,少了最后一步,同样的问题下周还会来。
