节点流导致大量内存占用或泄漏
Node streams cause large memory footprint or leak
我正在使用节点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
从数据库中检索数据。数据量非常大(100多万行)。我用一个虚拟流(见下面的代码)交换了这个可读流,以简化调试,并注意到相同的行为:我的内存使用量每次都会增加约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...
更新只是一个小更新,我一直没有找到原因。我最初的解决方案是使用集群来产生新的进程,这样其他请求仍然可以处理。
我已经更新到节点v4。虽然cpu/内存使用率在处理过程中仍然很高,但似乎已经修复了泄漏(意味着内存使用率下降)。
更新2:这里是各种流api的历史记录:
https://medium.com/the-node-js-collection/a-brief-history-of-node-streams-pt-2-bcb6b1fd74680.12使用Streams 3.
Update:这个答案对于旧的node.js流是正确的。新的流API有一个机制,当可写流跟不上时,暂停可读流。
反压力
看起来你遇到了典型的"背压"node.js问题。本文对此作了详细的解释。
但是这里有一个TL;DR:
你是对的,流是用来不需要加载大量的数据到内存。
但不幸的是,流没有一种机制来知道是否可以继续流。溪流是无声的。他们只是尽可能快地把数据扔到下一个流中。
在您的示例中,您正在读取一个大型csv文件并将其流式传输到客户端。问题是,读取文件的速度比通过网络上传文件的速度要快。因此,数据需要存储在某个地方,直到它们被成功地遗忘。这就是为什么你的内存一直在增长,直到客户端下载完成。
解决方案是将读取流节流到管道中最慢流的速度。也就是说,你在你的阅读流之前加上另一个流,它会告诉你的阅读流什么时候可以读取下一个数据块。
看来你做的一切都是正确的。我复制了您的测试用例,并在v4.0.0中遇到同样的问题。将其从objectMode取出并在对象上使用JSON.stringify
似乎可以防止高内存和高cpu。这导致我建立在JSON.stringify
似乎是问题的根源。使用流库JSONStream代替v8方法为我解决了这个问题。它可以这样使用:.pipe(JSONStream.stringify())
.
先试一下:
- 添加手动/显式垃圾收集调用到你的应用程序,和 添加堆转储
添加代码来清理垃圾并转储剩余的以查找泄漏:
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
npm install heapdump
在我组装的应用程序上强制垃圾回收后,内存使用恢复正常(67MB)。约)。这意味着:
-
可能GC在这么短的时间内没有运行,根本没有泄漏(主要的垃圾收集周期在开始之前可以空闲很长时间)。这是一篇关于V8 GC的好文章,但是没有提到GC的精确计时,只是比较了GC周期,但很明显,花在主GC上的时间越少越好。
-
我没有很好地重新创建你的问题。那么,请看看这里,帮我更好地重现问题。
在Node.js中很容易出现内存泄漏
通常都是小事,比如在创建匿名函数后声明变量,或者在回调中使用函数参数。但这对闭包上下文有很大的不同。因此,有些变量永远不能被释放。
这篇文章解释了可能存在的不同类型的内存泄漏以及如何找到它们。数字4——闭包——是最常见的一个。
我发现了一个可以让你避免泄漏的规则:
- 在给变量赋值之前一定要声明所有的变量。
- 声明所有变量后再声明函数
- 避免在任何靠近循环或大块数据的地方使用闭包
对我来说,看起来你正在加载测试多个流模块。这是为Node社区提供的一个很好的服务,但是您也可以考虑将postgres数据转储缓存到一个文件gzip中,并提供一个静态文件。
或者可以创建自己的可读文件,使用游标并输出CSV(作为字符串/文本)
- 重复应用 d3 转换导致的内存泄漏
- IE7中的blockUI插件内存泄漏25kb
- Javascript闭包-如何防止内存泄漏
- jQuery Draggable:内存泄漏
- "检测到可能的EventEmitter内存泄漏”;使用Gulp+Watchify+Factor捆绑包
- 在Dojo类中递归调用setTimeout时是否存在内存泄漏
- 是内存泄漏
- 将处理程序留在img.onload上是内存泄漏
- 具有并发sse连接的node.js内存泄漏
- 简单对象的Javascript内存泄漏
- WeakMap是否会将我从父/子关系的内存泄漏中拯救出来
- Javascript绘制画布内存泄漏
- 为什么我的svg节点在IE中泄漏内存
- AngularJS指令包装器与Kendo一起泄漏内存
- 经过几次清理尝试后,主干仍在泄漏内存
- 这个js代码如何泄漏内存
- 未处理的postMessage是否泄漏内存
- 动态创建和销毁三.js场景,而不会泄漏内存
- 我的PyV8上下文泄漏内存
- 为什么我的数字时钟功能泄漏内存