我正在使用节点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的使用率仍然很高,但它似乎修复了泄漏(意味着内存使用率下降)。
答案 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 - 闭包 - 是最常见的一个。
我找到了一条允许你避免泄漏的规则:
答案 3 :(得分:0)
在此之前试试这个:
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
在application I assembled上强制进行垃圾回收后,内存使用量恢复正常( 67MB 。约。)。 表示:
也许GC没有在这么短的时间内运行而且根本没有泄漏(主要的垃圾收集周期在启动前可能会闲置一段时间)。 Here is a good article on V8 GC,但不是关于GC确切时间的说法,只是将gc周期相互比较,但很明显,花在主要GC上的时间越少越好。
我没有重新创建你的问题。那么,请看一下here并帮助我更好地重现这个问题。
答案 4 :(得分:-1)
对我而言,您似乎正在加载测试多个流模块。这是为Node社区提供的一项很好的服务,但您也可以考虑将postgres数据转储缓存到文件gzip,并提供静态文件。
或者也许制作自己的Readable使用光标并输出CSV(作为字符串/文本)。