发生内存不足错误是因为您没有等待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));
}
}