繁体   English   中英

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

[英]Node.js fs.writeFile() empties the file

我有一个更新方法,它大约每 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”文件有时会变为空。 我该如何防止?

问题

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操作(实际上跨越多个 IO 线程)的系统调用时,我得到:

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() ed。 这是一个原子操作,使用提供的标志,要么在磁盘上创建一个空文件,要么在文件存在时截断它。 截断文件是确保只有您编写的内容最终出现在文件中的一种简单方法。 如果在文件中现有的数据和文件比你后来写入文件中的数据越长,额外的数据将保持。 为了避免这种情况,您可以截断。
  2. 内容写好了。 因为我写了这么短的字符串,这是通过单个pwrite()调用完成的,但是对于大量数据,我认为 nodejs 可能一次只写一个块。
  3. 把手关闭。

我的strace将这些步骤中的每一个都发生在不同的节点 IO 线程上。 这向我表明fs.writeFile()实际上可能是根据fs.open()fs.write()fs.close() 因此,nodejs 不会将这种复杂的操作视为在任何级别上都是原子的——因为它不是。 因此,如果您的节点进程在不等待操作完成的情况下正常终止,则该操作可能在上述任何步骤中。 在您的情况下,您会看到您的进程在writeFile()完成第 1 步之后但在完成第 2 步之前退出。

解决方案

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

  1. 将数据写入一个不同名称的文件, fsync()文件(请参阅“确保数据到达磁盘”中的“何时应该 fsync? ),然后close()它。
  2. rename() (或者,在 Windows 上,使用MoveFileEx()MOVEFILE_REPLACE_EXISTING )将不同名称的文件覆盖在要替换的文件上。

使用此算法,无论程序何时终止,目标文件都会更新或不更新。 而且,更好的是,日志式(现代)文件系统将确保,只要您在进行步骤 2 之前在步骤 1 中fsync()文件,这两个操作就会按顺序发生。 即,如果您的程序执行第 1 步,然后执行第 2 步,但您拔掉插头,则启动时您会发现文件系统处于以下状态之一:

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

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

const { writeFile, rename, } = require('fs');

function writeFileTransactional (path, content, cb) {
    // The replacement file must be in the same directory as the
    // destination because rename() does not work across device
    // boundaries.

    // This simple choice of replacement filename means that this
    // function must never be called concurrently with itself for the
    // same path value. Also, properly guarding against other
    // processes trying to use the same temporary path would make this
    // function more complicated. If that is a concern, a proper
    // temporary file strategy should be used. However, this
    // implementation ensures that any files left behind during an 
    // unclean termination will be cleaned up on a future run.
    let temporaryPath = `${path}.new`;
    writeFile(temporaryPath, content, function (err) {
        if (err) {
            return cb(err);
        }

        rename(temporaryPath, path, cb);
    });
};

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

如果错误是由于错误输入(您要写入的数据)引起的,请确保数据符合预期,然后执行 writeFile。 如果错误是由于 writeFile 失败导致的,即使输入正常,您可以检查该函数是否在写入文件之前执行。 一种方法是使用 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
    }
);

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

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

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

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

我还认为您可以写入本地临时文件,然后通过某种方式复制到共享目标(也可能重命名目标以提高速度),然后进行清理(取消与本地临时文件的链接)。 我想这个想法是将它推到 shell 命令中,所以不是更好。 无论如何,这里的主要思想仍然是如果发现为空则阅读两次。 我确定它不会被部分写入,通过 nodejs 8+ 到共享的 Ubuntu 类型 NFS 安装,对吗?

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM