flush() と fsync() は似ているようで全く異なる。この違いを理解していないと、「保存したはずのデータが消えた」という事態に遭遇する。
flush() と fsync() の決定的な違い
Python/C ライブラリのバッファからカーネルのバッファに転送する。ディスクには書き込まない
カーネルのバッファからディスクに書き込む。この関数が返ったとき、データはディスク上にある
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 まで遅延する可能性がある。やや速い
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 を呼び、通常の書き込みは OS に任せる。
sync コマンドとの関係
シェルの sync コマンドはシステム全体の dirty ページをディスクに書き込む。
# システム全体を同期 sync # Python から呼ぶ場合 import os os.sync()
ただし、特定のファイルだけを同期したい場合は fsync() を使うべきだ。sync() はシステム全体に影響するため重い。
まとめ
| 操作 | データの到達先 | 用途 |
|---|---|---|
| flush() | カーネルバッファ | 他プロセスから見えるようにする |
| fsync() | ディスク | 電源断でもデータを保証 |
| fdatasync() | ディスク(データのみ) | やや高速な永続化 |
「ファイルを閉じれば安心」ではない。close() は暗黙的に flush() を呼ぶが、fsync() は呼ばない。重要なデータを扱うなら、明示的に fsync() を呼ぶ習慣をつけよう。