协程消费瓶颈需通过 async-queue:info 的 pending 持续上涨、failed 新增、processing 长期接近 0 来确认,而非仅看 Redis 队列长度;concurrent.limit 应按 IO 或 CPU 密集型任务特性合理设置,并避免上下文泄漏与连接池复用。

任务堆积时怎么快速判断是不是协程消费瓶颈
先别急着调 concurrent.limit,得确认问题真出在消费侧。执行 php bin/hyperf.php async-queue:info,重点看三组数字:pending 持续上涨、failed 不断新增、processing 长时间卡在 0 或极低值(比如始终是 1~2)。如果 processing 几乎为 0,但 pending 却在涨,说明消费者根本没动起来——大概率是协程池卡死、Redis 连接池耗尽或 Job 执行中阻塞未释放。
常见假象:Redis CLI 查 LLEN queue:default 数值很大,就以为是队列积压。错。这个长度包含已出队但尚未 ack 的任务,不可信。必须以 async-queue:info 输出为准。
concurrent.limit 设多少才不卡又不浪费
concurrent.limit 不是并发数上限,而是「同一时刻最多有几个任务在 handle() 方法里跑」。设高了不等于吞吐高,反而会挤占内存、拖慢 IPC、触发协程饥饿。
- IO 密集型任务(如调第三方 API、发邮件):单任务平均耗时 800ms,目标 QPS 50 → 理论 limit = 50 × 0.8 ≈ 40,但实际建议从 10 起步,观察
co::stats()中coroutine_num和内存增长趋势 - CPU 密集型任务(如报表聚合计算):limit 应 ≤ CPU 核数 × 1.2,4 核机器设成 5 是安全线,设成 20 必然卡死
- 绝对不能复用 HTTP 的 Redis 连接池 —— 必须在
config/autoload/async_queue.php中指定独立的'pool' => 'queue',否则业务请求一高峰,队列连接直接被抢光
Job 执行卡住不退出?十有八九是上下文泄漏
协程本身启动销毁无开销,但 Job 实例若持有 DB 连接、日志上下文或闭包变量,且没主动清理,就会一直驻留内存,最终 OOM。典型现象:pending = 0,但 RSS 内存持续单向上涨,co::stats() 显示协程数稳定,memory_get_usage(true) 每分钟涨几 MB。
- 所有
Context::set($key, $value)必须配对Context::del($key),尤其在中间件或装饰器里塞了PDO或大数组 - 避免在
handle()里 new 大对象或长期 hold 文件句柄;用完立刻 unset - 启用
hyperf/memory-leak-detector组件,它能扫描出 5 分钟以上未被 GC 的上下文 key - 临时验证法:把 Job 类里的逻辑全注释掉,只留
sleep(1),再压测 —— 如果内存不涨,说明泄漏就在你写的业务代码里
拆分大任务时为什么必须原子化 + 协程化消费
一个订单生成后要发短信、推站内信、更新用户积分、同步 ERP,全塞进一个 OrderCompleteJob 里,哪怕加了 go,也解决不了根本问题:单个 Job 生命周期太长,失败就得全重试,协程池槽位被长期占用。
正确做法是拆成四个独立 Job:SmsNotifyJob、PushNotifyJob、UpdatePointJob、SyncErpJob,每个只做一件事,并在消费端开启协程化调度:
public function handle()
{
go(function () {
// 发短信
$this->sendSms();
});
go(function () {
// 推消息
$this->pushMessage();
});
// 其他同理...
}
注意:go 不等于新建协程资源,但它让 IO 并行发起;真正要防的是这些子协程里重复获取 DB 连接或写 Context —— 每个子协程仍需独立清理自己的上下文。
拆分后还要配套改配置:在 async_queue.php 中为不同 Job 类型设不同通道和限流,比如短信通道 concurrent.limit = 30,ERP 同步通道 concurrent.limit = 3,避免慢任务拖垮快任务。


















