我想创建一个写入流并在我的数据进入时写入它。但是,我能够创建该文件,但没有写入任何内容。最终,该过程耗尽内存。
我发现的问题是我在循环中调用write()。
这是一个简单的例子:
'use strict'
var fs = require('fs');
var wstream = fs.createWriteStream('myOutput.txt');
for (var i = 0; i < 10000000000; i++) {
wstream.write(i+'\n');
}
console.log('End!')
wstream.end();
什么都没有写,甚至没有问候。但为什么?如何在循环中写入文件?
答案 0 :(得分:5)
问题在于你没有给它一个耗尽缓冲的机会。最终这个缓冲区已满,你内存不足。
WriteStream.write
返回一个布尔值,指示数据是否已成功写入磁盘。如果未成功写入数据,则应等待drain
event,表示缓冲区已耗尽。
以下是使用write
和drain
事件的返回值编写代码的一种方法:
'use strict'
var fs = require('fs');
var wstream = fs.createWriteStream('myOutput.txt');
function writeToStream(i) {
for (; i < 10000000000; i++) {
if (!wstream.write(i + '\n')) {
// Wait for it to drain then start writing data from where we left off
wstream.once('drain', function() {
writeToStream(i + 1);
});
return;
}
}
console.log('End!')
wstream.end();
}
writeToStream(0);
答案 1 :(得分:4)
要补充@ MikeC的excellent answer,以下是writable.write()
当前文档(v8.4.0)中的一些相关详细信息:
如果返回
false
,则应该停止进一步尝试将数据写入流中,直到发出'drain'
事件为止。当流没有耗尽时,对
write()
的调用将缓冲chunk
,并返回false
。一旦所有当前缓冲的块被耗尽(接受由操作系统传递),将发出'drain'
事件。建议在write()
返回false
后,在发出'drain'
事件之前不再写入任何块。虽然允许在不耗尽的流上调用write()
,但 Node.js将缓冲所有写入的块,直到最大内存使用量发生,此时它将无条件地中止。即使在它中止之前,高内存使用率也会导致垃圾收集器性能不佳和高RSS(即使在不再需要内存之后也不会将其释放回系统)。
在数据缓冲区已超过
highWaterMark
或写入队列当前正忙的任何情况下,.write()
将返回false
。当返回
false
值时,背压系统启动。清空数据缓冲区后,将发出
.drain()
事件并恢复传入的数据流。队列完成后,背压将允许再次发送数据。正在使用的内存空间将自行释放并为下一批数据做好准备。
+-------------------+ +=================+
| Writable Stream +---------> .write(chunk) |
+-------------------+ +=======+=========+
|
+------------------v---------+
+-> if (!chunk) | Is this chunk too big? |
| emit .end(); | Is the queue busy? |
+-> else +-------+----------------+---+
| emit .write(); | |
^ +--v---+ +---v---+
^-----------------------------------< No | | Yes |
+------+ +---v---+
|
emit .pause(); +=================+ |
^-----------------------+ return false; <-----+---+
+=================+ |
|
when queue is empty +============+ |
^-----------------------< Buffering | |
| |============| |
+> emit .drain(); | ^Buffer^ | |
+> emit .resume(); +------------+ |
| ^Buffer^ | |
+------------+ add chunk to queue |
| <---^---------------------<
+============+
以下是一些可视化(使用--max-old-space-size=512
运行V8堆内存大小为512MB的脚本)。
此可视化显示i
的每10,000个步骤的heap memory usage(红色)和增量时间(紫色)(X轴显示i
):
'use strict'
var fs = require('fs');
var wstream = fs.createWriteStream('myOutput.txt');
var latestTime = (new Date()).getTime();
var currentTime;
for (var i = 0; i < 10000000000; i++) {
wstream.write(i+'\n');
if (i % 10000 === 0) {
currentTime = (new Date()).getTime();
console.log([ // Output CSV data for visualisation
i,
(currentTime - latestTime) / 5,
process.memoryUsage().heapUsed / (1024 * 1024)
].join(','));
latestTime = currentTime;
}
}
console.log('End!')
wstream.end();
当内存使用量接近512MB的最大限制时,脚本运行得越来越慢,直到达到限制时它最终崩溃。
此可视化使用v8.setFlagsFromString()
和--trace_gc
来显示每个垃圾收集的当前内存使用情况(红色)和执行时间(紫色)(X轴显示以秒为单位的总耗用时间):
'use strict'
var fs = require('fs');
var v8 = require('v8');
var wstream = fs.createWriteStream('myOutput.txt');
v8.setFlagsFromString('--trace_gc');
for (var i = 0; i < 10000000000; i++) {
wstream.write(i+'\n');
}
console.log('End!')
wstream.end();
大约4秒后内存使用率达到80%,垃圾收集器gives up trying to Scavenge
and is forced to use Mark-sweep
(慢10倍以上) - 请参阅this article了解更多详情。
为了进行比较,以下是@ MikeC代码在drain
缓冲区已满时等待write
的相同可视化:
答案 2 :(得分:2)
作为补充(甚至更多),@ Mike Cluck的answer使用节点流pipe()
实现了具有相同行为的解决方案。也许对某人有用。
根据{{3}}(节点11.13.0):
visible.pipe()方法将可写流附加到可读的, 使它自动切换到流动模式并推动所有 将其数据发送到附加的Writable。 数据流将 自动管理,以便不写入目标可写流 更快的可读流不知所措。
因此,pipe()
开箱即用地提供了反压策略。所有需要的是以某种方式创建Readable流。在我的示例中,我从节点流模块扩展Readable
类以创建简单的计数器:
const { Readable } = require('stream');
const fs = require('fs');
const writeStream = fs.createWriteStream('./bigFile.txt');
class Counter extends Readable {
constructor(opt) {
super(opt);
this._max = 1e7;
this._index = 1;
}
_read() {
const i = this._index++;
if (i > this._max)
this.push(null);
else {
this.push(i + '\n');
}
}
}
new Counter().pipe(writeStream);
行为完全相同-数据以小块的形式不断地推送到文件中,并且内存消耗是恒定的(在我的机器上约为50MB)。
关于pipe()
的伟大之处在于,如果您提供了可读流(来自请求),则只需使用readable.pipe(writable)
。