ファイルに書き込んでいる途中でシステムがクラッシュしたら、データはどうなるのか。ジャーナリングファイルシステムはこの問題に対処するが、Python プログラマも知っておくべき限界がある。
ジャーナリングとは
ジャーナリング(またはジャーナル)は、ファイルシステムの変更を「ログ」に先行記録する仕組みだ。
書き込み途中でクラッシュすると、ファイルシステムの整合性が壊れる可能性がある。起動時に fsck で全体をスキャンする必要がある
変更操作をまずジャーナルに記録し、その後で実際のデータを書き込む。クラッシュ後はジャーナルを再生するだけで復旧できる
ext4、XFS、NTFS、APFS など現代のファイルシステムはすべてジャーナリングを採用している。
ジャーナリングの 3 つのモード
ext4 を例にすると、3 つのジャーナリングモードがある。
メタデータとファイルデータの両方をジャーナルに記録。最も安全だが最も遅い。
メタデータのみジャーナルに記録。ただしメタデータを書く前にデータを書き込む順序を保証する。
メタデータのみジャーナルに記録。データとメタデータの順序は保証しない。最速だが危険。
# 現在のマウントオプションを確認 mount | grep "on / " # /dev/sda1 on / type ext4 (rw,relatime,errors=remount-ro,data=ordered)
ジャーナリングが守るもの、守らないもの
ジャーナリングは「ファイルシステムの整合性」を守るが、「アプリケーションデータの整合性」は守らない。
ファイルシステムの構造(ディレクトリ、inode など)の破損を防ぐ
書き込み途中のファイル内容。アプリケーションレベルでの対策が必要
つまり、ファイルに 1000 行書き込む途中でクラッシュした場合、500 行だけ書かれた中途半端なファイルが残る可能性がある。
Python での安全な書き込みパターン
アプリケーションレベルでデータを守るには、アトミックな書き込みパターンを使う。
import os
import tempfile
def atomic_write(filepath, content):
"""中途半端な状態を残さない書き込み"""
dir_name = os.path.dirname(filepath)
# 同じディレクトリに一時ファイルを作成
fd, temp_path = tempfile.mkstemp(dir=dir_name)
try:
with os.fdopen(fd, 'w') as f:
f.write(content)
f.flush()
os.fsync(f.fileno()) # ディスクへの書き込みを強制
# アトミックにリネーム
os.replace(temp_path, filepath)
except:
os.unlink(temp_path)
raise
このパターンでは、書き込み途中でクラッシュしても、元のファイルは無傷のまま残る。
fsync() の重要性
write() を呼んでも、データはカーネルのバッファに留まっている可能性がある。ディスクに確実に書き込むには fsync() が必要だ。
write() → Python のバッファに書く
flush() → カーネルのバッファに書く
fsync() → ディスクに書く(これで永続化される)
with open("important.txt", "w") as f:
f.write("critical data")
f.flush() # カーネルバッファまで
os.fsync(f.fileno()) # ディスクまで書き込む
ジャーナリングファイルシステムでも、fsync() を呼ばない限りデータがディスクに到達する保証はない。
rename のアトミック性
同一ファイルシステム内での rename() / os.replace() は POSIX でアトミックと規定されている。
# この操作はアトミック
os.replace("temp_file.txt", "target_file.txt")
# クラッシュが起きても、以下のどちらかの状態になる:
# 1. リネーム前の状態(temp_file.txt が存在、target_file.txt は古いまま)
# 2. リネーム後の状態(temp_file.txt は削除、target_file.txt は新しい内容)
# 中途半端な状態にはならない
ただし、fsync() でデータをディスクに書き込んだ後に rename() しないと、rename 自体はアトミックでもデータが失われる可能性がある。
データベースとの比較
データベースは WAL(Write-Ahead Logging)という仕組みでより強い保証を提供する。
ファイルシステム構造の整合性のみ保証
トランザクションレベルでのデータ整合性を保証。コミットされたデータは必ず永続化される
重要なデータを扱うなら、ファイルに直接書き込むよりも SQLite などのデータベースを使うほうが安全な場合が多い。
まとめ
ファイルシステムの構造を守る。アプリケーションデータは守らない。
一時ファイルに書き込み → fsync() → アトミックに rename という手順を踏む。
ジャーナリングを過信せず、アプリケーション側でも適切な対策を取ることが重要だ。