Node.js fs.writeFile()清空文件

时间:2015-06-17 08:30:13

标签: javascript node.js fs

我有一个更新方法,每16-40毫秒调用一次,内部我有这个代码:

this.fs.writeFile("./data.json", JSON.stringify({
    totalPlayersOnline: this.totalPlayersOnline,
    previousDay: this.previousDay,
    gamesToday: this.gamesToday
}), function (err) {
    if (err) {
        return console.log(err);
    }
});

如果服务器抛出错误,“data.json”文件有时会变空。我该如何预防?

3 个答案:

答案 0 :(得分:3)

问题

fs.writeFile不是原子操作。这是一个示例程序,我将在strace上运行:

#!/usr/bin/env node
const { writeFile, } = require('fs');

// nodejs won’t exit until the Promise completes.
new Promise(function (resolve, reject) {
    writeFile('file.txt', 'content\n', function (err) {
        if (err) {
            reject(err);
        } else {
            resolve();
        }
    });
});

当我在strace -f下运行并整理输出以显示writeFile操作(which spans multiple IO threads, actually)中的系统调用时,我得到:

open("file.txt", O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 9
pwrite(9, "content\n", 8, 0)            = 8
close(9)                                = 0

如您所见,writeFile分三步完成。

  1. 该文件为open()。这是一个原子操作,使用提供的标志,在磁盘上创建一个空文件,如果文件存在,则截断它。截断文件是一种简单的方法,可以确保只有您编写的内容最终会出现在文件中。如果文件中存在现有数据且文件更长而不是您随后写入文件的数据,则额外数据将保留。要避免这种情况,请截断。
  2. 内容已写入。因为我写了这么短的字符串,所以这是通过单个pwrite()调用来完成的,但是对于更大量的数据,我认为可能nodejs一次只能编写一个块。
  3. 手柄已关闭。
  4. strace的每个步骤都发生在不同的节点IO线程上。这表明fs.writeFile()可能实际上是根据fs.open()fs.write()fs.close()实施的。因此,nodejs不会像处理任何级别的原子那样处理这种复杂的操作 - 因为它不是。因此,如果您的节点进程终止,甚至正常,而不等待操作完成,则操作可以是上述任何步骤。在您的情况下,您在writeFile()完成步骤1之后但在完成步骤2之前看到您的流程退出。

    解决方案

    使用POSIX层以事务方式替换文件内容的常见模式是使用以下步骤:

    1. 将数据写入不同名称的文件。
    2. rename()(或者,在Windows上,MoveFileEx() with MOVEFILE_REPLACE_EXISTING)与您要替换的文件不同的命名文件。
    3. 使用此算法,无论程序何时终止,目标文件都会更新或不更新。而且,更好的是,记录(现代)文件系统将确保,只要您在继续执行步骤2之前{1}}步骤1中的文件,这两个操作将按顺序进行。即,如果您的程序执行步骤1然后执行步骤2但是您拔出插件,则在启动时您会发现文件系统处于以下状态之一:

      • 这两个步骤都没有完成。原始文件是完整的(或者如果以前从未存在过,则它不存在)。替换文件要么不存在(close()算法的第1步,writeFile(),实际上从未成功),存在但是空(open()算法的第1步完成),或存在某些数据(部分完成writeFile()算法的第2步)。
      • 第一步完成。原始文件是完整的(或者如果在它仍然不存在之前它不存在)。替换文件包含您想要的所有数据。
      • 两个步骤都已完成。在原始文件的路径中,您现在可以访问替换数据 - 所有数据,而不是空白文件。您在第一步中写入替换数据的路径不再存在。

      使用此模式的代码可能如下所示:

      writeFile()

      这基本上与您在任何语言/框架中用于同一问题的解决方案相同。

答案 1 :(得分:0)

如果错误是由于输入错误(您想要写入的数据)引起的,那么请确保数据符合要求,然后执行writeFile。 如果错误是由于writeFile失败引起的,即使输入为Ok,也可以检查函数是否执行,直到写入文件为止。一种方法是使用async doWhilst函数。

async.doWhilst(
    writeFile(), //your function here but instead of err when fail callback success to loop again
    check_if_file_null, //a function that checks that the file is not null
    function (err) {
        //here the file is not null
    }
);

答案 2 :(得分:0)

我没有运行一些真正的测试我只是注意到手动重新加载我的ide,有时文件是空的。 我首先尝试的是重命名方法,并注意到同样的问题,但重新创建一个新文件是不太理想的(考虑文件监视等)。

我的建议或我现在正在做的是你自己的readFileSync我检查文件是否丢失或者返回的数据是否为空并在再次尝试之前休眠100毫秒。我认为第三次尝试更多的延迟会真正推动sigma上升一个档次,但目前不会这样做,因为增加的延迟有希望是不必要的负面(在那一点会考虑承诺)。还有其他恢复选项机会相对于您自己的代码,您可以添加以防万一我希望。文件未找到或为空?基本上是另一种方式的重试。

我的自定义writeFileSync有一个添加的标志,可以在使用重命名方法(使用write sub-dir'.new'创建)或普通直接方法之间切换,因为您的代码需要可能会有所不同。根据文件大小可能是我的建议。

在此用例中,文件很小,一次只能由一个节点实例/服务器更新。我可以看到添加随机文件名作为重命名的另一个选项,以允许多台机器在以后需要时编写另一个选项。也许是一个重试限制参数?

我还在想你可以写一个本地的temp,然后通过某种方式复制到共享目标(也可能在目标上重命名以提高速度),然后清理(从本地临时取消链接)。我想这个想法有点推动shell命令所以不是更好。 无论如何,这里的主要想法是如果发现空的话会读两次。我确信通过nodejs 8+到共享的Ubuntu类型的NFS挂载,部分写入是安全的吗?