协程死锁时swoole_get_local_cid()返回-1,说明调度器离线、协程上下文丢失;根本原因是同步i/o阻塞(如file_get_contents、curl_exec)或误用sleep等非协程安全操作,需通过strace、协程快照(kill -usr1或coroutine::listcoroutines())定位waiting状态协程的阻塞调用点。

协程死锁时进程完全无响应,swoole_get_local_cid() 返回 -1 怎么办
Hyperf 在高并发场景下出现“进程卡住但 CPU 占用极低”时,大概率是协程死锁而非业务阻塞。此时 swoole_get_local_cid() 返回 -1,说明当前线程已脱离协程上下文——不是协程没启动,而是它被永久挂起,调度器再无法唤醒。
根本原因通常是:在协程中调用了非协程安全的同步 I/O(如 file_get_contents、curl_exec)、或误用了 Co::sleep() 以外的阻塞函数、或在协程内直接调用了未适配协程的 SDK(比如某些老版本 Redis 扩展的 connect())。
- 检查所有
vendor/下的 SDK 是否声明支持 Swoole 协程,尤其关注__construct和connect方法是否含stream_socket_client类调用 - 禁用所有
ini_set('swoole.enable_coroutine', '0')相关临时关闭逻辑,Hyperf 依赖全程协程环境 - 用
strace -p <pid> -e trace=epoll_wait,read,write</pid>观察是否长期停在epoll_wait且无后续系统调用
如何开启并读取 Swoole 协程快照(Coroutine::listCoroutines() + Coroutine::getBackTrace())
Hyperf 默认不暴露协程快照能力,需手动触发。关键不是“看有多少协程”,而是找出哪些协程状态为 WAITING 且堆栈停留在 I/O 调用点。
在终端执行 kill -USR1 <worker_pid></worker_pid> 可触发 Swoole 写入协程快照到 /tmp/swoole-coroutine-*.log;但更可控的方式是在代码中插入诊断逻辑:
use Swoole\Coroutine;
// 放在异常捕获中间件或定时任务里
$coros = Coroutine::listCoroutines();
foreach ($coros as $cid) {
$status = Coroutine::getStatus($cid);
if ($status === SWOOLE_CORO_WAITING) {
$trace = Coroutine::getBackTrace($cid, 0, 20);
// 记录到日志,重点看最后一层是否为 stream_select / socket_read / curl_exec 等
}
}
-
Coroutine::listCoroutines()返回的是整数 CID 数组,不是对象,别试图调->getBackTrace() -
Coroutine::getBackTrace()第二个参数是起始层级,设为0才能拿到最深调用帧;设太大可能跳过关键 I/O 行 - 快照日志默认权限是
600,若用非 root 用户运行 worker,确保/tmp可写且日志路径没被 SELinux 拦截
Hyperf 中常见死锁模式:Redis 连接池未设置 timeout 导致 connect() 卡住
Hyperf 的 RedisFactory 默认使用 Swoole\Coroutine\Redis,但若配置中漏掉 timeout 或设为 0,底层仍会回退到阻塞式连接,在 DNS 解析失败或目标端口不可达时无限等待。
验证方式:在 config/autoload/cache.php 或 redis.php 中检查 'timeout' => 3.0 是否显式存在;若用 Hyperf\Redis\RedisFactory::createPool() 手动建池,必须传入 ['timeout' => 3.0]。
- 不要依赖
default_timeout全局配置,Swoole Redis 客户端只认连接池配置里的timeout - 若使用
phpredis扩展(非协程版),即使开了enable_coroutine也无效,必须切换为Swoole\Coroutine\Redis -
timeout设为0.1不推荐——网络抖动易误判,2.0~5.0是较稳妥区间
为什么 go(function () { sleep(10); }); 看似正常却埋下隐患
sleep() 是 PHP 同步函数,它会让当前协程所在的 OS 线程休眠 10 秒,期间整个线程无法调度其他协程——这不是“协程挂起”,而是“线程冻结”。Hyperf Worker 进程通常只有 4~8 个线程,一个 sleep(10) 就可能吃掉全部并发能力。
正确做法永远是 Co::sleep(10),它只是让当前协程让出控制权,线程可继续跑别的协程。
- 全局搜索项目中所有
sleep(、usleep(、time_nanosleep(,替换为Co::sleep(或Co::usleep( - 第三方包若含硬编码
sleep(如某些 SDK 的重试逻辑),需通过class_alias或 AOP 替换其调用,不能仅靠ini_set -
Co::sleep()的精度受swoole.timer.tick影响,若设为50(毫秒),Co::sleep(1)实际可能延迟 50ms,别用于亚毫秒级定时
协程死锁的麻烦之处在于它不报错、不超时、不释放资源,只安静地把进程变成“活化石”。快照本身不解决问题,但它把隐性阻塞变成了可定位的调用栈——剩下的,就是顺着那一行 stream_socket_client 或 curl_exec 往上翻,找到那个没加 timeout、没切协程客户端、也没做 fallback 的调用点。











