MySQL 连接数爆满排查实战:Too many connections 的 Sleep 堆积判读、连接池算法与 wait_timeout 兜底

现象:网站突然全线 500,报错指向数据库连接数

晚上十点多,监控开始报警:网站开始间歇性 500,刷新几次又能打开,过一会儿又挂。登录服务器看 PHP 错误日志,刷的都是同一句话:SQLSTATE[HY000] [1040] Too many connections。用 mysql 命令想进去看看,连自己都被拒绝了——这个报错最气人的地方在于,它连"诊断用的连接"都不给你留。

这篇文章讲清楚三件事:MySQL 的连接数到底由哪些参数共同决定、为什么会突然打满、以及在不重启数据库的前提下如何一步步把连接压回去。个人站长的服务器资源有限,连接数爆满往往不是"量太大",而是"连接池配错"或"有代码在泄漏连接"——这两种问题都不用换服务器就能解决。

先理解连接数:三个参数决定上限,不是只有 max_connections

大多数人以为连接数上限就是 max_connections,其实真正生效的上限是三个值里最小的那个:

SHOW VARIABLES LIKE 'max_connections';
SHOW VARIABLES LIKE 'max_user_connections';
SHOW VARIABLES LIKE 'thread_cache_size';
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_running';
SHOW STATUS LIKE 'Max_used_connections';
SHOW STATUS LIKE 'Aborted_connects';
  • max_connections:实例级总上限,默认 151。它决定了整个 MySQL 最多同时开多少条连接。
  • max_user_connections:单个用户级上限。即使总量没满,某个应用账号也可能先撞到这个天花板,报错同样是 1040。
  • thread_cache_size:线程缓存池大小。它不影响上限,但直接影响"连接建立成本"——缓存太小,每次连接都要新建线程,高峰期雪上加霜。

关键要用 Threads_connected(当前已建立连接)和 Threads_running(当前真正在执行的连接)这一对来区分故障类型。如果 connected 很高但 running 很低,问题不在数据库忙,而是连接只开不还——这是连接池或应用代码的问题。反过来,如果 running 也一直很高,那才是真的慢查询把连接占死了。

再看 Max_used_connections,这是历史峰值,它告诉你"最高峰时用到过多少连接"。把 Max_used_connections / max_connections 作为一个参考比例:如果长期贴近 100%,说明上限设得太紧,任何突发流量都会撞墙。

第一步:不改配置先看清楚谁在占连接

连接已经打满时,第一件事不是去调大参数,而是看看这些连接都是谁。MySQL 从 5.7 起提供了按用户、按主机、按库分组的连接统计,几个视图一句话就能看出分布:

SELECT user, host, COUNT(*) AS conn
FROM information_schema.processlist
GROUP BY user, host
ORDER BY conn DESC;

SELECT command, state, COUNT(*) AS n
FROM information_schema.processlist
GROUP BY command, state
ORDER BY n DESC;

正常状态下,绝大多数连接应该处在 Sleep 状态。如果 Sleep 连接占比超过九成——这就是典型的"连接泄漏"特征:应用打开了连接却没归还到池子里,它们一直占着名额,真正的请求来了反而抢不到连接。

想进一步看是哪个应用账号在泄漏,按 user 分组就够用了。个人站点一般是"网站 + 一个后台脚本 + 一个监控",如果某个账号占了几百条 Sleep 连接,基本可以锁定是它的连接池配置有问题。

急救援手:如果不能重启应用,又必须马上恢复服务,可以先从 Sleep 且空闲时间过长的连接开始杀:

-- 先看要杀谁:空闲超过 300 秒的连接
SELECT id, user, host, time, state
FROM information_schema.processlist
WHERE command = 'Sleep' AND time > 300
ORDER BY time DESC;

确认无害后,用 KILL <id> 逐条终止。注意只杀 Sleep 状态的连接,正在执行查询的连接被 KILL 会导致事务回滚,可能引发数据问题。杀之前务必确认这些连接不是你正在跑的备份或导入任务。

第二步:定位连接泄漏的三类典型代码

连接数反复打满,根因八成在应用层。常见的三类问题:

一是每个请求都新建连接。 PHP 里用 PDO 时,如果每次都在函数内部 new PDO() 而不复用,并发一上来连接数就是请求数。正确做法是用单例或者交给框架的连接管理器,并且开启持久连接时要谨慎——持久连接在 PHP-FPM 里会跨请求复用,反而可能因为 worker 数量多而堆积连接。

二是异常路径没关连接。类似 try { ... } catch { return; } 的写法,异常分支直接返回,连接没释放。养成的习惯是:任何可能抛异常的逻辑,都要保证连接最终被关闭,别指望 GC。

三是连接池配置与 MySQL 上限不匹配。这是最隐蔽的一类。连接池的 maxPoolSize 之和如果超过了 MySQL 的 max_connections,平时没事,一旦所有应用实例同时打满,数据库就会拒绝新连接并报 1040。个人站长常犯的错是同时跑了网站、定时任务、队列消费者三套东西,每套都配了 20 条连接池,加起来正好超过默认的 151。

一个实用的自检方法:把每个应用的连接池上限加起来,和 max_connections 对比。经验法则是把所有池上限之和控制在 max_connections 的七成左右,留出的三成给运维连接、监控和突发。

第三步:配置层如何调,以及为什么不要无脑调大

排除了泄漏之后,如果确实是正常业务量增长,才轮到调参数。改动写进配置文件并重启生效:

[mysqld]
max_connections = 300
thread_cache_size = 64
wait_timeout = 300
interactive_timeout = 300
max_connect_errors = 1000

