节点:fs write()不在内部循环中写入。为什么不?

时间:2016-06-01 22:10:21

标签: javascript node.js fs

我想创建一个写入流并在我的数据进入时写入它。但是,我能够创建该文件,但没有写入任何内容。最终,该过程耗尽内存。

我发现的问题是我在循环中调用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();

什么都没有写,甚至没有问候。但为什么?如何在循环中写入文件?

3 个答案:

答案 0 :(得分:5)

问题在于你没有给它一个耗尽缓冲的机会。最终这个缓冲区已满,你内存不足。

WriteStream.write返回一个布尔值,指示数据是否已成功写入磁盘。如果未成功写入数据,则应等待drain event,表示缓冲区已耗尽。

以下是使用writedrain事件的返回值编写代码的一种方法:

'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(即使在不再需要内存之后也不会将其释放回系统)。

backpressuring in streams

  

在数据缓冲区已超过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();

Slow - stats

当内存使用量接近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();

Slow - GC

大约4秒后内存使用率达到80%,垃圾收集器gives up trying to Scavenge and is forced to use Mark-sweep(慢10倍以上) - 请参阅this article了解更多详情。

为了进行比较,以下是@ MikeC代码在drain缓冲区已满时等待write的相同可视化:

Fast - stats

Fast - GC

答案 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)