节点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什么时候可以读取下一个数据块。
只要尝试一下:
- 将手动/显式垃圾回收呼叫添加到您的应用程序中
- 添加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); });
-
使用节点的键
--expose_gc
:node --expose_gc app.js
运行应用程序 - 使用Chrome调查转储
在我组装的应用程序上强制垃圾收集之后, 内存使用恢复正常(大约67MB ) 。 这意味着 :
-
也许GC在这么短的时间内没有运行,而且根本没有泄漏(主要的垃圾收集周期可能会在开始之前闲置很长一段时间)。 以下是关于V8 GC的一篇很好的文章 ,但是在GC确切时序上并不是一个单词,只是在GC循环之间进行比较,但是很明显在主要GC上花费的时间越less越好。
-
我没有重新创build你的问题。 那么,请看看这里 ,帮助我更好地重现这个问题。
在Node.js中有一个内存泄漏太容易了
通常,这是一件小事,就像在创build匿名函数或在callback中使用函数参数后声明variables一样。 但是它对封闭的上下文有很大的影响。 因此一些variables永远不能被释放。
本文解释了不同types的内存泄漏,以及如何find它们。 数字4 – 闭包 – 是最常见的一个。
我发现了一个可以避免泄漏的规则:
- 在分配它们之前总是声明所有的variables。
- 声明所有variables后声明函数
- 避免在靠近循环或大块数据的地方closures
对我来说,它看起来像加载testing多个stream模块。 这是为Node社区提供的一个很好的服务,但是您也可以考虑将postgres数据转储caching到文件gzip并提供静态文件。
或者也许使自己的使用光标,并输出CSV(作为string/文本)的Readable。