报错只是表象,连接被谁占满才是问题
个人网站流量并不大,但"Too many connections"这个报错却一点不少见。典型场景是:WordPress 或 Typecho 突然打不开,页面提示数据库连接错误,看一眼 MySQL 错误日志,里面刷满了这条报错。很多站长第一反应是去把 max_connections 调大,结果过两天又爆,只好再调大,直到把服务器内存吃光。
连接数上限只是压垮骆驼的最后一根稻草,真正的问题在于连接被谁占满了。本文从报错现场开始,按"看现状、找来源、做调整、根治"的顺序,讲清楚排查这类问题的完整思路。MySQL 5.7 与 8.0 的命令基本通用,个别差异会单独说明。
第一步:先确认当前的连接状况
出现报错时数据库往往还能勉强连上(空闲连接会被拒绝,但已有连接不受影响),立刻登录数据库查看状态:
mysql -u root -p
SHOW VARIABLES LIKE 'max_connections';
SHOW STATUS LIKE 'Threads_connected';
SHOW STATUS LIKE 'Threads_running';
SHOW STATUS LIKE 'Max_used_connections';四个结果分别回答四个问题:上限是多少、当前连了多少、正在执行查询的有几个、历史最高用到多少。如果 Threads_connected 长期贴着 max_connections,而 Threads_running 只有一两个,说明绝大多数连接都闲着没干活,这是典型的"连接堆积";如果 Threads_running 也很大,那多半是查询本身太慢,把连接全堵住了,这种情况优先排查慢查询而不是调连接数(慢查询定位方法见本站《MySQL 慢查询排查与索引优化》一文)。
第二步:查清楚连接都被谁占着
用 processlist 可以列出当前所有连接,但直接看全列表很费眼,更好的做法是按维度分组统计,几秒就能定位问题:
SELECT user, host, db, command, COUNT(*) AS cnt
FROM information_schema.processlist
GROUP BY user, host, db, command
ORDER BY cnt DESC;重点关注三类结果。第一类是 command 为 Query 且数量多的,说明有慢查询在堆积,去慢日志里找元凶;第二类是 command 为 Sleep 且数量巨大的,说明有连接建立了却迟迟不释放,最常见的来源是程序里忘了关闭数据库连接,或者框架开启了长连接但闲置超时设置不当;第三类是同一个 host 来源的连接特别多,比如来自同一台应用服务器的连接数异常膨胀,这时候要去查应用本身是不是在疯狂创建连接。另外用 SHOW FULL PROCESSLIST 查看具体 SQL 时,注意 State 列长时间是 Sending data、Waiting for table metadata lock 或 Waiting for handler commit 的会话,它们往往是堵住后续连接的元凶。
顺带区分一个容易混淆的报错:客户端看到 Host is blocked because of many connection errors 时,并不是连接数超限,而是同一个来源 IP 连续连接失败次数太多,触发了 max_connect_errors 的封禁保护。处理方式是登录服务器执行 FLUSH HOSTS 清空计数,同时排查这个 IP 为什么一直连不上,常见原因是程序里配错了密码或者填错了地址在反复重试。两种报错的处理方向完全不同,混为一谈会白折腾半天。
第三步:临时恢复服务
定位的同时,服务还瘫着,需要先让网站恢复。最直接的办法是把上限临时调大,MySQL 5.7 用 SET GLOBAL,8.0 可以用 SET PERSIST 让重启后依然生效:
SET GLOBAL max_connections = 500; -- 5.7:仅当前生效
SET PERSIST max_connections = 500; -- 8.0:写入配置持久化调大之前先看一眼内存:每个连接大约占用数 MB 内存(取决于 buffer pool 之外的各种缓冲),连接数翻倍内存占用也会明显上涨,1G 内存的小机器把上限从 151 调到 500 之前要掂量一下,别把数据库调爆了。如果堆积的是无用的 Sleep 连接,更立竿见影的做法是设置一个较短的 wait_timeout,让空闲连接尽快被回收:
SET GLOBAL wait_timeout = 300;
SET GLOBAL interactive_timeout = 300;注意 interactive_timeout 针对的是交互式客户端,程序连接走的是 wait_timeout,两个值最好一起设。另外强烈不建议用 kill 大法清理连接:kill 正在执行长事务的会话可能导致数据回滚甚至复制中断,kill 之前先用 information_schema.innodb_trx 确认一下会话是否在事务中。
关于上限调到多少合适,给一个粗略的估算口径:每个连接在 MySQL 内部要占用线程栈和各类会话缓冲,按单连接 2MB 到 5MB 估算比较稳妥。1G 内存、innodb_buffer_pool_size 设置在 256M 左右的机器,max_connections 设在 150 到 200 之间比较合理;2G 内存可以放宽到 300 左右;4G 以上再考虑 500。盲目设成几千,内存告急时数据库会比网站先倒下,系统日志里会出现 mysqld 被 OOM Killer 杀掉的记录。想系统规划内存类参数,可以参考本站《MySQL 内存参数调优实战》一文,里面有现成的模板可以直接套。
第四步:从应用侧根治连接堆积
临时措施撑不了几天,要根治还得回到源头。个人站最常见的病因有三个。
一是代码里连接没有正确关闭。PHP 用 mysqli 或 PDO 时,脚本结束连接会自动释放,但如果你用了全局单例、常驻进程或者把连接存进了静态变量,就要检查是否真的释放了。排查方法很简单:在代码里开启慢查询日志和通用日志对比,看建立连接的频率是否和请求量匹配。
二是长连接被滥用。PHP 的 mysql_pconnect 或者连接池方案确实能省去反复握手的时间,但长连接在 PHP-FPM 这种多进程模型下有个副作用:每个 FPM 子进程都会保持一条连接,子进程数量乘以请求并发,连接数很容易失控。小流量站点用短连接完全够用,没必要为了那几毫秒的握手时间引入长连接。
三是数据库参数和应用模型不匹配。比如 wait_timeout 默认 8 小时,如果应用创建连接后闲置很久才复用,连接就会长期占用。把 wait_timeout 调到 5 到 10 分钟,配合程序里的连接超时设置,大部分堆积问题都能缓解。线程缓存 thread_cache_size 可以适当调大(一般设为 16 到 64),让新建连接复用缓存的线程,减少反复创建线程的开销,具体内存参数的整体规划可以参考本站《MySQL 内存参数调优实战》一文。
第五步:从架构上卸掉压力
如果应用侧没问题,但连接数还是周期性冲高,就要看流量特征了。最常见的隐藏凶手是搜索引擎爬虫和恶意采集:并发爬虫瞬间发起大量请求,每个请求都建一条数据库连接,连接数就被打满了。这种场景的治本方案是在 Nginx 层限流(见本站《Nginx 限流防 CC 攻击实战》),把爬虫挡在数据库之前;同时给热点数据加一层 Redis 缓存(见本站《Redis 缓存搭建实战》),让大部分请求根本不落到数据库。
网站流量再大一些,就该考虑主从分离了:把读请求分流到只读从库,主库专心处理写操作,连接压力直接减半。MySQL 主从复制与读写分离的搭建流程可以看本站《MySQL 主从复制实战》一文,个人站一台从库足够用很多年。
预防:把连接数纳入监控
这类问题最好的处理方式是不让它发生。把 Threads_connected 和 Threads_running 加进监控告警:连接数超过上限的百分之八十就告警,Threads_running 持续大于十也告警,配合进程、端口级别的探测,基本能在网站打不开之前就收到通知。监控系统的搭建可以参考本站《Docker Compose 搭建服务器监控告警系统》一文,一条简单的告警规则,往往能帮你避免一次深夜救火。
除了监控,养成定期体检的习惯也很重要:每周看一眼 Threads_connected 的趋势、慢查询的数量和连接建立失败的错误计数,用 mysqladmin status 或者一条 SQL 就能拿到核心指标。连接数问题很少是一夜之间出现的,多数是缓慢恶化,早发现一周,就少一次半夜爬起来救站的经历。
结语
"Too many connections"是 MySQL 最典型的"症状型报错":报错本身很简单,背后的原因却千差万别。遇到它别急着调参,按本文的顺序走一遍:看连接状态、按维度分组定位来源、临时调参恢复、应用侧关闭泄漏、架构侧拦截流量,最后用监控防复发。连接数从一百多调到几千不是本事,让连接数长期稳定在低位才是。排查思路可以浓缩成一句话:先问连接被谁占着,再问为什么占着不释放,最后才轮到调参数。