Node.js 中使用 Promise.all 并发请求过多导致内存溢出怎么优化?

文章导读
在 Node.js 中解决 Promise.all 并发过多导致的内存溢出,最推荐的做法是引入并发限制库(如 p-limit)控制同时进行的请求数量,或者改用流式处理避免一次性加载所有数据。适用场景为批量 HTTP 请求、文件读写或数据库查询,风险边界在于修改并发逻辑可能改变任务执行顺序或错误捕获机制。
📋 目录
  1. 快速处理思路
  2. 为什么会这样
  3. 分步处理
  4. 怎么验证是否生效
  5. 常见坑
  6. 常见问题
  7. 参考来源
A A

在 Node.js 中解决 Promise.all 并发过多导致的内存溢出,最推荐的做法是引入并发限制库(如 p-limit)控制同时进行的请求数量,或者改用流式处理避免一次性加载所有数据。适用场景为批量 HTTP 请求、文件读写或数据库查询,风险边界在于修改并发逻辑可能改变任务执行顺序或错误捕获机制。

先说结论:必须限制同时发起的异步任务数量,不能依赖 Promise.all 处理无限列表。

  • 先定位:使用 heap snapshot 或 process.memoryUsage 确认内存增长来源是否为响应数据缓冲。
  • 先做:安装并发控制库包裹任务生成器,将并发数限制在 CPU 核心数或网络带宽允许范围内。
  • 再验证:观察服务长时间运行时的堆内存曲线,确认不再出现线性增长或 OOM 崩溃。

快速处理思路

如果线上服务正在报警,优先降低并发数或分批处理,不要直接增加内存限制。

npm install p-limit

在代码中包裹异步任务:

const pLimit = require('p-limit');
const limit = pLimit(5); // 限制同时 5 个
const tasks = urls.map(url => limit(() => fetch(url)));
await Promise.all(tasks);

为什么会这样

Promise.all 会立即执行传入迭代器中的所有 Promise 函数,导致瞬间创建大量网络连接和缓冲区。

Node.js 进程内存受 V8 堆大小限制,默认上限约为 1.4GB 到 2GB 不等。当并发请求过多时,每个请求的响应数据、Socket 对象、TLS 上下文都会占用堆内存。如果响应数据未及时释放或累积速度超过垃圾回收速度,就会触发 Out Of Memory 错误。此外,操作系统对文件描述符(File Descriptors)也有上限,并发过高可能先触发 EMFILE 错误。

分步处理

按照以下步骤重构代码,确保并发可控且错误可追踪。

步骤 1:安装并发控制依赖

使用社区验证较多的 p-limit 库,避免手写并发队列引入边界条件错误。

npm install p-limit

步骤 2:包裹任务生成函数

注意传入 limit 的必须是函数,不能是 Promise 实例,否则限制无效。

const limit = pLimit(10);
const results = await Promise.all(
  ids.map(id => limit(() => queryDatabase(id)))
);

步骤 3:处理单个任务错误

Promise.all 遇到一个 reject 就会整体 reject,建议在内部 catch 或改用 Promise.allSettled。

Node.js 中使用 Promise.all 并发请求过多导致内存溢出怎么优化?
const tasks = ids.map(id => limit(() => 
  queryDatabase(id).catch(err => ({ error: err, id }))
));
const results = await Promise.all(tasks);

步骤 4:大数据改用流式

如果涉及文件处理或大响应体,使用 Stream 管道代替一次性 buffer。

怎么验证是否生效

通过监控指标和日志确认内存稳定,不再出现崩溃。

检查点 1:堆内存使用率

在代码中定期打印内存使用情况,观察 usedHeapSize 是否平稳。

setInterval(() => {
  const usage = process.memoryUsage();
  console.log(usage.heapUsed / 1024 / 1024 + ' MB');
}, 5000);

检查点 2:错误日志

搜索日志中是否还有 FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed 或 JavaScript heap out of memory 字样。

检查点 3:连接数监控

使用 netstat 或监控面板查看 ESTABLISHED 连接数是否维持在合理水位,不再瞬间飙升。

常见坑

  • 直接传 Promise 实例给 limit 函数,导致限制失效,因为 Promise 在传入前已经执行。
  • 忽略数据库连接池上限,Node.js 层限制了并发,但数据库层连接数不足会导致排队超时。
  • 使用 Promise.allSettled 后未处理 reject 状态,导致后续逻辑读取不到正确数据。
  • 增加 `--max-old-space-size` 只是推迟崩溃时间,不能解决内存泄漏或并发过高的根本问题。

常见问题

Promise.all 和 p-limit 的主要区别是什么?

Promise.all 负责等待所有任务完成,p-limit 负责控制同时执行的任务数量,两者通常配合使用。

如果其中一个请求失败,整个批次会中断吗?

使用 Promise.all 时任何一个 reject 都会导致整体抛出异常,建议在单个任务内部捕获错误或改用 Promise.allSettled。

直接增加 Node.js 内存限制能解决这个问题吗?

不能,增加内存只能延缓溢出时间,无法解决并发过高导致的资源耗尽和响应延迟问题。

参考来源

  • npm: p-limit - https://www.npmjs.com/package/p-limit
  • Node.js Documentation: process.memoryUsage() - https://nodejs.org/api/process.html#processmemoryusage