flush() と fsync() の違い:本当にディスクに書き込まれたか

flush()fsync() は似ているようで全く異なる。この違いを理解していないと、「保存したはずのデータが消えた」という事態に遭遇する。

flush() と fsync() の決定的な違い

flush()

Python/C ライブラリのバッファからカーネルのバッファに転送する。ディスクには書き込まない

fsync()

カーネルのバッファからディスクに書き込む。この関数が返ったとき、データはディスク上にある

import os

f = open("important.txt", "w")
f.write("critical data")

f.flush()
# この時点: カーネルのバッファにある(ディスクにはない)

os.fsync(f.fileno())
# この時点: ディスクに書き込まれた

f.close()

なぜ flush() だけでは不十分か

flush() の後、データはカーネルに渡されるが、カーネルはすぐにディスクに書き込むとは限らない。

flush() 呼び出し

データがカーネルのページキャッシュに入る

カーネルが「いつか」ディスクに書き込む(数秒〜数十秒後)

電源断が起きると、ページキャッシュのデータは消える

Linux のデフォルトでは、dirty ページ(変更されたがディスクに書き込まれていないページ)は 30 秒ほど保持される。この間に電源が落ちるとデータは失われる。

fsync() の動作

fsync() はシステムコールであり、カーネルにディスク書き込みを「強制」する。

import os

def safe_write(path, content):
    """データ消失を防ぐ書き込み"""
    with open(path, "w") as f:
        f.write(content)
        f.flush()            # カーネルに渡す
        os.fsync(f.fileno()) # ディスクに書き込む

fsync() はブロッキング関数で、ディスク I/O が完了するまで返らない。そのため低速だが、戻り値を受け取った時点でデータの永続化が保証される。

fdatasync() との違い

fsync() はデータとメタデータ(ファイルサイズ、更新時刻など)の両方を同期する。fdatasync() はデータのみを同期し、メタデータはスキップする場合がある。

fsync()

データ + メタデータを同期。より安全だがやや遅い

fdatasync()

データを同期。メタデータは次回の fsync まで遅延する可能性がある。やや速い

import os

f = open("data.txt", "w")
f.write("content")
f.flush()

# どちらか選ぶ
os.fsync(f.fileno())      # データ + メタデータ
os.fdatasync(f.fileno())  # データのみ(Linux 専用)

ディレクトリの fsync

新しいファイルを作成した場合、ファイルだけでなくディレクトリの fsync も必要になる場合がある。

import os

def really_safe_write(path, content):
    """完全に安全な書き込み"""
    dir_path = os.path.dirname(path) or "."
    
    with open(path, "w") as f:
        f.write(content)
        f.flush()
        os.fsync(f.fileno())
    
    # ディレクトリエントリも同期
    dir_fd = os.open(dir_path, os.O_RDONLY | os.O_DIRECTORY)
    try:
        os.fsync(dir_fd)
    finally:
        os.close(dir_fd)

ディレクトリの fsync がないと、ファイルの内容は書き込まれていても、ディレクトリエントリ(ファイル名と inode の対応)が失われる可能性がある。

パフォーマンスへの影響

fsync() は高コストな操作だ。毎回呼び出すとパフォーマンスが大幅に低下する。

import os
import time

# fsync なし: 高速
start = time.time()
with open("test.txt", "w") as f:
    for i in range(10000):
        f.write(f"line {i}\n")
print(f"fsync なし: {time.time() - start:.3f}")

# 毎回 fsync: 非常に低速
start = time.time()
with open("test2.txt", "w") as f:
    for i in range(10000):
        f.write(f"line {i}\n")
        f.flush()
        os.fsync(f.fileno())
print(f"毎回 fsync: {time.time() - start:.3f}")

# 結果例:
# fsync なし: 0.015秒
# 毎回 fsync: 15.234秒(1000倍遅い)

実践的なガイドライン

fsync が必要なケース

金融取引、設定ファイルの保存、データベースのコミットなど、データ消失が許されない場合。

fsync が不要なケース

ログファイル、キャッシュ、再生成可能なデータなど、消失しても問題ないか再取得可能な場合。

バランスを取る

重要なチェックポイントでのみ fsync を呼び、通常の書き込みは OS に任せる。

sync コマンドとの関係

シェルの sync コマンドはシステム全体の dirty ページをディスクに書き込む。

# システム全体を同期
sync

# Python から呼ぶ場合
import os
os.sync()

ただし、特定のファイルだけを同期したい場合は fsync() を使うべきだ。sync() はシステム全体に影響するため重い。

まとめ

操作データの到達先用途
flush()カーネルバッファ他プロセスから見えるようにする
fsync()ディスク電源断でもデータを保証
fdatasync()ディスク(データのみ)やや高速な永続化

「ファイルを閉じれば安心」ではない。close() は暗黙的に flush() を呼ぶが、fsync() は呼ばない。重要なデータを扱うなら、明示的に fsync() を呼ぶ習慣をつけよう。