使用 Codex 开发 Node.js 服务时,很多功能看起来只是“多做一点计算”。

例如:

  • 大文件解析;

  • 图片压缩;

  • PDF生成;

  • 大量 JSON 转换;

  • 数据加密;

  • 哈希计算;

  • Excel处理;

  • 复杂规则计算。

本地测试时通常没有问题,但真正上线以后,只要某个计算任务稍微重一点,就可能出现:

  • 一个接口执行时,其他接口也一起变慢;

  • CPU突然接近100%;

  • 健康检查开始超时;

  • 数据库明明正常,API却迟迟没有响应;

  • 单个文件处理拖慢整个Node.js服务;

  • Codex不断增加 async/await,性能却没有改善。

这类问题通常不是异步代码写错,而是 CPU密集任务阻塞了Node.js事件循环

一、async不等于不会阻塞

很多人看到:

async function generateReport() {
  return heavyCalculate();
}

会认为因为函数是 async,所以不会阻塞主线程。

实际上,如果:

heavyCalculate()

内部进行大量同步计算,那么它仍然会持续占用 JavaScript 主线程。

例如:

function heavyCalculate() {
  let result = 0;

  for (let i = 0; i < 2_000_000_000; i++) {
    result += i;
  }

  return result;
}

在这段循环结束之前,Node.js 很难及时处理其他 JavaScript 任务。

于是:

请求A
→ 开始大量计算
→ Event Loop被占用

请求B
→ 等待

请求C
→ 等待

健康检查
→ 也等待

最终表现就是整个服务像“卡死”了一样。

二、Node.js适合什么任务?

Node.js 非常适合大量 I/O 操作,例如:

HTTP请求
数据库访问
Redis
文件网络传输
消息队列

因为这些任务大部分时间是在等待外部系统。

例如:

const user = await db.user.findUnique(...);

等待数据库期间,Node.js 可以继续处理其他请求。

但 CPU 密集型任务不同。

例如:

图片解码
压缩
视频处理
复杂加密
大量循环
大型JSON序列化

这些任务需要真正使用 CPU。

如果直接运行在主线程上,就会占用 Event Loop。

三、怎么判断是不是Event Loop被阻塞?

最明显的现象是:

数据库延迟正常
Redis正常
网络正常
CPU很高
所有接口一起变慢

可以记录 Event Loop Lag。

例如定期测量:

const start = Date.now();

setTimeout(() => {
  const lag = Date.now() - start - 100;

  console.log({
    event: "event_loop_lag",
    lag
  });
}, 100);

如果本应100ms执行的Timer,实际:

500ms
1000ms
3000ms

以后才执行,说明主线程可能正在被长任务占用。

生产环境可以通过专门的监控工具持续观察 Event Loop Delay。

四、不要用Promise包住同步任务

错误方式:

await Promise.resolve(
  heavyCalculate()
);

或者:

new Promise(resolve => {
  resolve(heavyCalculate());
});

这并不会自动把计算放到另一个 CPU 线程。

heavyCalculate() 仍然运行在当前 JavaScript 主线程。

所以:

Promise

解决的是异步流程组织问题。

它不是:

CPU线程隔离工具。

五、setTimeout也不能真正解决CPU阻塞

有时会看到:

setTimeout(() => {
  heavyCalculate();
}, 0);

这样只是把任务推迟到后面的 Event Loop 阶段执行。

真正开始计算以后,主线程仍然会被占住。

所以它只能:

推迟阻塞

而不能:

消除阻塞。

六、Worker Threads适合CPU密集任务

Node.js 提供 Worker Threads,可以把 JavaScript 计算放到独立线程。

例如主线程:

import {
  Worker
} from "node:worker_threads";

function runWorker(data: unknown) {
  return new Promise((resolve, reject) => {
    const worker = new Worker(
      "./worker.js",
      {
        workerData: data
      }
    );

    worker.on("message", resolve);
    worker.on("error", reject);

    worker.on("exit", code => {
      if (code !== 0) {
        reject(
          new Error(
            `Worker stopped with ${code}`
          )
        );
      }
    });
  });
}

Worker:

import {
  parentPort,
  workerData
} from "node:worker_threads";

const result =
  heavyCalculate(workerData);

parentPort?.postMessage(result);

这时:

HTTP主线程
→ 继续处理请求

Worker Thread
→ 负责复杂计算

计算任务就不会长时间占用主要 Event Loop。

七、不要每次请求都无限创建Worker

