Node.js 进程仍在、CPU 很高、健康检查却连续超时,并不一定是网络或下游服务故障。若 CPU 密集计算、同步 API 或超大输入处理占住主事件循环,同一进程甚至无法及时接受一个简单的健康请求。此时反复重启只能暂时清空正在执行的工作,不能消除下一次阻塞。
适用范围与核心结论
本文适用于 Node.js HTTP 服务出现“进程存活但请求集体卡住”、事件循环延迟升高或探针超时。验证环境使用 Node.js 22.20.0 Alpine 与 Docker 24.0.7。
Node.js 的 JavaScript 回调主要运行在事件循环线程上。一个回调如果连续执行三秒 CPU 工作,这三秒内同进程的健康端点也不能获得执行机会。需要先证明阻塞发生在事件循环,再决定拆分计算、限制输入、使用 Worker Threads 或迁移到独立任务服务。
第一步:保存外部症状和进程证据
从负载均衡器或同网段客户端记录健康请求的状态、连接阶段和总耗时:
curl -sv --connect-timeout 2 --max-time 5 https://api.example.com/health
ps -o pid,ppid,stat,%cpu,%mem,etime,cmd -p "$PID"
top -H -p "$PID"
如果 TCP 能建立但响应超时,同时单个 Node 进程持续占用一个 CPU 核,应把“事件循环被长回调占用”列为假设。不要仅凭 CPU 高就下结论;正常的 Worker Thread 或原生线程池任务也会消耗 CPU。
第二步:测量事件循环延迟
可以使用 node:perf_hooks 的 monitorEventLoopDelay() 记录分位数,并把指标接入现有监控。采样本身应保持轻量:
import { monitorEventLoopDelay } from 'node:perf_hooks';
const delay = monitorEventLoopDelay({ resolution: 20 });
delay.enable();
setInterval(() => {
console.log({ p99Ms: delay.percentile(99) / 1e6, maxMs: delay.max / 1e6 });
delay.reset();
}, 10000).unref();
将 p99/max 延迟与请求超时、CPU、GC 暂停和具体路由关联。如果事件循环延迟高而 CPU 不高,还应检查同步文件系统、同步加密、正则灾难性回溯或原生扩展。
第三步:定位占用主线程的代码
在受控环境或短时间生产窗口采集 CPU profile,优先寻找单次耗时很长的 JavaScript 栈。不要在高峰期无限期开启详细 profiler。
node --cpu-prof server.mjs
# 或使用已经批准的诊断工具进行短时采样
重点审查同步 API、对大数组的无界循环、大 JSON 序列化、复杂正则、压缩/加密和用户可控输入规模。必须同时记录触发请求与输入上限,避免只优化实验样本。
修复选择:分片、限流或 Worker Thread
短任务可以拆成有界批次,让事件循环在批次间处理 I/O;CPU 密集且适合并行的任务可以放入 Worker Thread 池。Worker 不应按每个请求无限创建,应有固定并发、队列长度、超时和取消策略。普通数据库或 HTTP I/O 通常不需要 Worker Thread,异步 API 更合适。
import { Worker } from 'node:worker_threads';
// 实际项目应复用有界 worker 池,而不是每个请求新建无限 worker。
const worker = new Worker(new URL('./cpu-task.mjs', import.meta.url), {
workerData: boundedInput,
});
同时为请求体、数组长度、正则输入和计算时间设置上限。若任务可能超过 HTTP 请求生命周期,应转成异步作业并返回任务 ID。
复核与回滚
修复后用相同输入和并发重复测试,确认业务结果一致、健康端点在任务运行期间仍能在 SLO 内响应,并比较 CPU、内存、事件循环延迟与队列深度。Worker 方案需要额外检查线程泄漏、序列化成本和进程内存。
回滚应恢复上一版本代码和原并发配置。若新 Worker 池出现错误,先停止接收新任务并排空或安全取消队列,再回滚;不要直接杀死仍在执行不可重入写操作的线程。
V2CE 复现实验记录
隔离 Node 服务提供 /block、/worker 和轻量健康端点。/block 在主事件循环连续执行三秒 CPU 计算;并发的一秒健康请求没有得到响应并由边界超时结束,计算完成后健康请求恢复。/worker 把同样的三秒计算放入 Worker Thread,计算期间并发健康请求仍返回 healthy。
实验没有声称 Worker 一定提升总吞吐量;它只证明主线程 CPU 工作会阻塞同进程请求,而隔离 CPU 工作可以恢复事件循环响应性。证据保存在 labs/node-event-loop-blocked/evidence。