节点流导致大量内存占用或泄漏
我正在使用节点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...
更新
只是一个小小的更新,我从来没有找到原因。 我最初的解决方案是使用集群产生新的进程,以便其他请求仍然可以处理。
我已经更新到节点v4。 尽管在处理过程中cpu / mem的使用率仍然很高,但似乎已经修复了漏洞(意味着mem的使用率回落)。
看起来你正在做的一切正确。 我复制了你的测试用例,并且在v4.0.0中遇到了同样的问题。 将它从objectMode中取出并在对象上使用JSON.stringify
似乎可以防止高内存和高CPU。 这导致我建立了JSON.stringify
,这似乎是问题的根源。 使用流式库JSONStream而不是v8方法为我解决了这个问题。 它可以像这样使用: .pipe(JSONStream.stringify())
。
更新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流,这个答案是正确的。 如果可写入的流跟不上,新的Stream API有一种机制来暂停可读流。
背压
看起来你受到了经典的“backpressure”node.js问题的困扰。 本文详细解释它。
但是这里有一个TL; DR:
你是对的,流被用来不必将大量的数据加载到内存中。
但不幸的是,流没有机制知道是否可以继续流式传输。 流是愚蠢的。 他们只是尽可能快地将数据投入下一个数据流。
在你的例子中,你正在读取一个大型的csv文件并将其传输到客户端。 问题是读取文件的速度大于通过网络上传文件的速度。 所以数据需要存储在某个地方,直到成功被遗忘。 这就是为什么你的内存不断增长,直到客户端完成下载。
解决方法是将阅读流限制为管道中最慢流的速度。 也就是说,您可以在另一个流的前面加上另一个流,它将告诉您的阅读流什么时候可以阅读下一个数据块。
在Node.js中发生内存泄漏非常容易
通常,这是一件小事,例如在创建匿名函数或在回调中使用函数参数后声明变量。 但是它对封闭上下文有很大的影响。 因此一些变量永远不能被释放。
本文解释了您可能具有的不同类型的内存泄漏以及如何找到它们。 数字4 - 闭包 - 是最常见的一个。
我发现了一条可以避免泄漏的规则: