1. 先搞懂“Too many connections”到底在说什么
如果你在CentOS上跑MySQL,某天突然收到一条这样的报错:
ERROR 1040 (HY000): Too many connections别怀疑,你的MySQL实例连接数已经打满了。这个报错的意思是:MySQL服务器当前允许同时存在的客户端连接数量已经达到上限,新的连接请求直接被拒绝。这不是网络问题,也不是SQL写错了,更不是磁盘满了——就是你“占坑”占满了。
MySQL默认的max_connections参数通常只有151(5.7及之后版本),也就是说同一时刻最多只能有151个客户端连接到数据库。对于一个小型业务系统,这个数字看着够用,但一旦你的应用做了连接池、又有慢查询、还有一堆后台脚本、监控工具在连库,151个连接很快就会被吃光。
我在实际处理过的一次故障里,单单一个Java应用连接池就开了100个连接,加上几个定时任务脚本(每个脚本开5~10个连接),再算上运维排查时手工连上去的会话,瞬间就打满了。更难受的是,连接一旦打满,业务侧会不断重试建立新连接,越重试越排队,越排队连接越多,最后把数据库彻底“围死”。
这个报错真正让人头疼的地方在于:它不是一个简单的“调大参数”就能收工的问题。调大max_connections只是把天花板抬高,根子上的问题——连接为什么会被占满、有没有连接泄漏、连接池配置是否合理——如果不解决,迟早还会再来一次。
所以这篇文章我按实战顺序来写:先讲快速止血,再讲彻底排查,最后讲预防方案。全程以CentOS 7.x环境为例,MySQL版本以5.7/8.0为主,命令和配置直接可以照着敲。
2. 快速止血:让数据库先能连进去
2.1 预留连接通道:skip-networking的妙用
遇到Too many connections时最尴尬的一件事是:你连管理连接都被拒了。普通的连接进不去,那怎么排查?
MySQL其实预留了一个管理通道——如果你有系统的root权限,可以直接跳过网络层连接:
# 停掉MySQL服务(注意:这会中断所有业务) systemctl stop mysqld # 以跳过网络监听的方式启动 mysqld_safe --skip-networking &--skip-networking启动后,MySQL不会再监听3306端口,外部TCP连接全部拒绝,但本地的socket连接仍然可用。这时候你通过Linux的root用户登录本机MySQL:
mysql -uroot -p就能进得去了,因为socket连接不受TCP连接数限制。
注意:这个操作是“断电式”的,要先和业务侧确认能不能接受数据库短暂中断。实在紧急的情况下,该断就断,保数据库比保连接更重要。
如果你不想重启MySQL,还有另一个更温和的办法:用gdb或percona-toolkit在运行中直接修改连接数限制。比如用gdb附加到mysqld进程上执行:
gdb -p $(pidof mysqld) -ex "p max_connections=500" -ex "detach" -ex "quit"这招属于“外科手术式”的临时急救,能不动服务就把连接数上限调上去,但生产环境不建议频繁用,而且MySQL 8.0部分版本对gdb附加有防护限制,不一定成功。稳妥起见,我优先推荐重启进--skip-networking。
2.2 登录后立刻确认三件事
当你成功登进MySQL,按顺序执行下面几条命令,快速摸清现场:
-- 1. 当前实际连接数 SHOW STATUS LIKE 'Threads_connected'; -- 2. 历史最大连接数(这个值非常关键) SHOW STATUS LIKE 'Max_used_connections'; -- 3. 连接数上限 SHOW VARIABLES LIKE 'max_connections';Threads_connected是当前有多少连接在用,Max_used_connections是MySQL启动以来达到过的最大连接数。这两个对比一下就能判断:是短期内突发的尖峰打满了,还是持续处于高位运行导致的。
再查一下连接来源和状态:
-- 按用户分组看连接数 SELECT user, host, COUNT(*) FROM information_schema.processlist GROUP BY user, host ORDER BY COUNT(*) DESC; -- 按状态分组看连接在干嘛 SELECT command, state, COUNT(*) FROM information_schema.processlist GROUP BY command, state ORDER BY COUNT(*) DESC;第一句看“谁在用连接”,第二句看“连接在干什么”。如果是某个用户占了几十个连接,八成是那个业务连接池问题;如果大量连接卡在Query或Sleep状态,那就要往慢查询和空闲连接方向查。
2.3 临时扩容:把连接数调上去
确认情况后,如果业务急需恢复,临时调大max_connections是不得不做的一步:
-- 全局生效,不需要重启 SET GLOBAL max_connections = 500;这里有个坑:SET GLOBAL改的只是运行时值,MySQL重启之后会回退到配置文件里的值。所以这只是止血,后面必须同步修改配置文件。
另外一个参数别漏了:
SET GLOBAL max_connect_errors = 100000;如果客户端因认证失败或中断频繁触发max_connect_errors,MySQL会直接屏蔽该主机的连接,表现为连接被拒但又不是Too many connections。这个和本文主题相关但容易被忽略,建议顺手调一下。
注意:临时扩容只是给排查争取时间,不要以为调完就万事大吉。如果业务代码有连接泄漏,调再大也会被打满,只是时间问题。
3. 把连接数打满的真正原因挖出来
3.1 是谁占着连接不放手
连接数打满,最常见的原因有三类:连接池配置过大、连接泄漏、慢查询堆积。我一个个说。
连接池配置过大:很多应用的数据库连接池(比如HikariCP、Druid)默认最大连接数就设在50~100。如果服务是多实例部署,10个实例就是500~1000个连接,远超MySQL的默认上限。加上连接池会做“预热”,启动时甚至会把连接全部建好,数据库直接被占满。
排查方法是看processlist里的Sleep状态。别小看Sleep状态的连接——它们不执行SQL,但占着连接数。连接池里的空闲连接就是这种状态。正常情况下占比高是合理的,但如果一个应用建立了几十个Sleep连接、长时间不释放,就要怀疑连接池的idleTimeout或minIdle设置有没有问题了。
连接泄漏:代码里获取了连接但不归还,或者异常分支没有关闭资源,就会出现连接只增不减。这种问题最隐蔽,因为平时没有症状,等到连接数涨到顶了才爆发。有一个很典型的特征:Threads_connected会持续攀升,即使业务请求量没有明显增长。
慢查询堆积:SQL写得烂,一条查询跑几十秒甚至几分钟,连接一直被占着执行,新请求不断进来,慢慢就把连接池和MySQL的连接数全拖垮了。这种情况在processlist里能看到大量State=Statistics或Sending data的连接,配合慢查询日志能快速定位。
3.2 用information_schema精准定位
不要上去盲目猜,直接用数据说话。下面这几条是我每次排查都会用的SQL:
-- 查看每个连接对应了什么SQL(截取前100个字符) SELECT id, user, host, db, command, time, LEFT(info, 100) AS query FROM information_schema.processlist ORDER BY time DESC LIMIT 50;这条命令能让你一眼看到:谁连接的时间最长、正在执行什么SQL。如果TOP连接全是某个业务账号的Sleep,重点查那个业务的连接池;如果有大量Query且time值很大,重点查慢SQL。
再进一步,看看有没有“僵尸”连接:
-- 找出空闲时间超过300秒的连接 SELECT id, user, host, db, time, state, LEFT(info, 100) AS query FROM information_schema.processlist WHERE command = 'Sleep' AND time > 300;正常情况下,空闲连接有,但很少会超过几分钟。如果大量连接空闲超过300秒,说明连接池的idleTimeout没生效或者代码有泄漏。
3.3 查看连接数的历史峰值
Max_used_connections这个指标能帮你判断:打满到底是偶发还是常态。
SHOW GLOBAL STATUS LIKE 'Max_used_connections'; SHOW GLOBAL STATUS LIKE 'Max_used_connections_time';Max_used_connections_time会显示达到峰值的时间点。把这个时间和业务量曲线对比,就能知道连接打满有没有规律:是每天固定时间点、还是某个版本上线后才出现的、还是大促流量导致的。
如果历史峰值一直很接近max_connections,说明系统一直处于高水位运行,扩容是刚需。如果平时只有几十连接、某天突然飙到上限,那就是突发情况,需要从代码和慢查询下手。
4. 根治:参数调优与配置落地
4.1 max_connections到底调多大合适
无脑调大max_connections不是好习惯。连接数越大,MySQL需要为每个连接分配的线程栈、内存缓冲区就越多。每个连接大概会占用2~10MB的线程内存(取决于配置),如果有1000个连接,光线程栈就可能吃掉好几个GB。
我的经验做法是:先算出当前业务的真实峰值,再留20%~30%的余量。
-- 查看当前实际连接的峰值,以此为基线 SHOW GLOBAL STATUS LIKE 'Max_used_connections'; -- 同时关注连接相关的内存消耗 SHOW VARIABLES LIKE 'thread_stack'; SHOW VARIABLES LIKE 'sort_buffer_size'; SHOW VARIABLES LIKE 'join_buffer_size';比如你查出来Max_used_connections = 180,业务还在增长,那把max_connections调到300~350是一个相对合理的估算——满足当前峰值的同时,留了一部分缓冲。
一个重要的前提:调整之前先去确认服务器的可用内存。可以用free -h看内存,再结合innodb_buffer_pool_size(通常占内存的大头)和其他全局缓冲区,算一下能不能撑起更多连接。硬扛到OOM就得不偿失了。
修改配置文件的方式:
vim /etc/my.cnf在[mysqld]段下添加或修改:
[mysqld] max_connections = 300 max_connect_errors = 100000保存后重启服务:
systemctl restart mysqld重启完验证:
SHOW VARIABLES LIKE 'max_connections';4.2 别忽略wait_timeout和interactive_timeout
调大max_connections的同时,必须配合调整连接空闲超时时间。否则,连接池里的空闲连接会一直占着位置,白白浪费连接数。
wait_timeout:非交互连接的空闲超时时间,默认28800秒(8小时)。interactive_timeout:交互式连接(比如命令行客户端)的空闲超时时间,默认也是28800秒。
生产环境我一般建议把这些值调小一些,比如:
[mysqld] wait_timeout = 300 interactive_timeout = 300意思是:连接空闲超过5分钟就断开。这样一来,即使连接池忘了回收连接,MySQL也会主动清理,避免大量Sleep连接积压。
注意:
wait_timeout和interactive_timeout在线程连接时生效,修改全局变量后,已存在的连接不会立刻受影响,要等新连接建立或重启服务才生效。
这里提一个容易踩的坑:如果业务是长连接型应用(比如某些消息队列消费者、常驻脚本),wait_timeout设太短会导致连接被服务端断开,应用侧没做重连的话,反而会报错。所以调整前先了解业务连接模型,再决定超时时间设多少。
4.3 连接池侧的治理才是治本
MySQL参数调好了,只能算“防守”,真正的治本在应用连接池。
拿Java常用的HikariCP和Druid举例:
HikariCP推荐配置:
spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 idle-timeout: 300000 connection-timeout: 30000 max-lifetime: 1800000Druid推荐配置:
spring: datasource: druid: initial-size: 5 min-idle: 5 max-active: 20 max-wait: 30000 validation-query: SELECT 1 test-while-idle: true time-between-eviction-runs-millis: 60000连接池的maximum-pool-size要按“实例数”分摊来算。比如你有10个服务实例,MySQLmax_connections是300,那每个实例的连接池上限设在20~25比较合理,留出给DBA、监控、后台任务的空间。
我之前接手过一个系统,MySQLmax_connections已经调到800了还是经常打满,后来一查,连接池max-active配了200,而且有6个服务实例,总连接数直接1200+。把每个实例降到50后,连接数立刻降下来,系统稳如老狗。
如果你用的是PHP,PDO和mysqli默认脚本执行完会自动释放连接,一般不用太担心。但如果你用了pconnect(持久连接),记得控制PHP-FPM的子进程数量,否则N个FPM进程×每进程M个数据库连接,照样能打满。
4.4 从慢查询和锁等待下手
连接堆积的另一个重要推手是慢查询和锁等待。前面提到processlist里大量Query状态的连接,多半是SQL卡住了。这里给出定位慢SQL的完整链路:
# 开启慢查询日志(临时生效) SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; -- 超过1秒记入日志 SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';跑一段时间后分析日志:
mysqldumpslow /var/log/mysql/slow-query.logmysqldumpslow是MySQL自带的工具,能把慢查询日志汇总排序,直接看“哪条SQL最耗时”。
如果是锁等待导致的连接堆积,可以执行:
-- 查看当前是否有锁等待 SELECT * FROM information_schema.innodb_trx WHERE trx_state = 'RUNNING'\G -- 查看锁等待关系 SELECT * FROM sys.innodb_lock_waits\G当一条UPDATE卡在锁等待上,它占用的连接会被拖住,后续同表的写入请求又都要等待,连接数自然越积越多。这种问题调max_connections没用,得处理锁等待的根因——通常离不开“长事务”和“大批量更新”。
查长事务:
SELECT trx_id, trx_started, trx_state, trx_mysql_thread_id, trx_query FROM information_schema.innodb_trx WHERE trx_state = 'RUNNING' ORDER BY trx_started ASC;trx_started时间最早的,基本就是“罪魁祸首”。确认是业务事务执行太久后,再决定是优化SQL、分批提交,还是杀会话:
-- 杀掉长时间持有锁的会话(trx_mysql_thread_id) KILL <thread_id>;这一套组合拳下来,连接堆积问题基本能缓解。
4.5 扩展:MySQL 8.0的admin connection
MySQL 8.0.30之后引入了一个新特性:管理员通过admin账号连接时,即使max_connections打满了,也仍然有独立的管理连接通道可用。这比mysqld_safe --skip-networking优雅多了——不用重启服务。
启用方式:
SET GLOBAL admin_address = '0.0.0.0:33062'; SET GLOBAL admin_port = 33062;然后创建管理员账号:
CREATE USER 'admin'@'localhost' IDENTIFIED BY 'strong_password'; GRANT ALL PRIVILEGES ON *.* TO 'admin'@'localhost' WITH GRANT OPTION;通过这个专用端口连接,即使普通连接全满也能进去:
mysql -h127.0.0.1 -P33062 -uadmin -p我对这个功能的评价是:早该有了。MySQL占满连接后管理员进不去一直是个老毛病,8.0.30之后的版本建议大家都开起来。
5. 实战复盘:一次完整的处理过程
5.1 场景还原
上个月我处理过一次典型的Too many connections故障,场景是这样的:CentOS 7.9,MySQL 5.7.40,某个内部管理系统在下午3点左右开始报“Too many connections”,业务侧研发慌慌张张跑来找我。
我当时的排查路径是这样走的:
# 先看MySQL服务状态和端口 systemctl status mysqld ss -tanp | grep 3306 | wc -lss命令查出来连接数已经接近上限(当时的max_connections是200)。
然后尝试登录MySQL:
mysql -uroot -p结果直接报:
ERROR 1040 (HY000): Too many connections5.2 救火阶段
因为版本是5.7,不支持8.0的admin通道,我选择直接重启进--skip-networking:
systemctl stop mysqld mysqld_safe --skip-networking &大约等了10秒MySQL起来了,这会用socket登录:
mysql -uroot -p进去后第一件事:
SHOW STATUS LIKE 'Threads_connected'; SHOW STATUS LIKE 'Max_used_connections'; SHOW STATUS LIKE 'max_connections';结果:Threads_connected=200,Max_used_connections=200,max_connections=200——顶格打满了。
再看进程列表:
SELECT user, host, COUNT(*) AS cnt FROM information_schema.processlist GROUP BY user, host ORDER BY cnt DESC;结果很清晰:一个Java应用账号占了140多个连接,另外有20多个连接来自定时任务服务器。
再细看这些连接的状态:
SELECT id, user, host, db, command, time, LEFT(info, 100) AS query FROM information_schema.processlist WHERE user = 'app_user' ORDER BY time DESC;大量连接command=Sleep,time从60秒到几百秒不等,SQL是空的——典型的连接池空闲连接。
5.3 参数调整与业务协商
判断是连接池空闲连接过多导致的打满,先临时调大参数保业务:
SET GLOBAL max_connections = 500; SET GLOBAL wait_timeout = 300; SET GLOBAL interactive_timeout = 300;同时修改配置文件,防止重启失效:
[mysqld] max_connections = 500 wait_timeout = 300 interactive_timeout = 300 max_connect_errors = 100000这里注意:wait_timeout改成300后,已有的Sleep连接不会立刻被清掉,新连接才生效。为了让参数尽快起作用,最好是把那些长时间空闲的连接直接杀掉。
批量杀掉空闲连接:
-- 杀空闲超过300秒的Sleep连接 SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist WHERE command = 'Sleep' AND time > 300 AND user NOT IN ('root', 'admin');把生成的KILL语句手动执行一遍,或者直接在客户端工具里批量执行。这个操作杀伤力可控,因为只杀空闲连接,不影响正在跑SQL的业务。
5.4 应用侧的整改
临时救回来了,接下来是治本。和研发确认后发现:Java应用用的是Druid连接池,maxActive配了200,而服务是双节点部署——两个节点加起来最多400个连接,MySQL才500上限,再算上定时任务和其他运维工具,肯定挤爆。
最终把每个节点的Druid配置调成:
initial-size: 10 min-idle: 10 max-active: 50 max-wait: 30000这样两个节点最多100个连接,加上定时任务的几十个,MySQL 500的上限绰绰有余。
配合侧还加了监控和告警:
- 定时采集
Threads_connected和Max_used_connections到监控平台 - 当连接数达到上限80%时发出告警
- 新增每周慢查询日志分析
这套整改之后,这个系统的连接数一直稳定在100左右,再没出现过Too many connections。
6. 日常预防:把Too many connections扼杀在摇篮里
6.1 建立连接数监控和告警
不要等连接打满才发现问题,日常监控才是关键。我常用的监控指标有:
| 指标 | 含义 | 告警阈值建议 |
|---|---|---|
| Threads_connected | 当前连接数 | 达到max_connections的80%时告警 |
| Max_used_connections | 历史最大连接数 | 连续多次超80%时告警 |
| Threads_running | 正在执行的连接数 | 超过50时告警 |
| Connection_errors_max_connections | 因连接数满而失败的次数 | 大于0就告警 |
可以用系统自带的zabbix、prometheus+grafana,也可以用简单的crontab脚本采集。我之前在CentOS上用pt-stalk和自写Shell脚本采集过,效果也不错。监控的意义在于:连接数涨到150的时候你就知道,而不是等到200才被业务喊起来。
6.2 合理规划连接池参数
前面讲过一次连接池配置,这里再给一个常见的估算方法:
假设有几类使用者:
- 应用服务A:双节点部署,每节点连接池上限30
- 应用服务B:单节点部署,连接池上限20
- 定时任务C:脚本并发最大10
- DBA和运维:预留5~10
总需求大约是:2×30 + 1×20 + 10 + 10 = 100
那么MySQL的max_connections设置在150~200就是合理区间,余量充足且不会浪费内存。连接池侧一定要设置max-wait,避免连接获取不到时无限阻塞。
连接池调优还有一个细节:连接池的max-active不能比MySQL的max_connections还大,这听起来像废话,但我见过好多次这样的配置,必须要列出来提醒。
6.3 定期检查慢查询和锁等待
慢查询堆积是连接数打满的重要推手之一。建议每周跑一次慢查询分析,重点关注:
- 执行时间超过1秒的SQL数量是否在增长
- 有没有新出现的“烂SQL”
sys.innodb_lock_waits里有没有频繁锁等待
定时任务脚本在跑大批量数据更新时,建议拆分成小批次(比如每次1000条),避免长事务长时间占用连接和锁。
6.4 给OR开启预留通道
如果你的MySQL版本在8.0.30以上,强烈建议把admin_address配置写进my.cnf:
[mysqld] admin_address = 0.0.0.0:33062这样每次出现Too many connections的时候,你还能走管理端口进去排查,不至于每次都要重启MySQL来救火。旧版本配合--skip-networking方案,也建议提前写一份操作手册,真出事的时候照着执行,能省不少时间。
7. 常见问题与避坑清单
7.1 问与答
问:调大了max_connections还是打满?
答:先看processlist里大量连接是什么状态。如果是Sleep,看连接池空闲连接配置;如果是Query,看慢查询;如果来自某个特定账号,查那个业务系统的连接数设置。参数不是万能药,要找到占用大头。
问:SET GLOBAL max_connections=500后马上生效吗?
答:生效,但已建立的连接不受影响。重启MySQL后如果没改配置文件,会回到原来的值。
问:杀掉Sleep连接会不会影响业务?
答:Sleep表示连接空闲、没有在执行任何SQL,杀了一般不影响业务。但要注意:某些应用框架连接池里有缓存机制,连接被服务端杀掉后会自动重连,可能造成瞬时大量重连请求,建议杀之前确认业务有重连机制。
问:CentOS上MySQL 5.7和8.0处理方式有什么不同?
答:基本相同的排查思路和命令。主要区别是8.0.30以上版本支持admin_address管理通道;8.0的my.cnf里innodb_buffer_pool_size默认更大,调max_connections时多留意内存占用。
问:max_connections设多少算“合理”?
答:没有一个万能数字。合理的值是“业务峰值连接数 × 1.2~1.5”,同时考虑服务器内存。服务器内存8G、innodb_buffer_pool_size默认128M的场景下,300已经偏高了。内存不宽裕时,我宁可把连接池压小、也不愿一口气开到1000。
7.2 我踩过的一些坑
先说第一个坑:误杀root连接。我见过有人为了清空闲连接,把processlist里所有Sleep都杀了,结果把root的管理连接也咔嚓了,后面想连就难了。杀连接时一定要排除管理账号。
第二个坑:wait_timeout调太小导致连接频繁重建。有次我把wait_timeout调到60秒,结果连接池里的连接每1分钟被MySQL断一次,应用侧频繁重连,负载不降反升,最后调回300秒才恢复正常。超时时间短了反而产生更多重连开销,这个trade-off要把握好。
第三个坑:修改max_connections后没改文件。之前直接在命令行SET GLOBAL max_connections=500,当时一切正常,结果某次机房断电重启,MySQL回落到默认151,业务直接停摆。后来养成习惯,改任何MySQL参数都同时改配置文件,一步都不能省。
第四个坑:连接数打满不等于内存充足。之前为了图省事,直接把max_connections从200调到2000,结果MySQL内存狂飙,触发OOM被杀。连接不是免费的,每一条都要吃内存,服务器物理内存决定上限,不要拍脑袋调大。
7.3 一份可以直接用的检查清单
最后整理一份我在处理这类问题时从头到尾的检查清单:
- 能否正常登录MySQL?不能的话,走
--skip-networking或8.0的admin端口。 - 查看
Threads_connected、Max_used_connections、max_connections三个值。 - 查看
processlist按用户、状态分组统计。 - 定位空闲连接(
Sleep)和慢查询(Query时间长的)分别有多少。 - 临时扩容:
SET GLOBAL max_connections,同时改my.cnf。 - 应用侧检查连接池配置,计算实例总连接数是否超标。
- 调整
wait_timeout和interactive_timeout,避免空闲连接堆积。 - 开启慢查询日志,定位拖垮连接的SQL。
- 检查锁等待和长事务,必要时
KILL阻塞会话。 - 配置连接数监控告警,给8.0版本预留
admin_address。
这套流程走一遍,绝大多数Too many connections问题都能得到妥善处理。以后再有研发跑来说“数据库连不上了”,你也可以淡定地打开终端,按节奏一步步查,不用再手忙脚乱地重启服务了。