DEBUG.WIKI
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS
IPPeak:从目标地区访问网页,住宅代理、SEO 监测、广告验证
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国

BullMQ CPU 密集任务 stalled:事件循环阻塞了锁续期

确认同步CPU阻塞后,将计算放入外部processor,保留锁恢复和幂等。异步等待偶发stalled仍需另查连接与进程,不用无限续锁掩盖故障。

处理函数执行较长的同步计算,BullMQ 随后产生 stalled 事件,任务被重新放回 waiting,甚至再次执行。若同步工作确实阻塞了 Worker 的 Node.js 事件循环,应检查锁续期是否因此无法及时执行。

本文只处理已经确认的 CPU 阻塞分支。大量等待 HTTP 或数据库的任务偶发 stalled,仍需检查 Redis 连接、进程退出、资源限制和队列状态,不能因为报错相同就认定 CPU 占满。

分开记录任务耗时与循环阻塞

任务运行很久不一定阻塞事件循环。真正异步等待的网络请求可以让其他工作继续执行,连续同步计算、同步文件操作或某些处理库却可能阻塞。

对照 stalled 时间、事件循环延迟、CPU 限额、Redis 连接错误及进程启停记录。容器 CPU 被限制时,宿主机总体负载不高,也不能保证这个 Worker 能及时获得运行机会。

处理函数包含 await 不足以证明锁续期有机会运行。等待一个已经完成的 Promise 与真正让出循环、允许定时器阶段执行不是同一个保证。不要按代码里 await 的数量判断可用性。

将计算移到外部 processor

BullMQ 支持把 processor 放在独立文件中执行,让计算与主进程的队列管理分开。下面是 CommonJS 部署结构示例,路径应指向已经构建并部署的 JavaScript 文件;calculate 为项目已有的计算函数:

// processor.cjs
const { calculate } = require('./calculate.cjs');

module.exports = async job => calculate(job.data);

创建 Worker 时传文件路径,保留原队列前缀与已正确配置的连接:

// worker.cjs
const path = require('node:path');
const { Worker } = require('bullmq');

const worker = new Worker(
  'tasks',
  path.join(__dirname, 'processor.cjs'),
  { connection: workerConnection, prefix: queuePrefix },
);

workerConnection 与 queuePrefix 是原应用的配置,不能以示例默认值覆盖。TypeScript 项目应指向实际构建产物,并核对模块加载方式和相对依赖,不直接假设生产进程能执行源码 .ts。

默认外部 processor 使用独立进程;BullMQ 3.13.0 起也支持外部 processor 配合 useWorkerThreads。选择线程或进程时仍需考虑运行时、依赖和资源约束。隔离处理函数不能替代合理并发,开太多计算实例仍可能挤压管理进程。

保留锁恢复与业务幂等

stalled 描述事件,不是一个长期的独立任务状态。锁丢失后,任务可能回到等待或因超过允许次数而失败。原业务可能已经发生一部分副作用,重新处理时必须依据业务 ID 或幂等记录判断,不能自动重复扣款或重复提交。

调大 lockDuration 可以改变恢复边界,但不能让被阻塞的循环开始执行。长时间无法续期时,延长它还可能推迟异常任务被识别。也不要随手另建定时器无限 extendLock,让心跳与实际工作脱离;需要特定锁策略时应单独设计并核对失败路径。

修改前保留依赖和 Worker 设置,安排活动任务的排空与优雅退出。验收不仅看 stalled 是否减少,还要看管理进程在计算期间能够正常工作、连接错误仍能记录,以及异常重投递不会重复产生业务结果。