Java
Python
Linux
C言語
fsync

全言語で気をつけるべき、ファイル書き込み時のお作法

言いたいこと

重要なファイルを書くときは、予期しないOSシャットダウンなどを考慮した書き方にする必要があるというお話。
お作法を知らないと、中途半端なファイルや空ファイルが生成され、システム起動時や連携システムで致命的なことになる。

例としてC言語/Java/Pythonを挙げるが、ほぼすべての言語で対策する必要あり。

背景

本番運用されているソフトウェアが起動しなくなるという致命的な不具合が発生した。

ログやコンフィグファイルを収集・解析したところ、コンフィグファイルがぶっ壊れていた。

コンフィグファイルは起動時に読まれるが、必要に応じて書き込まれることもある。
コードを追っていくと、書き込み処理中に電源が落ちたりすると、中途半端に書かれる可能性があることに気づく。

使い終わると電源がぶち切りされる運用をされており、奇跡的にタイミングが重なったのかもしれない。

まずやってみたこと

コンフィグファイルに直で書いちゃ、わずかな時間とはいえ中途半端な状態ができるからだめじゃん。
config.yml.tmpにいったん書きこみ完了させて、その後config.ymlにリネームすれば全部が更新されるか、されないかアトミックにできる!

結果1

満を持してリリースするが、また起動しなくなる不具合が発生。
今度はコンフィグファイルが0KBになっている。
tmpファイル書き込みに失敗したらリネームしないから0KBになることは絶対フロー上はありえないはずなのに。
flushも明示的にしてるし、closeもきちんとしている。

お手上げ状態で、手元にあるオライリー本 Linuxシステムプログラミングをなんとなくパラパラとめくっていると、とある項目を発見。

  • fsyncとfdatasync

なになに・・?ダーティバッファをディスクに書き出します・・?

ここでようやく誤りと解決法に気づく。そういや知識としては学んだのに出てこなかった。

ダーティバッファ

ファイルは書き込み処理をプログラムしてもすぐには書き込まれずダーティバッファという形式で一旦保存される。

ディスク書き出し処理は極めて遅い処理のため、いちいち書き出していたらプログラムがめちゃくちゃ(100倍以上とかそのレベルで)遅くなる。
それを回避するため、最近のファイルシステムでは普通に取り入れられている手法である。
この手法では、プロセスはOSに遅い書き込み処理を委託し、自身は先に進むことができる。OSがディスクにいつ書き込むかはOSがやり取りするファイルシステムに依存する。
場合によっては10秒以上かかることもあり、ファイルを壊すには十分すぎるほどのリードタイムがある。

この状態で予期しない電源断などでOSが落ちたりすると、プログラムフロー上はflush、closeしていたとしても、ぶっ壊れたファイルや空ファイルが出来上がる。
つまり、config.yml.tmp自体が中途半端だったから、リネームでconfig.ymlにしても半端だったというわけ。

fsync()を呼ぶと、ダーティバッファがディスクに即時書き込みをして、完了してから次の処理に進める。

fsync()とfdatasync()

Linuxではファイルは以下の二種類のデータで構成される。

  • inodeと呼ばれるメタデータ
  • ファイルそのもののデータ

inodeはファイルの更新日時とか、ディレクトリで管理する際に表示されるデータのこと。

fsync()では両方書き出して、fdatasync()ではファイルそのもののデータのみ書き出す。

あまり更新されないファイルであったり、性能を気にしなくて良い場面ならfsync()でOK。

inode(つまり最終更新時刻とかのメタデータ)は最悪更新されなくてもいい場合や、頻繁に更新されて性能が気になる場合はfdatasync()を使う。

対策

重要ファイル生成時は、ディスクまで強制的に書き出せば予期しないシャットダウンなどがあっても安心。
なお、Linuxでは正規の手順を踏んだシャットダウンでは問題にならない。

いくつかコードの具体例を挙げる。エラー処理は一部省略。

sample.c
int sample_write() {
    const char* tmp_file_path = "/tmp/fsync_test.tmp";
    FILE *fp= fopen(tmp_file_path , "w");
    int fd = fileno(fp);
    fputs("fsync test()", fp);

    // ここがポイント!!!
    int fsync_ret = fsync(fd);

    fclose(fp);
    return fsync_ret;
}

FsyncTest.java
import java.io.File;
import java.io.FileOutputStream;

public class FsyncTest {
    public static void main(String[] args) throws Exception {
        File file = new File("./fsync.txt");
        FileOutputStream output = new FileOutputStream(file);
        output.write("fsync() test\n".getBytes("UTF-8"));
        output.getFD().sync(); // これがポイント!!
        output.close();
    }
}
sample.py
import os

with open('fsync_test.txt', 'w') as f:
    f.write('fsync() test')
    f.flush() # これだけだとダーティバッファ
    os.fsync(f.fileno()) # ここでようやくダーティバッファがディスクに書き出される

結果2

ファイルは無事壊れなくなった。

ちなみに、ダーティバッファとなっている時間は結構長い。ファイルシステムによっては30秒近くとか。
対策前の話だが、手元のWindows 7では、ファイルを書き込んで、テキストエディタで開いて内容を書き込まれているのを確認し、15秒後に電源を抜いてみたら、そのファイルが起動後にぶっ壊れていた。
CentOS6環境で試してもほぼ同様の結果だった。

対策後は書き込み直後でも壊れなくなった。

結論

言語に限らず、重要なファイル生成時にはfsync()もしくは同等の処理は必須。
ただし、極めて遅い同期的なディスク書き出しを強制することになるので、条件によっては性能に深刻な影響が出る。
安全だからと闇雲に行うのはNG。

補足

コメントに、本記事と関連度の高いリンクを張っていただけました。

firefoxの課題

https://www.atmarkit.co.jp/flinux/rensai/watch2009/watch05a.html

これは、fsync()が遅いことに起因する問題ですね。
fdatasync()を使うことでinodeが更新されない分だけ早くなるという記事です。

PostgreSQLの課題

https://masahikosawada.github.io/2019/02/17/PostgreSQL-fsync-issue/
fsync()が失敗した場合、再度fsync()を呼んでもダメだという問題。
PostgreSQLではfsync()に失敗したらデータベースをクラッシュさせて、トランザクションログ(WAL)から
復旧させる修正をしたらしい。

そもそも失敗なんてするのかと思ったが、SAN、NFSでは簡単に発生するらしい。
一般のシステムで書くなら、write()をするデータを保持しておいて、もう一度write()からやり直せばいいかな。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away