почему попытка записи большого файла приводит к нехватке памяти кучи js - PullRequest
0 голосов
/ 15 мая 2018

этот код

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
}

работает идеально почти мгновенно и выдает файл размером 10 МБ. Как я понял, смысл потоков заключается в том, что обе версии должны работать примерно за одинаковое время, поскольку данные идентичны. Даже увеличение числа a s до 100 или 1000 за итерацию едва ли увеличивает время работы даже и записывает файл размером 1 ГБ без каких-либо проблем. Запись одного символа за одну итерацию на 1e6 итерациях также работает нормально.

Что здесь происходит?

1 Ответ

0 голосов
/ 16 мая 2018

Ошибка нехватки памяти происходит из-за того, что вы не ожидаете, пока не будет отправлено событие 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));
    }
}
...