网站突然打不开了,Nginx报错“502 Bad Gateway”或者“Connection refused”,PHP日志里全是“SQLSTATE[HY000] [1040] Too many connections”。这个报错一出现,基本就宣告了MySQL的连接池已经彻底满了,新来的请求连数据库的大门都进不去,只能在外面排队,直到超时或被拒绝。
1040错误是MySQL里最常见的生产事故之一,而且它有个特点:一旦出现,恢复起来并不难,但如果你不懂原理,光靠重启MySQL或者重启PHP-FPM,只能撑几分钟,问题马上卷土重来。这篇文章不扯虚的,直接讲连接数到底是怎么耗尽的、怎么查、怎么调、以及怎么从根本上避免再犯。
先搞清楚连接数到底被谁吃了?
MySQL的max_connections是一个全局上限,它限制了同时能建立的客户端连接数量。默认值通常是151,对于稍微有点并发量的网站来说,这个数值确实偏小。
要查当前的连接数上限是多少,直接进MySQL执行:
SHOW VARIABLES LIKE 'max_connections';
然后看当前实际占用了多少连接:
SHOW STATUS LIKE 'Threads_connected';
如果Threads_connected的值接近max_connections,那说明连接池已经快要满了。再看一个更关键的指标——历史峰值:
SHOW STATUS LIKE 'Max_used_connections';
这个值记录了从MySQL启动以来,同时连接数的最高记录。如果这个数值已经达到或超过了max_connections的85%以上,就说明你的连接数上限设置偏低了,峰值时期不够用。
但仅仅知道用了多少连接还不够,你还得搞清楚这些连接是谁占着的。执行下面这条命令,看看当前所有连接的状态分布:
SHOW PROCESSLIST;
输出里会列出所有活跃连接的ID、用户、来源IP、数据库、执行状态、执行时间等信息。重点关注几个维度:
- Time列:如果某个连接的执行时间超过几十秒甚至几百秒,说明它可能卡住了。慢查询、锁等待、死循环SQL都有可能导致连接长时间不释放。
- State列:如果大量连接处于“Sleep”状态,说明这些连接是空闲的,应用程序没有及时关闭它们。
- Host列:如果大量连接来自同一个IP,可能是某个应用服务器配置了过大的连接池,或者PHP-FPM的进程数设置得过高。
如果看到成百上千个Sleep状态的连接,问题就很清楚了:你的应用程序在请求结束后没有主动关闭数据库连接,或者连接池的回收机制没配置好,导致连接一直被持有。这时候光调大max_connections,只是把问题往后推,并没有解决根本。
临时救急:先让网站恢复访问
服务器报1040错误的时候,最紧急的任务是释放一些连接,让新请求能进来。这时候你不需要重启MySQL,更不需要重启服务器,直接进MySQL命令行,手动杀掉一些长时间闲置的连接:
SHOW PROCESSLIST;
找到那些Time值很大、State是Sleep的连接,记住它们的Id,然后逐个杀掉:
KILL 连接ID;
如果Sleep状态的连接太多,一个一个杀效率太低,可以写一个简单的批量处理脚本。在MySQL命令行里直接执行下面这段SQL,它会生成所有Sleep连接对应的KILL命令:
SELECT CONCAT('KILL ', id, ';') FROM information_schema.processlist WHERE command = 'Sleep' AND time > 60;
把输出的KILL命令复制出来,一次性全部执行。这里的 time > 60 表示杀掉那些空闲超过60秒的连接,你可以根据自己的情况调整阈值。
杀掉一批Sleep连接之后,Threads_connected数值会立刻降下来,新请求就能进来了,网站恢复正常访问。
但这只是临时手段。如果你不解决连接不被释放的根本原因,过不了多久Sleep连接又会堆满,问题会反复出现。
调大max_connections:治标的操作
在找到根本原因之前,先把max_connections调大,给系统争取一些缓冲时间。但这个操作不是无限制的,每增加一个连接,MySQL都要分配一块内存给它,连接数设得太大,内存可能扛不住。
修改MySQL配置文件 /etc/my.cnf 或 /etc/mysql/mysql.conf.d/mysqld.cnf,在 [mysqld] 段里加上或修改:
[mysqld]
max_connections = 500
改完之后重启MySQL服务:
systemctl restart mysql
# 或
systemctl restart mariadb
重启生效后,确认一下新值是否已经应用:
SHOW VARIABLES LIKE 'max_connections';
但要注意,max_connections设得太高是有风险的。MySQL为每个连接分配的线程栈大小由 thread_stack 控制,默认通常是256KB或512KB,加上网络缓冲区、临时表等开销,每个连接大约会占用2-4MB的内存。如果max_connections设成1000,那么仅连接管理这一块就可能占用2-4GB的内存。如果你的服务器内存只有2GB或4GB,设1000个连接很可能直接把MySQL进程撑爆,导致更严重的问题。
一个粗略的安全计算公式是:max_connections = (可用内存 - 其他服务占用) / 每个连接平均内存占用。不确定的情况下,保守一点,500以内对大多数云服务器来说是比较安全的数字。
根本原因之一:PHP-FPM连接没释放
很多PHP环境使用PHP-FPM管理进程,每个PHP-FPM进程在执行完脚本后,默认会保持MySQL连接不断开,这就是所谓的“持久连接”或 pconnect。如果PHP代码里用了 mysql_pconnect() 或PDO的持久连接选项,FPM进程不会在请求结束后释放连接,而是把连接保留在连接池里,供下一个请求复用。
这个机制本身是为了提高性能设计的,但如果PHP-FPM的进程数太多,每个进程都持有一个MySQL连接,那MySQL的连接数就会等于PHP-FPM的进程数。当PHP-FPM的 pm.max_children 设为100,而MySQL的max_connections是150,光PHP-FPM就能占用100个连接,留给其他服务(如后台任务、工具脚本)的空间只剩下50个,很容易打满。
解决方案有三个方向:
1. 在PHP代码里禁用持久连接,使用短连接。每次请求结束后连接自动释放,但频繁建立销毁连接会增加开销。
2. 减少PHP-FPM的进程数,调低 pm.max_children 的值。
3. 如果业务确实需要持久连接,适当调高MySQL的 wait_timeout 和 interactive_timeout 参数,让空闲连接存活更久,避免频繁重建,同时结合 max_connections 预留足够的余量。
具体调整哪个,取决于你的业务场景。如果一个请求需要频繁访问数据库且连接开销大,持久连接有意义;如果只是简单的展示站,用短连接更省心。
根本原因之二:慢查询堆积
另一个常见的连接耗尽原因是慢查询。如果某个SQL查询跑了10秒才返回,而这个查询的并发量又很大,那么在这10秒之内,每个请求都会占用一个MySQL连接。并发100个请求就要占用100个连接,持续时间越长,连接积累越多,最终撑满。
开启MySQL的慢查询日志,找出那些执行时间过长的SQL:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
然后查看慢查询日志的位置:
SHOW VARIABLES LIKE 'slow_query_log_file';
找到日志后,分析哪些SQL频繁出现且执行时间很长。常见的优化手段包括:加索引、改写SQL语句、拆分大事务、把复杂的联表查询拆成多次简单查询、引入缓存层(如Redis)减少数据库的读压力。
根本原因之三:应用程序连接泄漏
最常见的应用层连接泄漏场景是:代码里建立了数据库连接,但在异常处理分支里忘记调用 mysqli_close() 或 PDO::null 释放连接。一旦程序抛出异常跳过了关闭逻辑,这个连接就一直挂在MySQL那里,直到超时被服务端强制回收。
排查方法是在代码全局搜索数据库连接对象的创建和销毁逻辑,确保所有执行路径——包括try-catch的finally块或析构函数——都能正确释放连接。使用连接池的框架要检查池的最大容量和回收策略是否配置合理。
合理的监控和告警
调优做完之后,建议在监控系统里加上对MySQL连接数的监控。无论是Zabbix、Prometheus还是云服务商自带的监控,都可以配置:
当 Threads_connected 超过 max_connections 的80%时,发出预警。
当 Max_used_connections 持续逼近 max_connections 时,触发告警。
有了监控,你可以在连接数真正打满之前发现趋势,提前介入处理,而不是等到网站挂了才去排查。
最后说一个容易被忽略的点:MySQL的 wait_timeout 默认是28800秒(8小时),这意味着一个Sleep状态的空闲连接如果没被应用程序主动关闭,最长要等8小时才会被MySQL服务端自动回收。这个值设得太长,会让大量僵尸连接长时间占用连接池。在云服务器场景下,建议把这个值适当调低,比如600秒(10分钟):
[mysqld]
wait_timeout = 600
interactive_timeout = 600
这样即使应用层偶尔泄漏了连接,MySQL也能在10分钟后自动清理掉,不至于一直堆积到打满。不过也要注意,调低 wait_timeout 后,如果应用使用了长连接,会出现连接被服务端断开后重新建立的额外开销,需要根据实际情况权衡。
相关内容
