Ошибка нехватки памяти происходит из-за того, что вы не ожидаете, пока не будет отправлено событие drain
, без ожидания. Node.js будет буферизовать все записанные чанки до максимального использования памяти.
.write
вернет false
, если внутренний буфер больше highWaterMark
, по умолчанию 16384 байта (16 КБ).В вашем коде вы не обрабатываете возвращаемое значение .write
, поэтому буфер никогда не очищается.
Это можно очень легко проверить с помощью: tail -f test.dat
При выполненииВ вашем сценарии вы увидите, что ничего не пишется на test.dat
до завершения сценария.
Для 1e7
буфер должен быть очищен 610 раз.
1e7 / 16384 = 610
Решение состоит в том, чтобы проверить .write
возвращаемое значение, и если false
возвращается, используйте 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
, и при выполнении 1e7 writes
без ожидания события drain
у вас есть 10 миллионов таких объектов в памяти (если честно, он падает до достижения 10M)
Неважно, если вы используете одну a
или 1000, увеличение памяти от этого незначительно.
Максимальный объем памяти, используемой узлом, можно увеличить с помощью флага --max_old_space_size={MB}
(Конечно, это не решение, просто проверка потребления памяти без сбоя сценария) :
node --max_old_space_size=4096 memory.js 1e7
ОБНОВЛЕНИЕ : Я допустил ошибку в фрагменте памяти, что привело к увеличению использования памяти на 30%.Я создавал новый обратный вызов для каждого .write
, узел повторно использует nop
обратный вызов.
ОБНОВЛЕНИЕ II
Если вы пишете всегда одинаковозначение (сомнительно в реальном сценарии), вы можете значительно уменьшить использование памяти и время выполнения, передавая каждый раз один и тот же буфер:
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));
}
}