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

我正在使用节点v0.12.7,并希望直接从数据库stream到客户端(用于文件下载)。 不过,使用stream时,我注意到了大量的内存占用(可能会发生内存泄漏)。

使用express,我创build了一个端点,只需将可读stream传递给响应,如下所示:

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

我开始使用stream的原因是不必将大量的数据加载到内存中。 这是行为吗?

我还注意到,在stream式传输的时候,我的CPU使用率跳跃到了100%,块(这意味着其他请求无法处理)。

我用这个不正确?

虚拟可读stream代码

 // 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... 

更新

只是一个小小的更新,我从来没有find原因。 我最初的解决scheme是使用集群生成新的进程,以便其他请求仍然可以处理。

我已经更新到节点v4。 尽pipe在处理过程中cpu / mem的使用率仍然很高,但似乎已经修复了漏洞(意味着mem的使用率回落)。

看来你正在做的一切正确。 我复制了你的testing用例,并且在v4.0.0中遇到同样的问题。 把它从objectMode中拿出来,在你的对象上使用JSON.stringify似乎可以防止高内存和高CPU。 这导致我build立在JSON.stringify这似乎是问题的根源。 使用stream库JSONStream而不是v8方法为我解决了这个问题。 它可以像这样使用: .pipe(JSONStream.stringify())

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

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

0.12使用stream3。

更新 :对于旧的node.jsstream,这个答案是正确的。 新的streamAPI有一个机制来暂停可读stream如果可写stream不能跟上。

背压

看起来你已经被经典的“backpressure”node.js问题所打击。 这篇文章详细解释它 。

但是这是一个TL; DR:

你是对的,stream被用来不必将大量的数据加载到内存中。

但不幸的是,stream不能确定是否继续stream式传输。 stream是愚蠢的。 他们只是尽可能快地把数据扔到下一个数据stream中。

在你的例子中,你正在阅读一个大的csv文件并将其传输到客户端。 问题是读取文件的速度比通过networking上传速度快。 所以数据需要存储在某个地方,直到成功被遗忘。 这就是为什么你的内存不断增长,直到客户端完成下载。

解决scheme是将读取stream节stream到pipe道中最慢的stream的速度。 也就是说,您可以在另一个stream的前面加上另一个stream,这个stream将告诉您的阅读stream什么时候可以读取下一个数据块。

只要尝试一下:

  1. 将手动/显式垃圾回收呼叫添加到您的应用程序中
  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_gcnode --expose_gc app.js运行应用程序

  5. 使用Chrome调查转储

在我组装的应用程序上强制垃圾收集之后, 内存使用恢复正常(大约67MB这意味着

  1. 也许GC在这么短的时间内没有运行,而且根本没有泄漏(主要的垃圾收集周期可能会在开始之前闲置很长一段时间)。 以下是关于V8 GC的一篇很好的文章 ,但是在GC确切时序上并不是一个单词,只是在GC循环之间进行比较,但是很明显在主要GC上花费的时间越less越好。

  2. 我没有重新创build你的问题。 那么,请看看这里 ,帮助我更好地重现这个问题。

在Node.js中有一个内存泄漏太容易了

通常,这是一件小事,就像在创build匿名函数或在callback中使用函数参数后声明variables一样。 但是它对封闭的上下文有很大的影响。 因此一些variables永远不能被释放。

本文解释了不同types的内存泄漏,以及如何find它们。 数字4 – 闭包 – 是最常见的一个。

我发现了一个可以避免泄漏的规则:

  1. 在分配它们之前总是声明所有的variables。
  2. 声明所有variables后声明函数
  3. 避免在靠近循环或大块数据的地方closures

对我来说,它看起来像加载testing多个stream模块。 这是为Node社区提供的一个很好的服务,但是您也可以考虑将postgres数据转储caching到文件gzip并提供静态文件。

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