workerman协程卡死本质是协程被同步io、未关闭channel或死锁阻塞,导致事件循环停滞;需用kill -usr1和strace确认,再替换同步调用、闭环channel、修正等待逻辑,并设超时熔断与强制回收机制。

Workerman协程模式下消息队列消费卡死,本质是协程被阻塞在同步IO、未关闭channel、或死锁等待上,导致整个worker事件循环停滞,新消息无法进入、旧消息无法ACK、连接心跳中断——这不是队列积压,而是协程调度器彻底失能。
确认是否真为协程卡死而非业务慢
执行kill -USR1 {pid}触发Workerman状态快照,检查输出中是否有大量Coroutine::create但无对应Coroutine::id()活跃记录;若发现协程ID持续增长却无退出迹象,且ps aux | grep workerman显示CPU接近0%但内存缓慢上涨,基本可判定为协程卡死。
这一步不能跳过:用strace -p {pid} -e trace=epoll_wait,recvfrom,sendto观察系统调用是否长期停在epoll_wait,若停住超3秒,说明事件循环已被阻塞,不是网络延迟问题。
排查并修复协程阻塞点
方法一:禁用所有同步IO调用
把file_get_contents、curl_exec、mysqli_query、Redis::get等全部替换为协程版:Co\Http\Client、Co\Redis、Co\Mysql。PHP原生同步函数会令当前协程挂起,但不释放控制权给调度器,整个worker就僵住了。
方法二:检查channel使用是否闭环
若代码中出现$ch = new chan(1)后只$ch->push()不$ch->pop(),或go(function() use($ch) { $ch->pop(); })但主协程未go()启动该接收协程,【协程会在push或pop处永久阻塞】。必须确保每个chan都有明确的发送方和接收方,且至少一方带timeout兜底。
方法三:验证协程间等待逻辑
搜索代码中所有Co::wait()、Channel::pop()、Scheduler::schedule()调用,确认其等待目标必然可达。例如:Co::wait([function() { sleep(5); }])这种写法会让协程卡满5秒——协程里不能用sleep(),应改用Co::sleep(5)。
强制清理卡死协程并重建消费链
第一步:在onWorkerStart中设置协程超时熔断
添加Co::set(['hook_flags' => SWOOLE_HOOK_ALL]);启用全钩子,再调用Co::set(['socket_connect_timeout' => 3, 'socket_read_timeout' => 5, 'socket_write_timeout' => 5]);避免底层socket操作无限等待。
第二步:为每条消费逻辑包裹协程级超时
用Co::create(function () { /* 消费逻辑 */ });启动协程后,立即调用Co::set(['max_coroutine' => 1000]);限制总数,并在消费回调开头插入if (Co::getCid() === 0) { throw new RuntimeException('main coroutine not allowed'); }防止误在主线程执行。
第三步:监听心跳失败触发协程强制回收
在onWorkerStart中注册心跳检测:Timer::tick(10000, function () { if (Co::stats()['coroutine_num'] > 500) { Co::stats()['coroutine_num'] > 500 && Co::getStats()['coroutine_num'] > 500 && gc_collect_cycles(); } });,当协程数异常堆积时主动触发GC。
第四步:消费完成前必须显式关闭channel与连接
所有Co\Redis、Co\Mysql实例在任务结束前调用$redis->close()、$mysql->close();所有chan对象在最后一次pop后执行$ch->close(),否则协程调度器无法识别资源释放信号。











