Write a file correctly with ext4

ext4 で atomicity を保証してファイルに正しく書き込む方法について説明します。 だいたい Don’t fear the fsync! を読めば理解できるので英語を読みたい人はこっちを読んでください。

TL;DR

難しいのでファイルに書かずにデータベースを使おう。

前提

Linux で、ファイルの書き込み対象のディスクは ext4 を data=ordered で mount している状況を前提に書いています。

ポイント

変更したファイルに対して fsync を叩く必要がある

ext4 に限らず、fsync を叩かない限りディスクへ変更が反映されていることを保証できません。O_DIRECT でファイルを開いている場合はこの限りではないですが。 私の経験上、仕事で見たコードではことごとく fsync を叩いていなかったため、この事実はほぼ理解されていないと思います。

ファイルを作成した場合に、parent directory に対して fsync を叩く必要がある

ext4 ではファイルを新規作成した場合は parent directory に対しても fsync を叩く必要があります。これは見落しがちな点なので注意してください。

一時ファイルを作らずに直接ファイルを上書きすると、サーバー障害でファイルが破損する

ext4 を data=ordered で mount している場合、journaling の対象となるのは filesystem の metadata だけです。 データそれ自体は journaling されません。 そのため、filesystem がファイルへの書き込みの途中に電源断した場合などでファイルの中身が中途半端な状態になる可能性があります。

ではこの問題を回避するにはどうすればよいかというと、まず一時ファイルに書き込み、rename(2) で対象のファイルをそのファイルで置き換える、です。 POSIX の仕様で rename(2) は atomic であることが保証されています。

さらに、他にもデータも障害時に破損させない方法に以下の選択肢があります:

fsync の前に fflush や fclose を叩かない

IO のライブラリ側でバッファリングされているデータが存在する可能性があるので、flush する必要があります。詳しくは man 3 fflush してください。

正しくファイルに書き込む方法

以下にサンプルコードを載せておきます。間違いがあれば指摘してください。(goto を使ってるのは意図的にやってますし、本質には関係ないので、無視してください。)

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <dirent.h>

int main(int argc, char** argv)
{
  char *content     = "This is a pen.\0";
  char *file_path   = "/tmp/foo.txt.new\0";
  size_t length     = strlen(content);
  DIR *parent_dir   = opendir("/tmp\0");
  FILE *file_handle = fopen(file_path, "w+");
  
  if (file_handle == NULL || parent_dir == NULL) {
    goto EXIT_AFTER_REPORT_ERROR;
  }

  if (fwrite(content, sizeof(char), length, file_handle) != length) {
    goto EXIT_AFTER_REPORT_ERROR;
  };
  
  if (fflush(file_handle) != 0) {
    goto EXIT_AFTER_REPORT_ERROR;
  }

  if (fsync(fileno(file_handle)) == -1 || fsync(dirfd(parent_dir)) == -1) {
    goto EXIT_AFTER_REPORT_ERROR;
  }

  if (rename(file_path, "/tmp/foo.txt\0") == -1) {
    goto EXIT_AFTER_REPORT_ERROR;
  }

  closedir(parent_dir);
  fclose(file_handle);
  
  return 0;
  
 EXIT_AFTER_REPORT_ERROR:
  fprintf(stderr, "%s\n", strerror(errno));
  closedir(parent_dir);
  fclose(file_handle);
  exit(-1);
}

Comments

comments powered by Disqus