节点流导致大量内存占用或泄漏

时间:2015-09-18 22:19:46

标签: javascript node.js memory-leaks stream httpresponse

我正在使用节点v0.12.7并希望直接从数据库流式传输到客户端(用于文件下载)。但是,在使用流时,我注意到大量内存占用(以及可能的内存泄漏)。

使用express,我创建一个端点,只需将可读流管道到响应中,如下所示:

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 +行)。我用虚拟流交换了这个可读流(参见下面的代码)以简化调试并注意到相同的行为:我的内存使用量每次都会跳跃大约200M。有时垃圾收集会启动并且内存会下降一点,但它会线性上升,直到我的服务器内存不足。

我开始使用流的原因是必须将大量数据加载到内存中。预期会出现这种情况吗?

我还注意到,在流式传输时,我的CPU使用率会跳至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...

更新

只是一个小小的更新,我从未找到原因。我最初的解决方案是使用cluster生成新进程,以便仍然可以处理其他请求。

我已经更新到节点v4。虽然在处理过程中cpu / mem的使用率仍然很高,但它似乎修复了泄漏(意味着内存使用率下降)。

5 个答案:

答案 0 :(得分:5)

看来你正在做的一切都正确。我复制了您的测试用例,在v4.0.0中遇到了同样的问题。从objectMode中取出并在对象上使用JSON.stringify似乎可以防止高内存和高CPU。 这导致我内置JSON.stringify,这似乎是问题的根源。使用流式库JSONStream而不是v8方法为我修复了这个问题。它可以像这样使用:.pipe(JSONStream.stringify())

答案 1 :(得分:5)

更新2 :以下是各种Stream API的历史记录:

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

0.12使用Streams 3.

更新:对于旧的node.js流,这个答案是正确的。新流API具有暂停可读流的机制,如果可写流不能跟上。

<强>背压

看起来你已被经典&#34;背压&#34; node.js问题。 This article explains it in detail

但这里是TL; DR:

你是对的,流习惯于不必将大量数据加载到内存中。

但不幸的是,流媒体并没有机制知道继续流媒体是否可以。溪流是愚蠢的。他们只是尽可能快地在下一个流中投放数据。

在您的示例中,您正在阅读大型csv文件并将其流式传输到客户端。问题是,读取文件的速度大于通过网络上传文件的速度。因此,数据需要存储在某个地方,直到它们被成功遗忘为止。这就是为什么在客户端下载之前你的记忆力不断增长的原因。

解决方案是将读取流量调节到管道中最慢流的速度。即你用另一个流来预读你的阅读流,这将告诉你的阅读流什么时候可以读取下一个数据块。

答案 2 :(得分:1)

在Node.js

中发生内存泄漏太容易了

通常,这是一个小问题,比如在创建匿名函数或在回调中使用函数参数后声明变量。但它对闭包背景产生了巨大的影响。因此,一些变量永远不会被释放。

This article解释了您可能遇到的不同类型的内存泄漏以及如何找到它们。数字4 - 闭包 - 是最常见的一个。

我找到了一条允许你避免泄漏的规则:

  1. 在分配之前始终声明所有变量。
  2. 声明所有变量后声明函数
  3. 避免在循环或大块数据附近的任何地方关闭

答案 3 :(得分:0)

在此之前试试这个:

  1. manual/explicit garbage collection calls添加到您的应用中,然后
  2. 添加heapdump npm install heapdump
  3. 添加代码以清理垃圾并转储其余部分以查找泄漏:

    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);
    });
    
  4. 使用节点的密钥--expose_gc运行您的应用:node --expose_gc app.js

  5. Investigate dumps with Chrome
  6. application I assembled上强制进行垃圾回收后,内存使用量恢复正常( 67MB 。约。)表示

    1. 也许GC没有在这么短的时间内运行而且根本没有泄漏(主要的垃圾收集周期在启动前可能会闲置一段时间)。 Here is a good article on V8 GC,但不是关于GC确切时间的说法,只是将gc周期相互比较,但很明显,花在主要GC上的时间越少越好。

    2. 我没有重新创建你的问题。那么,请看一下here并帮助我更好地重现这个问题。

答案 4 :(得分:-1)

对我而言,您似乎正在加载测试多个流模块。这是为Node社区提供的一项很好的服务,但您也可以考虑将postgres数据转储缓存到文件gzip,并提供静态文件。

或者也许制作自己的Readable使用光标并输出CSV(作为字符串/文本)。