MySQL连接池耗尽根本原因是连接未归还或配置失配,需通过SHOW FULL PROCESSLIST定位Sleep/Query异常连接,结合heartbeat与max_idle_time协同调优,并显式释放CallableStatement和ResultSet。

Hyperf 连接耗尽不是数据库扛不住,而是连接被借走没还、或池子配错导致“有连接却用不上”。 根本解法不在调大 max_connections,而在切断泄漏源头 + 配准心跳与空闲时间的协同逻辑。
查清到底是泄漏还是真高并发
先别改配置,直接连 MySQL 执行:SHOW FULL PROCESSLIST;,重点看三类连接:
-
Command是Sleep且Time持续增长 → 应用层拿了连接没归还,大概率是代码漏关CallableStatement或未消费完ResultSet -
Command是Query但Time > 30→ 慢 SQL、长事务、存储过程卡住,检查是否含SELECT ... FOR UPDATE或未提交事务 -
Info字段以CALL开头 → 存储过程正在执行,记下Id,必要时KILL CONNECTION <id>
如果大部分是 Sleep 状态且 Threads_connected 接近 max_connections,90% 是泄漏;如果全是 Query 且 Time 很小,才是真实高并发压测场景。
heartbeat 和 max_idle_time 必须配对生效
Hyperf 的 heartbeat 默认不常驻运行——它只在连接被取出时单次 PING,真正起效的是后台 IdleConnectionChecker 线程,它依赖 max_idle_time 触发检测。
-
max_idle_time必须严格小于 MySQL 的wait_timeout(查法:SHOW VARIABLES LIKE 'wait_timeout';),建议设为wait_timeout - 30 -
heartbeat值 ≤max_idle_time,推荐设为max_idle_time / 2(如max_idle_time = 270,则heartbeat = 135) - 若
max_idle_time > wait_timeout(比如 MySQL 是 60s,你设了 120s),连接会在池里“睡过头”,取出来必报MySQL server has gone away -
min_connections别设 0 或 1,冷启时所有协程抢唯一连接,一旦失效整个池就卡死;建议 ≥ 5
代码里最容易漏掉的释放点
Hyperf 使用协程连接池,Connection::close() 只归还连接,不自动清理其创建的语句和结果集。以下写法必出泄漏:
- 调用
CallableStatement::execute()后直接return,没关cs - 存储过程返回多个
SELECT,只处理第一个ResultSet就结束 - 用原生
PDO对象执行查询,但没包在try-with-resources或显式unset()
正确姿势必须显式循环消费全部结果集:
try (CallableStatement cs = $conn->prepareCall('{CALL proc_report(?)}')) {
cs->setInt(1, $tenantId);
cs->execute();
do {
try ($rs = $cs->getResultSet()) {
if ($rs !== null) {
while ($rs->next()) { /* 处理 */ }
}
}
} while ($cs->getMoreResults());
}wait_timeout 不是数据库参数,别乱调
Hyperf 配置里的 wait_timeout(默认 3.0)是协程在池中“排队等连接”的最大秒数,不是 MySQL 的 wait_timeout。设太小(如 0.1)会导致抖动式失败,日志刷屏 No available connection;设太大(如 30)会让请求无意义等待。
- 生产环境建议保持
wait_timeout => 5.0~10.0,给协程合理排队窗口 - MySQL 侧的
wait_timeout一般不动(默认 28800 秒),靠连接池的heartbeat+max_idle_time主动保活和清理 - 长周期任务(如导出、批量同步)应绕过连接池:
DB::connection()->getPdo()拿直连,避免占住池中连接
最常被忽略的是:驱动不支持 PING 时,heartbeat 实际无效,得切到 'checker' => 'SELECT 1',但该语句不能在事务中执行——这个限制不会报错,只会让心跳静默失效。


















