我正在使用节点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的使用率仍然很高,但它似乎修复了泄漏(意味着内存使用率下降)。
看来你正在做的一切正确。我复制了你的测试用例,在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流,这个答案是正确的。 New Stream API有一个机制 暂停 可写流如果可写流无法跟上。
背压
看起来你已经遇到了经典的“背压”node.js问题。
本文详细解释了它。
但这是一个TL; DR:
你是对的,流用于不必将大量数据加载到内存中。
但不幸的是,流没有一种机制来知道继续流式传输是否可行。溪流是愚蠢的。他们只是尽可能快地在下一个流中投放数据。
在您的示例中,您正在读取大型csv文件并将其流式传输到客户端。问题是,读取文件的速度大于通过网络上传文件的速度。因此,数据需要存储在某个地方,直到它们被成功遗忘为止。这就是为什么你的内存不断增长,直到客户端完成下载。
解决方案是将读取流量调节到管道中最慢流的速度。即你用另一个流来预读你的阅读流,这将告诉你的阅读流什么时候可以读取下一个数据块。
在Node.js中发生内存泄漏太容易了
通常,这是一个小问题,比如在创建匿名函数或在回调中使用函数参数后声明变量。但它对闭包背景产生了巨大的影响。因此,一些变量永远不会被释放。
本文 解释了您可能遇到的不同类型的内存泄漏以及如何找到它们。数字4 - 闭包 - 是最常见的。
我找到了一条允许你避免泄漏的规则:
- 在分配之前始终声明所有变量。
- 声明所有变量后声明函数
- 避免在循环或大块数据附近的任何地方关闭
试试这个:
- 加 手动/显式垃圾回收调用 到你的应用程序,和
- 加 堆转储
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上的时间越少越好。
我没有重新创造你的问题。然后,请看一看 这里 并帮助我更好地重现这个问题。
对我来说,看起来你正在加载测试多个流模块。这是为Node社区提供的一项很好的服务,但您也可以考虑将postgres数据转储缓存到文件gzip,并提供静态文件。
或者也许制作自己的Readable使用光标并输出CSV(作为字符串/文本)。