Подтвердить что ты не робот

Потоки Node вызывают большой объем памяти или утечку памяти

Я использую node v0.12.7 и хочу напрямую передавать данные из базы данных клиенту (для загрузки файла). Однако при использовании потоков я замечаю большой объем памяти (и возможную утечку памяти).

С помощью выражения я создаю конечную точку, которая просто передает читаемый поток в ответ следующим образом:

app.post('/query/stream', function(req, res) {

  res.setHeader('Content-Type', 'application/octet-stream');
  res.setHeader('Content-Disposition', 'attachment; filename="blah.txt"');

  //...retrieve stream from somewhere...
  // stream is a readable stream in object mode

  stream
    .pipe(json_to_csv_transform_stream) // I've removed this and see the same behavior
    .pipe(res);
});

В процессе производства читаемый stream извлекает данные из базы данных. Объем данных довольно большой (1M + строки). Я поменял этот читаемый поток фиктивным потоком (см. Код ниже), чтобы упростить отладку и замечать одно и то же поведение: мое использование памяти увеличивается на ~ 200 М каждый раз. Иногда сбор мусора вбивается, и память падает немного, но она линейно возрастает, пока у моего сервера не хватит памяти.

Причина, по которой я начал использовать потоки, заключалась в том, чтобы не приходилось загружать большие объемы данных в память. Ожидается ли такое поведение?

Я также замечаю, что при потоковой передаче мое использование процессора скачкообразно достигает 100% и блоков (что означает, что другие запросы не могут быть обработаны).

Я использую это неправильно?

Чистое чтение кода потока

// Setup a custom readable
var Readable = require('stream').Readable;

function Counter(opt) {
  Readable.call(this, opt);
  this._max = 1000000; // Maximum number of records to generate
  this._index = 1;
}
require('util').inherits(Counter, Readable);

// Override internal read
// Send dummy objects until max is reached
Counter.prototype._read = function() {
  var i = this._index++;
  if (i > this._max) {
    this.push(null);
  }
  else {
    this.push({
      foo: i,
      bar: i * 10,
      hey: 'dfjasiooas' + i,
      dude: 'd9h9adn-09asd-09nas-0da' + i
    });
  }
};

// Create the readable stream
var counter = new Counter({objectMode: true});

//...return it to calling endpoint handler...

Update

Просто небольшое обновление, я так и не нашел причину. Моим первоначальным решением было использовать cluster для создания новых процессов, чтобы другие запросы все равно могли быть обработаны.

С тех пор я обновился до node v4. Несмотря на то, что использование процессора/памяти во время обработки все еще остается высоким, оно, похоже, устраняет утечку (это означает, что использование памяти возвращается).

4b9b3361

Ответ 1

Кажется, вы все делаете правильно. Я скопировал ваш тестовый пример и испытываю ту же проблему в версии 4.0. Вывод его из objectMode и использование JSON.stringify на вашем объекте, как оказалось, предотвратили как высокую память, так и высокую скорость процессора. Это привело меня к встроенному JSON.stringify, который, как представляется, является корнем проблемы. Использование потоковой библиотеки JSONStream вместо метода v8 исправил это для меня. Его можно использовать следующим образом: .pipe(JSONStream.stringify()).

Ответ 2

Обновление 2. Здесь представлена ​​история различных API-интерфейсов Stream:

https://medium.com/the-node-js-collection/a-brief-history-of-node-streams-pt-2-bcb6b1fd7468

0.12 использует потоки 3.

Обновить. Этот ответ был прав для старых node.js потоков. В API нового потока есть механизм, позволяющий приостановить чтение, если поток записи не может быть в курсе.

противодавление

Похоже, вы столкнулись с классической проблемой "противодавления" node.js. Эта статья подробно объясняет это.

Но здесь TL; DR:

Вы правы, потоки используются, чтобы не загружать большие объемы данных в память.

Но, к сожалению, потоки не имеют механизма, чтобы знать, нормально ли продолжать потоковое вещание. Потоки тупые. Они просто бросают данные в следующий поток так быстро, как только могут.

В вашем примере вы читаете большой файл csv и передаете его клиенту. Дело в том, что скорость чтения файла больше, чем скорость его загрузки через сеть. Поэтому данные должны храниться где-нибудь, пока их не смогут успешно забыть. Вот почему ваша память продолжает расти, пока клиент не завершит загрузку.

Решение состоит в том, чтобы подавать поток чтения на скорость самого медленного потока в трубе. То есть вы добавляете свой поток чтения другим потоком, который будет показывать ваш поток чтения, когда будет нормально читать следующий фрагмент данных.

Ответ 3

Просто попробуйте это прежде всего:

  • Добавить вручную/явные вызовы сбора мусора в ваше приложение и
  • Добавить heapdump npm install heapdump
  • Добавьте код для очистки мусора и оставите остальную часть, чтобы найти утечку:

    var heapdump = require('heapdump');
    
    app.post('/query/stream', function (req, res) {
    
        res.setHeader('Content-Type', 'application/octet-stream');
        res.setHeader('Content-Disposition', 'attachment; filename="blah.txt"');
    
        //...retrieve stream from somewhere...
        // stream is a readable stream in object mode
    
        global.gc();
        heapdump.writeSnapshot('./ss-' + Date.now() + '-begin.heapsnapshot');
    
        stream.on('end', function () {
            global.gc();
            console.log("DONNNNEEEE");
            heapdump.writeSnapshot('./ss-' + Date.now() + '-end.heapsnapshot');
        });
    
        stream
                .pipe(json_to_csv_transform_stream) // I've removed this and see the same behavior
                .pipe(res);
    });
    
  • Запустите приложение с помощью клавиши node --expose_gc: node --expose_gc app.js

  • Исследуйте дампы с Chrome

После того, как я принудительно собрал сбор мусора в приложении которое я собрал, использование памяти вернулось к нормальному состоянию ( 67 МБ. прибл.). Это означает:

  • Возможно, GC был запущен за такой короткий промежуток времени, и утечки вообще нет (основной цикл сбора мусора может простаивать довольно долго до начала). Вот хорошая статья о V8 GC, однако ни слова о точном тайминге GC, только в сравнении gc-циклов друг с другом, но ясно что чем меньше времени тратится на основной GC, тем лучше.

  • Я не воссоздал вас хорошо. Тогда, пожалуйста, посмотрите здесь и помогите мне лучше воспроизвести проблему.

Ответ 4

Слишком легко иметь утечку памяти в Node.js

Обычно это второстепенная вещь, например объявление переменной после создания анонимной функции или использование аргумента функции внутри обратного вызова. Но это имеет огромное значение для контекста закрытия. Таким образом, некоторые переменные никогда не могут быть освобождены.

В этой статье объясняются различные типы утечек памяти, которые могут возникнуть у вас и как их найти. Число 4 - Closures - является наиболее распространенным.

Я нашел правило, которое позволит вам избежать утечек:

  • Всегда указывайте все свои переменные перед их назначением.
  • Объявлять функции после объявления всех переменных
  • Избегайте замыканий где-нибудь рядом с петлями или большими кусками данных.

Ответ 5

Мне кажется, что вы загружаете несколько модулей потока. Это хорошая услуга для сообщества Node, но вы также можете рассмотреть возможность кэширования дампа данных postgres в файле, gzip и обслуживать статический файл.

Или, может быть, сделать свой собственный Readable, который использует курсор и выводит CSV (как строку/текст).