如果每个请求都:

new Worker(...)

高并发时可能出现:

请求1000个
→ 创建1000个线程

这同样会把机器拖垮。

因为线程本身也需要:

  • 内存;

  • CPU;

  • 调度成本。

更适合建立:

Worker Pool

例如机器有:

8核CPU

可以根据实际任务设置:

4个
6个
8个

固定 Worker。

新任务进入:

任务队列
↓
空闲Worker领取
↓
处理完成
↓
继续下一个

而不是无限创建线程。

八、CPU并发不能照搬HTTP并发

假设服务器可以同时保持:

1000个HTTP连接

不代表可以同时执行:

1000个CPU计算任务。

CPU核心可能只有:

4
8
16

如果同时运行大量计算:

线程互相抢CPU
↓
上下文切换增加
↓
每个任务都变慢

所以 CPU 任务通常应该限制并发。

例如:

HTTP请求:1000并发

CPU Worker:
最多6个任务同时执行

其他任务进入等待队列。

九、长任务最好不要绑在HTTP请求上

例如用户上传一个大文件,然后接口:

上传文件
↓
解析
↓
压缩
↓
生成报告
↓
等待2分钟
↓
返回

风险很大。

用户可能遇到:

  • 网关超时;

  • 浏览器断开;

  • 重复提交;

  • 服务重启任务丢失。

更稳定的方式:

POST /jobs
↓
创建任务
↓
返回jobId

例如:

{
  "jobId": "job_10086",
  "status": "pending"
}

后台 Worker 负责真正计算。

前端再查询:

GET /jobs/job_10086

获取:

pending
processing
completed
failed

这样长计算与HTTP生命周期解耦。

十、Worker任务也必须设置超时

不能假设所有计算最终都会完成。

例如:

异常输入
算法Bug
超大文件
死循环

可能让一个 Worker 长时间无法返回。

可以为任务设置:

最大运行时间

例如:

30秒
60秒
5分钟

达到上限以后:

终止Worker
↓
任务标记失败
↓
记录日志

否则少量异常任务就可能逐渐占满整个 Worker Pool。

十一、任务队列也要有限制

如果 Worker 只能同时执行:

8个任务

但每秒进来:

100个任务

队列会不断增长:

100
1000
10000
100000

最终还是可能耗尽内存。

因此需要设置:

最大队列长度

超过以后可以:

  • 拒绝请求;

  • 返回繁忙;

  • 延迟任务;

  • 写入外部消息队列。

不能让应用内存无限承担排队压力。

十二、大JSON也可能阻塞Event Loop

不只有复杂算法会阻塞。

例如:

JSON.stringify(hugeObject);

如果对象非常大,同样属于同步 CPU 工作。

例如响应:

几十MB JSON

序列化本身就可能让主线程停顿。

因此大型数据接口应该评估:

  • 是否分页;

  • 是否流式输出;

  • 是否真的需要全部字段;

  • 是否能异步生成文件;

  • 是否应该改为下载任务。

不要把所有性能问题都归因于数据库。

十三、正则表达式也可能导致CPU暴涨

某些复杂正则可能产生严重回溯。

例如处理用户输入时,如果正则设计不当,少量特殊字符串就可能让 CPU 长时间满载。

表现为:

单请求进来
↓
CPU 100%
↓
整个服务响应下降

因此外部输入上的复杂 Regex 也属于需要审查的 CPU 风险点。

Codex 生成复杂正则后,应该测试异常长度输入。

十四、加密和密码Hash本身就应该慢

例如:

bcrypt
scrypt
Argon2

密码哈希故意设计成较高计算成本。

如果登录请求量突然增大:

大量Hash同时执行

CPU压力会明显提升。

不能为了让接口快就随意降低安全参数。

更合理的是:

限制登录并发
+
合理线程池配置
+
限流

同时保留足够安全的Hash成本。

十五、图片处理特别适合独立Worker

例如:

原图上传
↓
生成缩略图
↓
压缩
↓
转WebP
↓
提取尺寸

如果全部在Web进程执行,高并发上传时很容易拖慢普通API。

更合理:

Web API
→ 接收文件
→ 创建图片处理任务

Image Worker
→ 压缩
→ 转码
→ 保存

这样图片处理CPU压力不会直接影响登录、订单等普通接口。

十六、多实例环境也要控制总Worker数

假设:

单实例:
8个Worker

生产部署:

10个实例

最终就有:

80个CPU Worker

