为什么尝试写入大文件会导致 js 堆内存不足

2024-01-23

这段代码

const file = require("fs").createWriteStream("./test.dat");
for(var i = 0; i < 1e7; i++){

    file.write("a");
}

运行大约 30 秒后出现此错误消息

<--- Last few GCs --->

[47234:0x103001400]    27539 ms: Mark-sweep 1406.1 (1458.4) -> 1406.1 (1458.4) MB, 2641.4 / 0.0 ms  allocation failure GC in old space requested
[47234:0x103001400]    29526 ms: Mark-sweep 1406.1 (1458.4) -> 1406.1 (1438.9) MB, 1986.8 / 0.0 ms  last resort GC in old spacerequested
[47234:0x103001400]    32154 ms: Mark-sweep 1406.1 (1438.9) -> 1406.1 (1438.9) MB, 2628.3 / 0.0 ms  last resort GC in old spacerequested


<--- JS stacktrace --->

==== JS stack trace =========================================

Security context: 0x30f4a8e25ee1 <JSObject>
    1: /* anonymous */ [/Users/matthewschupack/dev/streamTests/1/write.js:~1] [pc=0x270efe213894](this=0x30f4e07ed2f1 <Object map = 0x30f4ede823b9>,exports=0x30f4e07ed2f1 <Object map = 0x30f4ede823b9>,require=0x30f4e07ed2a9 <JSFunction require (sfi = 0x30f493b410f1)>,module=0x30f4e07ed221 <Module map = 0x30f4edec1601>,__filename=0x30f493b47221 <String[49]: /Users/matthewschupack/dev/streamTests/...

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
 1: node::Abort() [/usr/local/bin/node]
 2: node::FatalException(v8::Isolate*, v8::Local<v8::Value>, v8::Local<v8::Message>) [/usr/local/bin/node]
 3: v8::internal::V8::FatalProcessOutOfMemory(char const*, bool) [/usr/local/bin/node]
 4: v8::internal::Factory::NewFillerObject(int, bool, v8::internal::AllocationSpace) [/usr/local/bin/node]
 5: v8::internal::Runtime_AllocateInTargetSpace(int, v8::internal::Object**, v8::internal::Isolate*) [/usr/local/bin/node]
 6: 0x270efe08463d
 7: 0x270efe213894
 8: 0x270efe174048
[1]    47234 abort      node write.js

而这段代码

const file = require("fs").createWriteStream("./test.dat");
for(var i = 0; i < 1e6; i++){

    file.write("aaaaaaaaaa");//ten a's
}

几乎立即完美运行并生成 10MB 文件。据我了解,流的要点是两个版本应该在大约相同的时间内运行,因为数据是相同的。甚至增加数量a每次迭代 s 到 100 或 1000 几乎不会增加运行时间,甚至写入 1GB 文件也没有任何问题。在 1e6 次迭代中每次迭代写入一个字符也可以正常工作。

这里发生了什么?


发生内存不足错误是因为您没有等待drain事件被发出,无需等待 Node.js 将缓冲所有写入的块,直到出现最大内存使用量。

.write将返回false如果内部缓冲区大于highWaterMark默认为 16384 字节 (16kb)。在您的代码中,您没有处理返回值.write,因此缓冲区永远不会被刷新。

这可以很容易地使用以下方法进行测试:tail -f test.dat

执行脚本时,您将看到没有任何内容被写入test.dat直到脚本完成。

For 1e7缓冲区应清除 610 次。

1e7 / 16384 = 610

解决方案是检查.write返回值和如果false返回,使用file.once('drain')包裹着一个等待的承诺drain事件被发出

NOTE: writable.writableHighWaterMark在节点 v9.3.0 中添加

const file = require("fs").createWriteStream("./test.dat");

(async() => {

    for(let i = 0; i < 1e7; i++) {
        if(!file.write('a')) {
            // Will pause every 16384 iterations until `drain` is emitted
            await new Promise(resolve => file.once('drain', resolve));
        }
    }
})();

现在如果你这样做tail -f test.dat您将看到脚本仍在运行时数据是如何写入的。


至于为什么 1e7 而不是 1e6 会出现内存问题,我们必须研究一下 Node.Js 如何进行缓冲,这发生在写或缓冲区 https://github.com/nodejs/node/blob/master/lib/_stream_writable.js#L365功能。

此示例代码将使我们能够粗略估计内存使用情况:

const count = Number(process.argv[2]) || 1e6;
const state = {};

function nop() {}

const buffer = (data) => {
    const last = state.lastBufferedRequest;
    state.lastBufferedRequest = {
      chunk: Buffer.from(data),
      encoding: 'buffer',
      isBuf: true,
      callback: nop,
      next: null
    };

    if(last)
      last.next = state.lastBufferedRequest;
    else
      state.bufferedRequest = state.lastBufferedRequest;

    state.bufferedRequestCount += 1;
}

const start = process.memoryUsage().heapUsed;
for(let i = 0; i < count; i++) {
    buffer('a');
}
const used = (process.memoryUsage().heapUsed - start) / 1024 / 1024;
console.log(`${Math.round(used * 100) / 100} MB`);

执行时:

// node memory.js <count>
1e4: 1.98 MB
1e5: 16.75 MB
1e6: 160 MB
5e6: 801.74 MB
8e6: 1282.22 MB
9e6: 1442.22 MB - Out of memory
1e7: 1602.97 MB - Out of memory

所以每个对象都使用~0.16 kb,当执行 1e7 时writes无需等待drain事件中,内存中有 1000 万个这样的对象(公平地说,它在达到 10M 之前就崩溃了)

如果您使用单个也没关系a或 1000,由此带来的内存增加可以忽略不计。


您可以增加节点使用的最大内存--max_old_space_size={MB} flag (当然这不是解决方案,只是为了检查内存消耗而不使脚本崩溃):

node --max_old_space_size=4096 memory.js 1e7

UPDATE:我在内存片段上犯了一个错误,导致内存使用量增加了 30%。我正在为每个创建一个新的回调.write,节点复用nop打回来。


更新二

如果你总是写相同的值(在真实场景中值得怀疑),你可以减少greatly通过每次传递相同的缓冲区来计算内存使用量和执行时间:

const buf = Buffer.from('a');
for(let i = 0; i < 1e7; i++) {
    if(!file.write(buf)) {
        // Will pause every 16384 iterations until `drain` is emitted
        await new Promise(resolve => file.once('drain', resolve));
    }
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

为什么尝试写入大文件会导致 js 堆内存不足 的相关文章

随机推荐

  • PHP 准备好的语句返回 -1

    我使用准备好的语句已经有一段时间了 从来没有遇到过任何问题 现在我正在尝试 sql SELECT PhotoID Caption FROM Photos WHERE EntityID AND TypeID LIMIT iDB new mys
  • 在 Perl 中使用 foreach 或 while 迭代文件有什么区别?

    我有一个文件句柄FILE在 Perl 中 我想迭代文件中的所有行 以下有区别吗 while
  • C、如何结合使用fgets和fscanf

    我有一份大学作业 即使用文件IO 有这样的TXT文件 Brian s213551 50 70 70 50 Alex Fernandes s210011 70 81 50 89 Young Lee s211213 60 80 60 90 an
  • 如何在 MySQL 中调用过程?

    我已经开始检查 MySQL 中的过程 但我所有的努力都不起作用 这是我的程序创建 DELIMITER CREATE PROCEDURE test IN a INT BEGIN SELECT FROM table WHERE id a END
  • KBUILD_DEFCONFIG_KMACHINE ?= defconfig_file 无法按预期工作

    KBUILD DEFCONFIG KMACHINE defconfig file没有按我的预期工作 我学到的第一课 defconfig config Bare Kernel Build 没有 yocto 下面是一个新的 defconfig
  • 在 Python 中调试期间绘制函数

    我曾经在 Matlab 中工作 在调试过程中使用可视化中间结果非常方便 当使用大数组 矩阵和嵌套函数时 plot功能 在Python中 我无法在调试模式下绘制任何内容 带有图形图的窗口永远不会加载 我正在使用Spyder IDE进行编码和m
  • 在 Google 地图的不同图层上显示标记集

    我需要在 Google 地图上显示一组标记 我知道可以直接在 Google 地图上添加标记 但鉴于我有 3 组标记 一组用于商店 一组用于公园 另一组用于酒店 我如何在 3 个不同的图层上显示它们 以便稍后使用 javascript 我可以
  • GRPC 异步响应流 C#

    如何从处理程序外部生成 RPC 的流响应值 具体来说 来自 IObservable 我目前正在执行以下操作 但这会产生跨线程问题 因为AnRxObservable在 RPC 处理程序之间共享 public override Task Get
  • 计算列中唯一值的每个实例

    假设你有一个 SQL 表格 Prices 13 99 14 00 52 00 52 00 52 00 13 99 您如何计算输入不同字段的次数 因此 此类计数的示例将输出 13 99 2 times 14 00 1 times 52 00
  • 在后台运行 Webrick 服务器?

    MBPro shovell myname ruby script server gt Booting WEBrick gt Rails 2 3 8 application starting on http 0 0 0 0 3000 gt C
  • tidytext::unnest_tokens 是否适用于西班牙语字符?

    我正在尝试将 unnest tokens 与西班牙语文本一起使用 它适用于一元语法 但会破坏二元语法中的特殊字符 该代码在 Linux 上运行良好 我添加了一些有关区域设置的信息 library tidytext library dplyr
  • Mono - 通过 SSL 的 HttpWebRequest - 写入标头时出错

    下面抛出一个 System Net WebException Error SendFailure Errorwriting headers over SSL 但工作正常http www google com http www google
  • 加速Python中的双循环

    有没有一种方法可以加快双循环的速度 从而更新上一次迭代的值 In code def calc N m x 1 0 y 2 0 container np zeros N 2 for i in range N for j in range m
  • 当我的应用程序在 Ionic 中关闭时,如何发送通知?

    我正在使用 Ionic 进行移动开发 我实际上正在使用本地通知 https ionicframework com docs native local notifications 每 5 分钟我会检查我的服务器是否有新问题 this chec
  • 类型错误:图像数据的形状无效(3072)

    这是我的事情 我不想在 Colab 上运行 而是想读取本地 CIFAR10 数据集并使用以下代码玩 CNNcolab https colab research google com github tensorflow docs blob m
  • 为什么 devise 不通过 gmail smtp 发送电子邮件?

    我正在使用设备进行身份验证 它提供了一个忘记密码的链接 当我发送电子邮件时 电子邮件未发送 以下是我使用过的设置 你能告诉我为什么 gmail 不发送电子邮件吗 我还打开了 允许不太安全的应用程序发送电子邮件 并且还在 gmail 设置中启
  • SQL Server 时区

    使用 AT TIME ZONE 有一种方法可以获取我的 UTC 时间 而无需在查询中使用 LEFT 结尾的 00 00 AT 我正在这样做 SELECT GETDATE AT TIME ZONE EASTERN standard time
  • SKProductsRequest 返回空结果

    我查看了其他一些答案 它们似乎对我的情况没有帮助 我有一个新应用程序即将首次发布 我正在处理 应用程序内购买 部分 我在之前的应用程序中使用过 IAP 所以我认为转移应该是直接的 然而 问题是 每当我运行 SKProductsRequest
  • 在 C++ 中格式化输出

    在 C 代码中 我有一个双变量矩阵 我将其打印出来 然而 由于它们的位数不同 输出格式被破坏 一种解决方案是做cout precision 5 但我希望不同的列有不同的精度 此外 由于在某些情况下存在负值 因此 标志也会引起问题 如何解决这
  • 为什么尝试写入大文件会导致 js 堆内存不足

    这段代码 const file require fs createWriteStream test dat for var i 0 i lt 1e7 i file write a 运行大约 30 秒后出现此错误消息 lt Last few