You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NodeJS千万级Promise触发内存溢出及I/O性能问题咨询

NodeJS千万级异步任务内存溢出问题分析

问题背景

我正在分析NodeJS处理异步函数的实际效能表现,编写了如下NodeJS脚本:一次性初始化1000万个会休眠2秒的Promise,用于模拟高并发后端API调用场景。脚本运行约30秒后,内存占用最高达到4096MB,最终抛出JavaScript heap out of memory错误。

测试代码如下:

const sleep = async (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const fakeAPICall = async (i) => {
  await sleep(2000);
  return i;
};

const NUM_OF_EXECUTIONS = 1e7;
console.time(`${NUM_OF_EXECUTIONS} executions:`);

[...Array(NUM_OF_EXECUTIONS).keys()].forEach((i) => {
  fakeAPICall(i).then((r) => {
    if (r === NUM_OF_EXECUTIONS - 1) {
      console.timeEnd(`${NUM_OF_EXECUTIONS} executions:`);
    }
  });
});

运行时抛出的错误信息:

<--- Last few GCs --->

[41215:0x10281b000]    36071 ms: Mark-sweep (reduce) 4095.5 (4100.9) -> 4095.3 (4105.7) MB, 5864.0 / 0.0 ms  (+ 1.3 ms in 2767 steps since start of marking, biggest step 0.0 ms, walltime since start of marking 7190 ms) (average mu = 0.296, current mu = 0.[41215:0x10281b000]    44534 ms: Mark-sweep (reduce) 4096.3 (4104.7) -> 4096.3 (4105.7) MB, 8461.4 / 0.0 ms  (average mu = 0.140, current mu = 0.000) allocation failure scavenge might not succeed


<--- JS stacktrace --->

FATAL ERROR: MarkCompactCollector: young object promotion failed Allocation failed - JavaScript heap out of memory
 1: 0x100098870 node::Abort() [/usr/local/opt/node@14/bin/node]
 2: 0x1000989eb node::OnFatalError(char const*, char const*) [/usr/local/opt/node@14/bin/node]
 3: 0x1001a6d55 v8::Utils::ReportOOMFailure(v8::internal::Isolate*, char const*, bool) [/usr/local/opt/node@14/bin/node]
 4: 0x1001a6cff v8::internal::V8::FatalProcessOutOfMemory(v8::internal::Isolate*, char const*, bool) [/usr/local/opt/node@14/bin/node]
 5: 0x1002dea5b v8::internal::Heap::FatalProcessOutOfMemory(char const*) [/usr/local/opt/node@14/bin/node]
 6: 0x100316819 v8::internal::EvacuateNewSpaceVisitor::Visit(v8::internal::HeapObject, int) [/usr/local/opt/node@14/bin/node]

待解答的核心疑问:

  1. Promise对象本身是否真的会占用如此高的内存?
  2. NodeJS被公认为适合处理I/O密集型操作,为何该场景下会出现内存占用过高的问题?
  3. Golang仅需10MB内存即可处理1亿个Go Routines,是否说明Golang处理I/O密集型操作的能力优于NodeJS?

问题解答

1. Promise的内存开销问题

1000万级Promise占满4GB堆内存是完全符合V8内存模型的正常现象,不是异常bug。
单个Promise在V8中并非轻量对象:除了Promise实例本身的结构内存,每个Promise还会关联:

  • 内部状态字段、成功/失败回调队列
  • 绑定的resolve/reject函数引用
  • 链式调用then/catch注册的回调闭包
  • 对应setTimeout创建的定时器底层句柄
  • 闭包上下文引用的参数(比如代码里的i值、返回值判断逻辑)

再加上代码里用[...Array(NUM_OF_EXECUTIONS).keys()]提前生成了长度1000万的临时数组,仅这个数组本身就要占用数百MB内存。所有开销累加下来,单条异步任务关联的JS对象平均占用约400字节,1000万条刚好达到4GB左右的V8默认堆内存上限。

2. NodeJS I/O密集场景的内存问题

NodeJS适合I/O密集型场景的核心优势,是不需要为每个等待中的I/O请求分配独立的线程栈,靠单线程事件循环就能调度大量I/O任务,避免了线程切换和线程栈的大额开销,但这绝不代表NodeJS可以无成本承载无限量的待处理任务。

所有已经提交到事件循环、还没执行完成的任务,不管是定时器、网络请求还是文件操作,它关联的回调、上下文、底层资源句柄都必须存在内存里,没有任何运行时能做到零成本持有待执行任务。
这个测试场景完全不符合真实业务逻辑:生产环境的NodeJS服务都会做并发控制,同时处于等待状态的I/O任务一般维持在几百到几万的量级,内存压力极低。无任何限流一次性派发千万级任务,不管用什么技术栈都会碰到内存瓶颈。

3. 和Golang Goroutine的能力对比

“Golang仅需10MB内存即可处理1亿个Goroutine”是典型的脱离实际场景的讹传:

  • Goroutine的初始栈大小为2KB,就算不考虑栈动态增长、不计算Goroutine关联的任务上下文、channel、回调、定时器句柄的开销,1亿个Goroutine仅栈内存就要占用200GB左右,10MB内存连1万个空Goroutine都跑不起来,相关说法要么是极端简化的无逻辑demo,要么是刻意裁剪测试条件的误导性结论。
  • 从并发模型本身来看,Goroutine作为用户态协程,在超大量级并发任务的单任务内存开销上确实比V8的Promise更有优势,但这种优势只有在同时承载百万级以上并发任务时才会体现。在绝大多数后端业务的常规并发量级下(几万并发以内),NodeJS的事件循环模型的吞吐表现、开发效率和Golang没有量级差距,两者选型更多看团队技术栈和业务场景,不存在绝对的优劣之分。

内容的提问来源于stack exchange,提问作者ThaiSon Nguyen

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 12:51:24