但机器集群实际CPU资源未必能支持这么多任务同时运行。

因此配置 Worker 时不能只看单实例。

还要看:

单实例Worker数
×
实例数量
×
Pod CPU Limit

与数据库连接池问题类似,局部合理不代表全局合理。

十七、容器CPU限制会影响Worker数量

Kubernetes 中 Pod 可能设置:

CPU Limit = 2

即使宿主机有32核,当前容器实际仍然只有有限CPU资源。

如果 Codex 根据:

os.cpus().length

直接创建大量 Worker,可能并不符合容器真实配额。

因此容器部署时需要根据实际 CPU Limit 配置 Worker 并发,而不是盲目使用宿主机核心数量。

十八、什么时候适合独立Worker服务?

如果计算任务只是偶尔执行:

少量PDF
小型图片
偶尔Excel

Worker Threads 可能已经够用。

如果任务已经包含:

大量图片
视频处理
复杂报表
批量数据分析
AI后处理

更适合拆成独立:

Worker Service

架构:

API
↓
Queue
↓
CPU Worker Service

这样可以单独扩容:

API实例

和:

计算Worker实例。

两种工作负载互不影响。

十九、让Codex先分析CPU热点

遇到Node.js服务卡顿时,可以先这样要求:

请先不要修改代码。

分析当前服务中的CPU密集任务:

1. 哪些函数存在大量同步循环;
2. 是否存在大型JSON stringify / parse;
3. 是否存在图片、压缩或加密任务;
4. 哪些任务运行在HTTP主线程;
5. 单次任务平均耗时;
6. 是否可能同时运行多个任务;
7. 当前Event Loop Lag是多少;
8. 哪些任务适合迁移到Worker Threads。

先确认:

到底是谁占满了CPU

再决定是否拆线程。

二十、测试时要观察Event Loop Lag

压测不能只看:

QPS
响应时间

还应该观察:

CPU
Event Loop Lag
Worker队列长度
Worker任务耗时
内存

例如:

并发20:
Event Loop Lag = 10ms

并发100:
Event Loop Lag = 80ms

启动图片处理后:
Event Loop Lag = 2200ms

这就能明显判断问题来自 CPU 阻塞,而不是数据库。

二十一、把CPU任务规则写进AGENTS.md

# Node.js CPU任务规则

- CPU密集任务禁止默认运行在HTTP主线程
- async / Promise不能视为CPU线程隔离
- 大型同步循环必须评估Event Loop阻塞
- Worker Threads必须限制最大并发
- 禁止每个请求无限创建Worker
- 长计算任务优先评估Queue + Worker
- Worker任务必须设置超时
- Worker任务队列必须有最大长度
- 大型JSON序列化需要评估事件循环影响
- 修改CPU任务后必须观察Event Loop Lag

这样 Codex 后续处理性能问题时,就不会只在原函数前增加一个 async

二十二、Plus还是Pro?

如果主要使用 Codex 处理:

  • 单个Node.js接口;

  • 普通异步逻辑;

  • 少量文件处理;

  • 小型后端项目;

Plus通常已经能够覆盖大部分任务。

如果长期需要:

  • 大型Node.js服务;

  • 多Worker架构;

  • CPU性能分析;

  • 复杂任务队列;

  • 大量日志与压测数据;

  • 多服务性能重构;

可以根据实际开发强度评估 Pro。

更高使用空间更适合连续进行“定位热点—修改代码—压测—再次分析”这一类长任务。

但无论使用哪种方案,都不能绕过 Node.js 最重要的一个问题:

当前代码是在等待I/O,还是正在真正占用CPU?

总结

Codex 写 Node.js 计算任务后,为什么一个接口执行就能让整个服务一起卡住?

根本原因往往是 CPU 密集型 JavaScript 占用了主 Event Loop。

通过:

Worker Threads
Worker Pool
任务队列
并发限制
Event Loop监控

可以把计算工作从HTTP主线程中隔离出去。

真正稳定的Node.js服务,不应该要求主线程同时负责:

接收请求
+
处理网络
+
执行大量CPU计算

主线程最重要的任务,是保持事件循环流畅。

计算可以慢,但不能让一个慢任务拖住整个服务。

CSDN文章描述

本文介绍 Codex 编写 Node.js CPU 密集任务时常见的 Event Loop 阻塞问题,并通过 Worker Threads、Worker Pool、任务队列、并发控制和 Event Loop Lag 监控提升服务稳定性。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