每一行都有意义:

  • max_connections 从 151 提到 300,给突发留余量。但不要无脑调大:每条连接都要占内存(排序缓冲、连接缓冲等),连接数上去了,内存压力会跟着上去,极端情况下反而触发 OOM。
  • thread_cache_size 调到 64 左右,让断开后的线程能被复用,减少高峰期的线程创建开销。
  • wait_timeout / interactive_timeout 从默认的 8 小时(28800 秒)降到 300 秒。这是止血最有效的一招:空闲连接 5 分钟就被回收,泄漏的连接不会无限累积。把它当作一道兜底防线,而不是主要方案。
  • max_connect_errors 放宽一些,避免因为网络抖动累积 error 计数后,把正常主机拉入黑名单(\`FLUSH HOSTS\` 可以手动解除)。

调完 wait_timeout 要注意一个连带影响:如果应用持有长连接却不发心跳,连接可能被服务端提前断开,应用再用这条连接时会报 "MySQL server has gone away"。所以调小超时的同时,应用的连接池必须配置验证查询(如 SELECT 1)或合理的空闲检测。

第四步:把连接数纳入监控,别再等报警

连接数这类指标最好的做法是提前预警,而不是等 500 出现。一个极简的采集脚本,写进 crontab 每分钟跑一次:

#!/bin/bash
# 连接数监控:超过阈值就写日志并触发告警
THRESHOLD=250
CONN=$(mysql -N -e "SHOW STATUS LIKE 'Threads_connected'" | awk '{print $2}')
RUNNING=$(mysql -N -e "SHOW STATUS LIKE 'Threads_running'" | awk '{print $2}')
if [ "$CONN" -gt "$THRESHOLD" ]; then
  echo "$(date '+%F %T') CONN=$CONN RUNNING=$RUNNING" >> /var/log/mysql-conn-watch.log
fi

监控的关键是同时记录 Threads_connected 和 Threads_running。只看 connected 会误判:连接多不代表数据库忙;两个一起看,才能立刻分清是"连接泄漏"还是"真的慢查询堆积"。前者去改应用,后者去优化 SQL,方向完全不同。

阈值建议设在 max_connections 的 80% 左右。这样在真正打满之前,你至少有几分钟的反应时间,而不是接到用户投诉才知道。

把这些排查串起来看,连接数爆满的处理有一条清晰的顺序:先看视图分清 Sleep 堆积还是 running 堆积 → 杀超时空闲连接止血 → 从代码和池配置找泄漏 → 最后才是调参数兜底 → 用监控提前预警。跳过前面几步直接调 max_connections,只是把故障推迟到下一次更高的流量,问题的根还在那里。

补充一:PHP-FPM 场景下连接池该怎么算

对大多数个人站长来说,应用层就是 PHP-FPM。这套架构下算连接上限有一个容易忽略的乘数关系,必须讲清楚,否则调参总是猜。

PHP 是"每请求一进程"的模型,连接池往往不存在于 PHP 进程内部,而是靠持久连接(persistent connection)实现跨请求复用。关键点在于:持久连接是绑定在某个 FPM worker 进程上的,一个 worker 进程同一时刻只持有它自己那条持久连接。所以理论上的最大连接数约等于:

最大连接数 ≈ pm.max_children + 其他独立应用(定时任务/队列/监控)

也就是说,pm.max_children 是多少,MySQL 高峰期就可能收到多少条连接。个人站点常见的配置是 pm.max_children = 50,再加上定时任务脚本、后台队列、监控采集各自的连接,总数很容易顶到 60 到 100 之间。如果你同时又没把 max_connections 从默认的 151 提上去,并发一高立刻 1040。

所以正确的算法是:先统计所有会连数据库的进程上限之和,再让 max_connections 略高于这个和,而不是反过来。一个稳妥的配比是:

max_connections ≥ pm.max_children + 队列 worker 数 + 定时任务并发 + 20(运维/监控余量)

还要注意持久连接本身的一对矛盾:它减少了连接创建开销,但会让连接在 FPM worker 空闲时依然保持占用而不释放。如果 pm.max_children 配得很大,高峰期过后这些连接会一直挂着,Threads_connected 迟迟降不下来,看起来就像"连接泄漏"。这正是前面把 wait_timeout 降到 300 秒的价值所在——它给持久连接加了一道自动回收的闸门。

补充二:为什么"连接数报错"有时根本不是连接数的问题

排查到最后还有一个反直觉的坑值得单独说:你看到的 1040 报错,可能只是更深层故障的表象。

设想一种情况:某张表被一个没提交的事务锁住,所有后续查询都堵在锁等待上。这些查询各自占着一条连接不释放,连接池很快被耗尽,应用开始报 1040。这时候你去调 max_connections,只会让更多查询挤进来一起堵,问题反而更糟——真正的病根是那个长事务。

所以每次遇到 1040,都要顺手做一次交叉检查,分清是"连接管理问题"还是"执行阻塞问题":

-- 看有没有长时间运行的查询/事务
SELECT id, user, time, state, LEFT(info, 60) AS query
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 10
ORDER BY time DESC;

-- 看 InnoDB 锁等待情况
SHOW ENGINE INNODB STATUS\G

如果 processlist 里有一堆连接卡在同样的 state 上、time 都很长,那问题不在连接数,而在执行阻塞——去查那个罪魁祸首的慢查询或长事务。反过来,如果所有卡住的连接都是 Sleep,才是真正的连接泄漏或上限太小。

这两种情况的处理方向完全相反:前者要"疏通阻塞"(杀长事务、优化 SQL),后者要"限制连接"(改池配置、调超时)。判别标准就一句话:卡住的是 Sleep 连接,还是 running 连接。把这一条判断做对,能省掉大量无效的调参时间。

Last modification:September 26th, 2026 at 07:26 pm

Leave a Comment