异步写入文件百万次导致内存不足

时间:2018-04-21 03:15:33

标签: node.js asynchronous

以下是代码:

var fs = require('fs')

for(let i=0;i<6551200;i++){
    fs.appendFile('file',i,function(err){

    })
}

当我运行此代码时,几秒钟后,它会显示:

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory

但文件中没有任何内容!

我的问题是:

  1. 为什么文件中没有字节?
  2. 导致内存不足的地方?
  3. 如何异步写入文件for for loop no mater写入时间有多大?
  4. 感谢提前。

3 个答案:

答案 0 :(得分:1)

这里的底线是fs.appendFile()是一个异步调用而你根本就没有等待&#34;等待&#34;调用在每次循环迭代时完成。这有许多后果,包括但不限于:

  • 回调在解决之前会继续分配,这会导致 &#34;堆内存不足&#34; 最终到达。< / p>

  • 您正在与文件句柄竞争,因为您正在使用的功能实际上是打开/写入/关闭给定的文件,如果您不等待每个回合都这样做,那么您& #39;只是简单地发生冲突。

因此,这里的简单解决方案是 &#34;等待&#34; ,并且一些现代语法糖使这很容易:

const fs = require('mz/fs');

const x = 6551200;

(async function() {
  try {
    const fd = await fs.open('file','w');
    for (let i = 0; i < x; i++) {
      await fs.write(fd, `${i}\n`);
    }
    await fs.close(fd);
  } catch(e) {
    console.error(e)
  } finally {
    process.exit();
  }
})()

这当然需要一段时间,但它不会“爆炸”#34;你的系统在它工作的同时。

第一个简化的事情是获取mz库,它已经包含了常见的nodejs库以及支持promises的每个函数的现代化版本。与使用回调相比,这将有助于清理语法。

接下来要认识到的是fs.appendFile()关于它是如何&#34;打开/写入/关闭&#34;一次通话。这不是很好,所以你通常做的只是open然后write循环中的字节,当它完成时你实际上可以close文件句柄

那&#34;糖&#34;现代版本,虽然&#34;可能&#34; 具有明确的承诺链接,但它仍然不是那么易于管理。因此,如果您实际上没有一个nodejs环境支持async/await糖或工具来转换&#34;这样的代码,那么你可以考虑使用asyncjs libary和普通的回调:

const Async = require('async');
const fs = require('fs');

const x = 6551200;

let i = 0;
fs.open('file','w',(err,fd) => {
  if (err) throw err;

  Async.whilst(
    () => i < x,
    callback => fs.write(fd,`${i}\n`,err => {
      i++;
      callback(err)
    }),
    err => {
      if (err) throw err;
      fs.closeSync(fd);
      process.exit();
    }
  );

});

同样的基本原则适用于我们&#34;等待&#34;在继续之前完成每个回调。这里的whilst()帮助器允许迭代直到满足测试条件,当然不会进行下一次迭代,直到数据被传递给迭代器本身的回调。

还有其他方法可以解决这个问题,但对于一个大型循环来说,这可能是最合理的两个方法。迭代。常见的方法,如&#34;链接&#34;通过.reduce()真的更适合'#34;合理的&#34;您已经拥有的大小数据,并且在这里构建这种大小的数组具有它自己的固有问题。

例如,以下&#34;工作&#34; (至少在我的机器上)但它确实消耗了大量资源:

const fs = require('mz/fs');
const x = 6551200;

fs.open('file','w')
  .then( fd =>
    [ ...Array(x)].reduce(
      (p,e,i) => p.then( () => fs.write(fd,`${i}\n`) )
      , Promise.resolve()
    )
    .then(() => fs.close(fd))
  )
  .catch(e => console.error(e) )
  .then(() => process.exit());

因此,在内存中构建如此大的链然后允许它解决它真的不那么实用。你可以在这上面加上一些&#34;治理&#34; ,但是显示的主要两种方法要简单得多。

对于那种情况,那么你可以获得async/await糖,因为它在当前LTS版本的Node(LTS 8.x)中,或者我会坚持使用其他尝试过的和真正的&#34; async helpers& #34;对于您被限制为没有该支持的版本的回调

你当然可以&#34; promisify&#34;任何与nodejs的最后几个版本相关的函数#34;开箱即用&#34;就像它在哪里一样,Promise已经成为一个全球性的东西了一段时间:

const fs = require('fs');

await new Promise((resolve, reject) => fs.open('file','w',(err,fd) => {
  if (err) reject(err);
  resolve(fd);
});

所以真的没有必要只是为了那样导入库,但是这里给出的mz库为你做了所有这些。因此,在引入其他依赖项时,这取决于个人偏好。

答案 1 :(得分:0)

1 - 文件为空,因为fs.append调用都没有完成,之前Node.JS进程被破坏。

2 - Node.JS堆内存是有限的,并存储回调,直到它返回,而不仅仅是“i”变量。

3 - 你可以尝试使用promises来做到这一点。

"use strict";

const Bluebird = require('bluebird');
const fs = Bluebird.promisifyAll(require('fs'));

let promisses = [];
for (let i = 0; i < 6551200; i++){
    promisses.push(fs.appendFileAsync('file', i + '\n'));
}

Bluebird.all(promisses)
.then(data => {
  console.log(data, 'End.');
})
.catch(e => console.error(e));

但是没有逻辑可以避免这个大循环的堆内存错误。您可以增加Node.JS Heep Memory,或者以合理的方式,获取间隔的数据块:

'use strict';

const fs = require('fs');

let total = 6551200;

let interval = setInterval(() => {
  fs.appendFile('file', total + '\n', () => {});
  total--;
  if (total < 1) {
    clearInterval(interval);
  }
}, 1);

答案 2 :(得分:0)

Javascript是一种单线程语言,这意味着您的代码可以在当时执行一个函数。因此,当您执行异步功能时,它将排除&#34;排队&#34;在下一个要执行的堆栈中。

所以在您的代码中,您正在向堆栈发送6551200个调用,这当然会在开始工作之前使您的应用程序崩溃&#34; appendFile&#34;他们中的任何一个。

你可以通过将循环分成更小的循环,使用async和等待函数或迭代器来实现你想要的。

如果您尝试实现的内容与代码一样简单,则可以使用以下内容:

const fs = require("fs");

function SomeTask(i=0){
    fs.appendFile('file',i,function(err){
        //err in the write function
        if(err) console.log("Error", err);
        //check if you want to continue (loop)
        if(i<6551200) return SomeTask(i);
        //on finish
        console.log("done");
    });
}
SomeTask();

在上面的代码中,你写了一行,完成后,你调用下一行。 此函数仅供基本使用,需要重构并使用Javascript迭代器进行高级用法check out Iterators and generators on MDN web docs