hyperf中慢查询会卡死协程调度而非仅拖慢接口,因swoole协程mysql客户端是同步i/o封装,sql执行超时导致协程无限等待,使整个worker进程停摆、qps归零。

Hyperf里慢查询不是“拖慢接口”,而是直接卡死协程调度——一条没走索引的SELECT,会让整个Worker进程在等待MySQL返回时停摆,其他几十个协程全被挂起,QPS瞬间归零。
为什么慢查询会阻塞整个协程池
Hyperf的协程MySQL客户端(如Swoole\Coroutine\MySQL)本质仍是同步I/O封装:发起查询后,协程挂起、等待socket就绪;但若SQL执行超时(比如全表扫描),MySQL不返回结果,协程就一直等在那儿,无法让出CPU。这不是“慢”,是“卡死”。
- 现象:压测时
swoole_server->stats()显示coroutine_num飙升,但request_count几乎不动;strace -p [pid] -e trace=recvfrom能看到大量阻塞的recvfrom - 根本原因:Swoole未接管MySQL协议层的超时控制,依赖PHP MySQL驱动自身的
connect_timeout/read_timeout,而Hyperf默认配置常忽略这些 - 误区:
DB::timeout(5)只作用于Laravel风格的Query Builder,对原生Db::select()或Eloquent原始查询无效
必须加的三类索引,否则with()也救不了
Hyperf中with()预加载看似能解决N+1,但若关联字段没索引,生成的IN子查询或JOIN反而更慢——MySQL会放弃使用索引,回退到全表扫描。
-
belongsTo关系:外键字段(如user_id)必须建B+树索引,且WHERE user_id = ?不能被函数包裹(如WHERE DATE(created_at) = '2026-05-25') -
hasMany分页场景:复合索引要覆盖order by + limit,例如INDEX (status, created_at)比单列created_at索引高效得多 - 多层嵌套
with(['user', 'user.profile']):确保profile.user_id有索引,否则第二层JOIN会触发全表扫描 - 验证方式:用
DB::enableQueryLog()拿到SQL后,在MySQL中执行EXPLAIN FORMAT=TREE,重点看rows是否远超预期、是否有Using temporary
开启Swoole aio前,先禁掉所有隐性IO
很多人开了aio => true却没效果,是因为日志、配置加载、opcache等环节仍在偷偷做同步磁盘IO——它们不走Swoole Runtime接管路径,照样阻塞协程。
- 日志:把
Monolog\Handler\StreamHandler换成Hyperf\Logger\Handler\StdoutHandler(输出到stdout)或自研协程安全FileHandler(基于Swoole\Coroutine\System::writeFile) - opcache:关闭
opcache.file_cache(它会在每次请求时同步读取缓存文件),生产环境仅启用opcache.enable和opcache.memory_consumption - 配置加载:避免在
config/autoload/*.php中调用file_get_contents读取外部YAML/JSON;改用Hyperf\Config\Loader内置的缓存机制 - 确认aio生效:
php --ri swoole输出中必须含aio => enabled,且config/autoload/server.php中'aio' => true已显式设置
最易被忽略的致命点:事务内慢查询会锁住整个协程生命周期
在DB::transaction()块里执行一条没索引的UPDATE,不仅慢,还会让该协程持有的数据库连接、锁资源、内存全部卡死——直到事务超时或MySQL kill连接。此时协程池里其他空闲协程无法复用这个连接,只能新建,迅速耗尽max_connections。
真正有效的解法不是加索引,而是:把长事务拆成短事务 + 显式设置innodb_lock_wait_timeout(建议设为5秒),并确保业务代码捕获DeadlockLoserException和LockWaitTimeoutException后重试,而不是静默失败。











