结论是连接归还逻辑缺失或错误导致连接无法回收;需定位未归还路径、验证归还时机、检查连接池used/idle/waitCount状态,异常分支必须用try/finally确保push,结合MySQL端SHOW PROCESSLIST和生命周期标记追踪。

直接说结论:不是“忘了释放连接”,而是连接归还逻辑缺失或错误,导致连接卡在使用中无法回收;排查重点是定位未归还路径、验证归还时机、检查连接池状态变化。
看连接池实时使用情况
连接池是否真被耗尽,不能只看报错,要查实际状态:
- 在代码关键位置(比如 query 前后、事务结束处)加日志,打印
$pool->getStats(),关注used、idle、waitCount三个字段 - 如果
used持续上涨不回落,且idle长期为 0,基本确认连接没归还 - 用
curl http://127.0.0.1:9501/status(假设启用了 HTTP 管理端口)或自定义 status 接口暴露池子统计,避免重启才能看到数据
抓异常路径下的归还遗漏点
90% 的“忘记释放”其实发生在异常分支里,比如 try/catch 后没写 finally,或协程中途退出:
- 所有
$pool->pop()后必须配对$pool->push($mysql),且必须包裹在try/finally中,不能只靠业务逻辑结尾去 push - 特别注意:协程 sleep、await 异步调用、throw Exception、return 提前退出这些场景,都会跳过后续代码,导致 push 被跳过
- 示例修复写法:
<?php
$mysql = $pool->pop();
try {
$result = $mysql->query('SELECT ...');
// 其他操作
} finally {
$pool->push($mysql); // 这行必须存在,且不能被任何条件绕过
}
用 MySQL 端反向验证连接归属
应用层看不见的泄漏,数据库端往往有痕迹:
- 登录 MySQL,执行
SHOW PROCESSLIST;,观察Command列是否大量为Sleep,Time值持续增长(比如 > 60 秒) - 重点看
User和Host是否集中来自你的 Swoole 服务 IP,说明连接确实由你发起但没断开 - 配合
SELECT * FROM performance_schema.threads WHERE TYPE = 'FOREGROUND' AND PROCESSLIST_COMMAND = 'Sleep';查更细线程信息 - 若发现大量 Sleep 连接对应同一个应用进程 PID,基本锁定该 worker 内存在归还缺失
加连接生命周期标记辅助追踪
在连接 pop 出来时打唯一标记,push 回去时校验是否匹配,能快速发现“借了没还”或“还错池子”:
- 扩展连接池,在
pop()时给$mysql实例加一个__trace_id属性(如uniqid('c-')) - 在
push()前检查该 ID 是否存在,不存在就告警并丢弃(防止无效对象混入池) - 同时记录每次 pop/push 的时间戳和协程 ID(
Swoole\Coroutine::getuid()),出问题时可回溯哪条协程拿了连接却没还


















