连接池耗尽的典型现象是出现“no available connection”或“too many connections”错误,需通过show processlist、对比max_connections、检查hyperf日志定位未归还连接、异常未捕获、长事务阻塞等问题。

连接池耗尽的典型现象和定位方法
看到 No available connection 或 Too many connections 错误,基本可以确认是连接池耗尽。但别急着调大 max_connections —— 这往往只是掩盖问题。真实瓶颈常藏在连接没归还、异常未捕获、或长事务卡住连接上。
先快速验证:用 SHOW PROCESSLIST 查看 MySQL 当前活跃连接数;再对比 SHOW VARIABLES LIKE 'max_connections' 看是否逼近上限;最后检查 Hyperf 日志里是否有未关闭事务、Db::transaction() 缺少闭包结束、或 sleep() 类阻塞调用。
- 协程内执行
Db::table('user')->get()后没走完就抛异常 → 连接大概率没释放 - 在定时任务或 WebSocket 处理器中复用同一个
Db实例做多次查询,但没显式控制生命周期 → 连接可能被长期占用 -
wait_timeout设为 3 秒,但某条 SQL 平均耗时 4 秒 → 池子里的连接频繁被 MySQL 主动断开,后续请求拿到失效连接直接报错
启用心跳检测清理空闲/失效连接
Hyperf 的数据库连接池支持自动心跳(heartbeat),但它不是默认开启的,也不会自动重连。你得手动配,并理解它只对“空闲连接”起效 —— 正在被使用的连接不会被探测或踢出。
在 config/autoload/databases.php 对应数据库配置下加:
'pool' => [
'min_connections' => 2,
'max_connections' => 30,
'wait_timeout' => 3.0,
'max_idle_time' => 60,
'heartbeat' => true, // 必须显式开启
],
这个 heartbeat 会定期(默认每 30 秒)向 MySQL 发送 PING 命令。如果收到错误(比如 MySQL server has gone away),连接会被标记为失效并从池中移除,下次获取时就不会再分给你。
-
max_idle_time控制连接空闲多久后被回收(非心跳触发),设太短会导致低峰期频繁重建连接 -
heartbeat不等于“保活”,它不防止连接被中间设备(如 SLB)静默断开,仅用于清理已断开但池子还不知道的连接 - 若 MySQL 的
wait_timeout是 60 秒,建议把max_idle_time设为 ≤50 秒,避免池子保留一个马上要被 MySQL 关掉的连接
断线重连不能依赖客户端自动处理
Hyperf 的 MySQL 客户端(基于 Swoole MySQL 协程驱动)本身不支持自动重连。一旦连接因网络抖动、MySQL 重启或 gone away 断开,后续在这个连接上的任何查询都会失败,且该连接不会自动恢复 —— 它只会留在池子里,直到被心跳检测到并剔除。
所以关键不在“重连”,而在“换连接”。你要确保业务代码能容忍单次查询失败,并由上层逻辑兜底重试(比如幂等接口 + 重试策略),而不是指望底层悄悄续上。
- 不要在事务中依赖自动重连:
Db::transaction()内部一旦连接断开,整个事务会失败,回滚逻辑也可能因连接失效而无法执行 - 高频写场景下,可配合
try/catch捕获Hyperf\Database\Exception\QueryException,判断错误码是否为2006(MySQL server has gone away)后主动重试一次 - 避免在协程间共享
ConnectionInterface实例:每个Db::调用都应从池中取新连接,而不是缓存并复用
协程上下文污染导致的隐性连接泄漏
最隐蔽的耗尽原因:MySQL 异常污染了 Redis 或其他组件的协程上下文。例如,在同一个协程里先执行一条慢 SQL 导致连接超时断开,接着又调用 $redis->get() —— 此时 Redis 客户端可能因为上下文里残留的 MySQL 错误状态,无法正常初始化连接,最终卡在 WaitTimeoutException 上,看起来像 Redis 池耗尽,实则是 MySQL 问题引发的连锁反应。
这类问题不会出现在日志第一行,需要结合 Context::has($redis->getContextKey()) 和 Context::get($mysql->getContextKey()) 手动打点排查。
- 所有跨组件调用(DB + Redis + HTTP Client)必须隔离 try/catch,避免一个组件异常波及另一个
- 禁止在全局作用域或
Swoole\Timer::tick回调中直接调用 DB 方法 —— 这些地方没有协程上下文,连接无法自动归还 - 高风险操作(如大事务、文件导入)建议包裹在独立协程中:
Co::create(fn() => Db::transaction(...)),让上下文生命周期可控
真正难调的不是参数值,而是连接何时被创建、谁持有、什么时候该还、还给谁、还了之后有没有人再用它 —— 这些细节藏在协程调度和上下文传递里,一不留神就漏掉。











