Hyperf MySQL协程客户端因慢查询与连接池参数错配会导致Worker卡死,根本原因是Swoole未接管MySQL协议层超时,需显式配置read_timeout及PDO::ATTR_TIMEOUT,并结合EXPLAIN优化索引与连接池参数。

Hyperf 的 MySQL 协程客户端本身不阻塞,但慢查询 + 错配的连接池参数会让整个 Worker 进程卡死——这不是并发能力问题,是配置和 SQL 双重失控。
为什么一条慢查询就能让整个协程池停摆
Hyperf 的 Swoole\Coroutine\MySQL 是同步 I/O 封装:发起 SELECT 后协程挂起,等待 socket 就绪;若 MySQL 执行超时(比如没走索引的全表扫描),它就一直等在 recvfrom 上,无法让出 CPU。此时哪怕有 100 个待调度协程,也全被锁在同一个 Worker 里。
现象包括:swoole_server->stats() 显示 coroutine_num 暴涨但 request_count 几乎不动;strace -p [pid] -e trace=recvfrom 看到大量阻塞调用。
根本原因不是协程不协程化,而是 Swoole 没接管 MySQL 协议层超时,完全依赖 PHP 驱动的 read_timeout,而 Hyperf 默认配置里常漏掉这个。
-
DB::timeout(5)只对 Laravel 风格 Query Builder 生效,对Db::select('SELECT ...')或 Eloquent 原生查询无效 - 必须显式设置 MySQL 连接级超时:
'options' => [PDO::ATTR_TIMEOUT => 5](PDO)或'read_timeout' => 5.0(Swoole MySQL 驱动) - 配合
EXPLAIN FORMAT=TREE验证慢查询是否真由缺失索引导致,而非单纯数据量大
pool.wait_timeout 和 max_connections 怎么设才不翻车
pool.wait_timeout 是协程在连接池里排队等空闲连接的最长容忍时间,不是 SQL 执行超时。设太小(如 0.1)会导致瞬时抖动就满屏 WaitTimeoutException;设太大(如 15.0)则掩盖真实瓶颈,比如 max_connections 不足或连接泄漏。
推荐值为 2.0~5.0,兼顾缓冲与快速失败。
-
max_connections必须按实际负载反推:峰值 QPS × 平均 DB 耗时(秒) × 1.5~2.0,别拍脑袋填 100 - 记得把定时任务、WebSocket 长连接占用的连接算进去,它们常驻协程可能长期持有连接
- 用
hyperf:pool-status查used和wait:一旦wait > 0,说明协程已在排队,是扩容信号
max_idle_time 和 heartbeat 配错会直接失效
很多人开了 pool.heartbeat => true 却还报 MySQL server has gone away,是因为 heartbeat 在 Hyperf 里默认只在连接取出时单次 PING,不起后台探测作用。真正靠的是 IdleConnectionChecker 线程,它依赖 max_idle_time 和 heartbeat 协同工作。
-
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) -
min_connections别设 0,冷启延迟高;建议 ≥ 2,避免首请求建连阻塞 - 若驱动不支持 PING(如某些旧版 PostgreSQL),可改用
'checker' => 'SELECT 1',但该语句不能在事务中执行
with() 预加载救不了没索引的关联字段
with(['user', 'user.profile']) 看似解决 N+1,但如果 user_id 或 profile.user_id 没索引,生成的 IN 子查询或 JOIN 会触发全表扫描,比 N+1 更慢。
-
belongsTo关系:外键字段(如user_id)必须建 B+ 树索引,且 WHERE 条件不能包裹函数(如WHERE DATE(created_at) = '2026-05-25') -
hasMany分页场景:复合索引要覆盖ORDER BY + LIMIT,例如INDEX (status, created_at)比单列created_at高效得多 - 多层嵌套 with:确保每层 JOIN 字段都有索引,否则第二层 JOIN 就可能回退到全表扫描
- 验证方式:开
DB::enableQueryLog()拿 SQL,在 MySQL 中执行EXPLAIN FORMAT=TREE,重点看rows是否远超预期、是否有Using temporary
最易被忽略的点是:慢查询卡死协程、连接池参数失配、索引缺失三者叠加时,日志里往往只报 wait timeout 或 gone away,但根因藏在 SQL 执行计划里——不看 EXPLAIN,光调参只是给炸药桶换引信。



















