hyperf异步队列卡死时,根本原因是消费者进程未运行或task worker阻塞:需先用ps aux确认async-queue:consume进程存活,再检查task_wait_queue_len>0且tasking_num==0;接着验证/dev/shm空间、redis连接池耗尽、协程上下文污染及同步io(如日志、opcache、配置文件读取)等关键陷阱。

Hyperf异步队列卡死时,接口响应停滞、任务积压不消费、日志静默无报错,但swoole_server->stats()显示task_wait_queue_len持续上涨而tasking_num长期为0——这不是队列没投递成功,而是消费者进程根本没在干活。
确认消费者进程是否存活
执行 ps aux | grep 'php.*async-queue:consume',检查是否有正在运行的消费者进程;若无输出,说明消费者未启动或已崩溃退出。
手动启动消费者:php bin/hyperf.php async-queue:consume --daemon,观察是否立即报错(如Connection refused、Class not found);若启动后立刻退出,需查supervisor日志或直接加--verbose参数重试。
【关键前提】 异步队列消费者必须作为独立进程常驻运行,不能依赖HTTP Worker自动拉起——Hyperf默认不会在worker里启动consumer。
排查Redis连接池耗尽
方法一:直查连接数
执行 ss -s | grep ESTAB 或 swoole_get_local_socket_count(),若ESTABLISHED连接数逼近redis pool.max_connections值(如设为30却看到28+),基本可判定连接被占满。
方法二:复现并抓异常
在任务handle()开头插入 throw new RuntimeException('test');,然后触发一次任务;若日志中出现WaitTimeoutException而非该异常,说明连接池在获取Redis客户端时就已超时,根本没走到handle逻辑。
注意:pipeline/multi未正常exec会导致连接泄漏,且泄漏连接不会随协程结束自动释放——它会一直挂在连接池里,直到消费者进程重启。
定位Task Worker阻塞点
第一步:查当前task状态
调用 $server = \Hyperf\Contract\ServerInterface::class; $stats = $server->getServer()->stats(); 输出task_wait_queue_len和tasking_num。若前者>0且后者==0,说明任务进来了,但没人处理。
第二步:验证Task Worker是否真在工作
在任意一个可执行的Job::handle()里写入 file_put_contents('/tmp/task_test', date('Y-m-d H:i:s') . "\n", FILE_APPEND);,提交任务后检查文件是否有追加内容;若无,说明Task Worker进程未执行该回调。
第三步:检查task_worker_num配置与IPC通信
若task_worker_num > worker_num × 1.5,且/dev/shm空间不足(df -h /dev/shm),序列化任务参数会失败,导致execute()静默丢弃任务——此时连日志都不会产生。
第四步:确认task_enable_coroutine是否生效
在handle()中加入 Co::sleep(0.01);,再用strace -p $(pgrep -f 'task_worker') -e trace=epoll_wait观察是否进入等待;若持续busy-loop(不停打印epoll_wait返回0),说明协程调度器未启用或被阻塞。
检查MySQL事务污染Redis上下文
当DB操作抛出PDOException(如MySQL server has gone away)且未被捕获时,当前协程上下文可能已损坏;后续同一协程内调用$redis->get()会直接卡住,既不返回也不报错。
临时验证方式:在Job::handle()最开头强制开启新协程上下文——Co::create(function () { $redis = make(\Hyperf\Redis\Redis::class); var_dump($redis->ping()); }); 若ping能返回'+PONG',而原路径下$redis->ping()卡死,则证实上下文被污染。
【致命陷阱】 这类卡死不会触发任何PHP异常,也不会写入Hyperf日志,仅表现为任务无限挂起,且tasking_num恒为1(因为那个协程永远睡在了Redis I/O上)。
禁用所有隐性同步IO
打开config/autoload/logger.php,将Monolog\Handler\StreamHandler替换为Hyperf\Logger\Handler\StdoutHandler;否则每次写日志都会触发file_put_contents同步磁盘IO,阻塞整个Task Worker。
关闭opcache.file_cache:该配置会在每次请求时同步读取opcache文件缓存,即使开了task_enable_coroutine也无效——它不走Swoole Runtime接管路径。
检查config/autoload/*.php中是否存在file_get_contents、json_decode(file_get_contents())等同步调用;若有,改用Co\System::readFile或把配置提前加载到内存。











