ファイルに書きこみを行ったらかならず fsync しよう

通常、なんの考えも無しにプログラムがファイルに書きこみを行った後、運悪くシステムがクラッシュするとファイルが失われる可能性がある。通常のファイル書き込みはファイルに書き込んでいるように見えて実はバッファがメモリ上に蓄えているだけだからだ。このメモリ上に蓄えられた、いずれストレージに書き込むデータのことをダーティページと言う。何もしなくてもダーティページに書きこまれたデータはカーネルが定期的にストレージに書き込むのだが、若干のタイムラグがある。この間にクラッシュするとデータが失われるわけだ。データが書き込んだ直後、すぐストレージに書き出したい場合は fsync システムコールを呼ぶ必要がある。

C や C++ といった低レイヤーを扱う言語なら fsync を呼ぶだけで良いが、Java のような高級言語でどうやって fsync を発動させればよいか。

通常のファイル書き込み

下記のような一般的なファイル書き込みロジックを実行した際に呼ばれるシステムコールを見てみよう。

public class SyncTest {
    public static void main(String[] args) throws IOException {
        File file = new File("/tmp/hello");
        FileOutputStream output = new FileOutputStream(file);
        output.write("Hello, World!\n".getBytes("UTF-8"));
        output.close();
    }
}

システムコールを見るには strace を使う。

$ strace -f java SyncTest
...
18598 open("/tmp/hello", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 10
18598 fstat(10, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
18598 fcntl(10, F_GETFD)                = 0
18598 fcntl(10, F_SETFD, FD_CLOEXEC)    = 0
18598 write(10, "Hello, World!\n", 14)  = 14
18598 close(10) 
...

"Hello, World!\n" という文字列を write システムコールで書き込んだ後、クローズしている。ちなみに 10 という値はファイルディスクリプタ


ファイル書き込み後に fsync

さて、strace のおかげで晴れて fsync が発行されてないことが判明した。
fsync を発行するには、FileOutputStream#getFD で取得できるファイルディスクリプタオブジェクトの sync メソッドを呼び出せば良い。

public class SyncTest {
    public static void main(String[] args) throws IOException {
        File file = new File("/tmp/hello");
        FileOutputStream output = new FileOutputStream(file);
        output.write("Hello, World!\n".getBytes("UTF-8"));
        output.getFD().sync(); // この行を追加
        output.close();
    }
}

上記コードを実行してシステムコールを監視。すると・・・

$ strace -f java SyncTest
...
19751 open("/tmp/hello", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 10
19751 fstat(10, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0
19751 fcntl(10, F_GETFD)                = 0
19751 fcntl(10, F_SETFD, FD_CLOEXEC)    = 0
19751 write(10, "Hello, World!\n", 14)  = 14
19751 fsync(10)                         = 0    <- fsync が呼ばれている
19751 close(10)                         = 0
...

無事 fsync が呼ばれている。これで、ストレージに書き込まれることが保証される。以前より安全で、障害に強いプログラムになった。


まとめ

作り直せるデータや一時的なデータはここまできっちりやらなくても良いかもしれないが、そうではないデータを書き込んだ時はできるだけ fsync するよう心がけよう。
毎回 finally 構文で fsync 発行するのは記述し忘れが起こるので、ユーティリティ化して flush, sync, close をやってくれるような形にしておいたほうが良いだろう。