虚拟主机数据库连接数超限排查:慢查询与连接池优化

[虚拟主机](https://cn.hostease.com/shared-hosting.html)报“连接数超限”,先分清是哪一种超限

数据库连接数超限示意图:大量请求挤向有限的数据库连接通道

本篇指南教你如何用面板和 SQL 两端确认自己撞的到底是哪一道墙,再用连接池参数与慢查询优化把峰值连接数压回去,整个过程不需要重启数据库。网站突然大面积返回 500 时,日志里通常只有两条报错:Too many connections 说明整台数据库服务器同时接入的客户端总数被打满,max_user_connections 相关报错则说明你这个数据库账号的并发连接数已经用完。两者指向完全不同的上限,处理动作也完全不同,很多站长的第一反应是“数据库坏了”或者“该升级套餐了”,结果往往改错了地方。

先纠正一个几乎所有人都踩过的误解:连接数上限不是“网站能同时接待多少访客”,而是“同一时刻有多少条数据库会话处于打开状态”。一条会话从建立到关闭可能只持续几毫秒,也可能以 Sleep 状态挂满 8 小时——MySQL 的 wait_timeout 默认值是 28800 秒。只盯着访问量数字看,永远解释不了为什么日 IP 两千的小站也能把连接数打满。想把这条链路彻底搞清,建议先补一下 慢查询日志的定位方法,因为连接被长期占用的头号原因,往往就是某条拖了几秒的 SQL。

四层上限串联:别把服务器总量和账号配额混为一谈

对比示意图:宽通道的服务器全局上限与窄门框的单账号配额

排查之前必须先把四层限制拆开,因为它们归属不同的管理者,能改的权限也完全不同。

  • max_connections:数据库服务器全局允许的最大并发会话数,MySQL 8.0 默认 151。共享环境的用户无权修改这一项,看到 1040 报错说明整台机器被打满,只能减少自身占用或联系主机商核查。
  • max_user_connections:单个数据库账号的并发上限,虚拟主机通常设在 10 到 30 之间。报错 1226 就是撞了这一层,问题基本都出在自己站点的连接泄漏或瞬时并发。
  • PHP 进程数:每个 PHP-FPM 子进程在同一时刻最多持有一条数据库连接,所以 pm.max_children 实际上决定了连接数的理论上限,50 个子进程就是 50 条潜在连接。
  • 应用连接池:程序侧复用连接的容器。池子上限开得比数据库配额还大,等于把排队压力从数据库搬到了报错日志里。

这四层是串联关系,实际可用值由最窄的那一层决定。假设账号配额是 20、PHP 子进程配了 50,那么第 21 个并发请求立刻开始报错,剩下的 30 个进程只是在排队制造异常;反过来,如果 pm.max_children 只有 5,即便账号配额给到 30,你的站点也只能同时处理 5 个动态请求。木桶最短的那块板在哪里,决定了你该先动哪一个参数,所以下面先做定位,再谈调参。

三组命令定位:谁在占着连接不放

确认撞的是账号层上限之后,下一步是找出占用者。虚拟主机一般只提供 phpMyAdmin,那就直接在 SQL 窗口依次执行下面三条查询,前两条看水位,第三条看个体。

  • SHOW STATUS LIKE 'Threads_connected'; —— 当前打开的会话总数,连续采样几次就能看出它是否只涨不落。
  • SHOW STATUS LIKE 'Max_used_connections'; —— 数据库启动以来的峰值连接数,除以账号配额超过 80%,说明配额长期偏紧。
  • SELECT ID, USER, HOST, TIME, STATE, LEFT(INFO,60) FROM information_schema.processlist WHERE COMMAND='Sleep' ORDER BY TIME DESC LIMIT 10; —— 列出挂起最久的空闲连接,TIME 单位是秒。

架构示意图:网站进程经连接池转发到数据库的层级关系

三条查询的结果组合起来,基本能锁定问题类型。如果 Sleep 连接数量多、TIME 动辄几百秒,说明程序打开连接后没有归还,典型来源是持久连接的空闲超时设得比服务器 wait_timeout 更长,连接被服务端单方面断开而客户端池子里还留着记录。如果 Sleep 很少但 Threads_running 持续偏高,那连接数只是表象,真凶是执行慢的查询在占坑。至于 Aborted_connects 持续增长,通常意味着握手失败或密码错误被反复重试,与性能无关,先排查配置和凭据。这三种情况对应三套修法,混着改只会让问题更难定位。

慢查询如何把连接数放大十倍

用一个初中算术就能说明连接数为什么会被慢查询撑爆:稳态并发连接数约等于每秒查询数乘以单条查询的平均耗时。某站高峰期 150 QPS、单条查询平均 40 毫秒,只需要 6 条连接就能轻松跑完;一旦某条 SQL 因为缺索引从 40 毫秒退化到 1.2 秒,同样的流量立刻需要 180 条连接,配额 20 的环境在几秒内就会全线报错。这也是为什么“连接数超限”和“慢查询”几乎总是同时出现,而先看连接水位、再看慢日志的顺序不能颠倒。

定位慢 SQL 的动作并不复杂:把 long_query_time 从 1 秒起步,用 EXPLAIN 查看执行计划里 type 是否为 ALL,重点处理扫描行数远大于返回行数的语句。如果问题集中在表损坏、冗余数据或插件留下的统计表上,先做修复与清理,效果常常比扩连接更直接,具体命令可以参考 WordPress 数据库修复与优化中的 WP-CLI 与 phpMyAdmin 步骤。还要留意长事务和 MyISAM 表锁:一条未提交的事务会一直占着连接,锁等待则会把更多请求堵在后面,形成连接数雪崩。

连接池怎么调:四个参数决定成败

示意图:连接被循环复用回连接池的闭环路径

把慢查询处理掉之后,剩下的工作是把连接数稳稳压在配额以下。连接池的作用是复用而不是扩容,下面四个参数决定了它到底是帮忙还是添乱。

  • 池子上限取配额的七成:账号配额 20 时把池子开到 12 到 14,剩下的余量留给备份、面板操作和定时任务,避免后台跑一次导出就把前台挤死。
  • 空闲回收必须短于 wait_timeout:服务器默认 28800 秒意味着僵死连接能挂 8 小时,把池的空闲超时设成 60 到 120 秒,并在取出连接时做一次探活,可以避开 MySQL server has gone away 这类报错。
  • PHP 侧开启进程内复用:mysqli 在主机名前加 p: 前缀(如 p:localhost),或 PDO 设置 PDO::ATTR_PERSISTENT => true,两者都会复用同一进程内的连接,省掉每次请求的握手开销。
  • 同步下调 PHP 子进程:pm.max_children 与池上限之和不应超过账号配额,多出来的进程只会排队制造 1226 报错。

开启持久连接前要清楚它的副作用:连接在进程级被复用,上一次请求未提交的事务、未重置的会话变量会带到下一次请求。依赖 SET 修改会话参数的插件,必须在归还连接前显式复位,否则会出现“某个用户的设置串到别人页面上”的诡异问题。共享环境里更稳妥的路径是降低数据库访问频次——页面缓存和对象缓存把 QPS 直接砍掉一个数量级,比在连接数上抠细节收益大得多。

Hostease 的虚拟主机面板在数据库管理页会显示当前连接数与账号配额,调参之前先在那里核对真实上限,别按网上的默认值盲调。当连接水位长期贴着配额、慢查询已经清理干净、缓存也上了,仍然在高峰期报错,说明瓶颈是共享资源本身,这时候该考虑把站点搬到独享资源的环境,迁移步骤可以参考从虚拟主机迁移到云服务器(按需分配计算资源的云端主机),选型思路上共享主机与独享方案的决策方法给出了按流量和技术能力匹配的判断框架。

总结与行动建议

回顾整条排查链路:先用 Threads_connected 与 Max_used_connections 确认水位,再用 information_schema 找出长时间 Sleep 的连接,区分是连接泄漏还是慢查询占坑;慢查询按“开日志 → EXPLAIN → 加索引或改写”的闭环处理,连接池则把上限压到配额的七成、空闲回收设到 120 秒以内,并让 PHP 子进程数与池上限之和不超过配额。四步做完,绝大多数“连接数超限”都会从每天报错变成偶发告警。

建议把它做成月度例行检查:月初采样一次峰值连接数,对比上月的慢查询清单是否收敛,顺手确认空闲连接的最长存活时间有没有异常增长。如果你需要长期观察水位,可以考虑在监控里加一条 Threads_connected 曲线,超过配额 70% 就提前预警,比等用户报 500 更划算。虚拟主机套餐的数据库连接配额与资源规格可以在虚拟主机产品页核对,升级前先确认现有连接是否真的用满了。

发表评论