为什么尝试写入大文件会导致 js 堆 运行 内存不足
why does attempting to write a large file cause js heap to run out of memory
此代码
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
}
运行s 几乎立即完美地生成了一个 10MB 的文件。据我了解,流的要点是两个版本应该 运行 在大约相同的时间内,因为数据是相同的。即使每次迭代将 a
的数量增加到 100 或 1000,也几乎不会增加 运行ning 时间,并且写入 1GB 的文件也没有任何问题。在 1e6 次迭代中每次迭代写入单个字符也可以正常工作。
这是怎么回事?
发生内存不足错误是因为您没有等待 drain
事件发出,不等待 Node.js 将缓冲所有写入的块,直到发生最大内存使用。
如果内部缓冲区大于默认为 16384 字节 (16kb) 的 highWaterMark
,.write
将 return false
。在您的代码中,您没有处理 .write
的 return 值,因此永远不会刷新缓冲区。
这可以很容易地测试使用:tail -f test.dat
执行脚本时,在脚本完成之前,您会看到 test.dat
上没有写入任何内容。
对于1e7
,缓冲区应该被清除 610 次。
1e7 / 16384 = 610
一个解决方案是检查 .write
return 值,如果 false
被 returned,使用 file.once('drain')
包裹在承诺中等待直到drain
事件已发出
注意: 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 如何进行缓冲,这发生在 writeOrBuffer 函数中。
此示例代码将使我们能够粗略估计内存使用情况:
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
,并且在不等待 drain
事件的情况下执行 1e7 writes
时,内存中有 1000 万个这样的对象(公平地说,它之前崩溃了达到 10M)
使用单个 a
或 1000 并不重要,内存增加可以忽略不计。
您可以使用 --max_old_space_size={MB}
标志增加节点使用的最大内存 (当然这不是解决方案,只是为了检查内存消耗而不会使脚本崩溃):
node --max_old_space_size=4096 memory.js 1e7
更新:我在内存片段上犯了一个错误,导致内存使用量增加了 30%。我正在为每个 .write
创建一个新回调,Node 重用 nop
回调。
更新二
如果您总是写入相同的值(在实际情况下值得怀疑),您可以通过每次传递相同的缓冲区来大大减少内存使用和执行时间:
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));
}
}
此代码
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
}
运行s 几乎立即完美地生成了一个 10MB 的文件。据我了解,流的要点是两个版本应该 运行 在大约相同的时间内,因为数据是相同的。即使每次迭代将 a
的数量增加到 100 或 1000,也几乎不会增加 运行ning 时间,并且写入 1GB 的文件也没有任何问题。在 1e6 次迭代中每次迭代写入单个字符也可以正常工作。
这是怎么回事?
发生内存不足错误是因为您没有等待 drain
事件发出,不等待 Node.js 将缓冲所有写入的块,直到发生最大内存使用。
highWaterMark
,.write
将 return false
。在您的代码中,您没有处理 .write
的 return 值,因此永远不会刷新缓冲区。
这可以很容易地测试使用:tail -f test.dat
执行脚本时,在脚本完成之前,您会看到 test.dat
上没有写入任何内容。
对于1e7
,缓冲区应该被清除 610 次。
1e7 / 16384 = 610
一个解决方案是检查 .write
return 值,如果 false
被 returned,使用 file.once('drain')
包裹在承诺中等待直到drain
事件已发出
注意: 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 如何进行缓冲,这发生在 writeOrBuffer 函数中。
此示例代码将使我们能够粗略估计内存使用情况:
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
,并且在不等待 drain
事件的情况下执行 1e7 writes
时,内存中有 1000 万个这样的对象(公平地说,它之前崩溃了达到 10M)
使用单个 a
或 1000 并不重要,内存增加可以忽略不计。
您可以使用 --max_old_space_size={MB}
标志增加节点使用的最大内存 (当然这不是解决方案,只是为了检查内存消耗而不会使脚本崩溃):
node --max_old_space_size=4096 memory.js 1e7
更新:我在内存片段上犯了一个错误,导致内存使用量增加了 30%。我正在为每个 .write
创建一个新回调,Node 重用 nop
回调。
更新二
如果您总是写入相同的值(在实际情况下值得怀疑),您可以通过每次传递相同的缓冲区来大大减少内存使用和执行时间:
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));
}
}