ファイルシステムのジャーナリングと Python の書き込み安全性

ファイルに書き込んでいる途中でシステムがクラッシュしたら、データはどうなるのか。ジャーナリングファイルシステムはこの問題に対処するが、Python プログラマも知っておくべき限界がある。

ジャーナリングとは

ジャーナリング(またはジャーナル)は、ファイルシステムの変更を「ログ」に先行記録する仕組みだ。

ジャーナリングなし

書き込み途中でクラッシュすると、ファイルシステムの整合性が壊れる可能性がある。起動時に fsck で全体をスキャンする必要がある

ジャーナリングあり

変更操作をまずジャーナルに記録し、その後で実際のデータを書き込む。クラッシュ後はジャーナルを再生するだけで復旧できる

ext4、XFS、NTFS、APFS など現代のファイルシステムはすべてジャーナリングを採用している。

ジャーナリングの 3 つのモード

ext4 を例にすると、3 つのジャーナリングモードがある。

journal モード

メタデータとファイルデータの両方をジャーナルに記録。最も安全だが最も遅い。

ordered モード(デフォルト)

メタデータのみジャーナルに記録。ただしメタデータを書く前にデータを書き込む順序を保証する。

writeback モード

メタデータのみジャーナルに記録。データとメタデータの順序は保証しない。最速だが危険。

# 現在のマウントオプションを確認
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)という仕組みでより強い保証を提供する。

ファイルシステムのジャーナリング

ファイルシステム構造の整合性のみ保証

データベースの WAL

トランザクションレベルでのデータ整合性を保証。コミットされたデータは必ず永続化される

重要なデータを扱うなら、ファイルに直接書き込むよりも SQLite などのデータベースを使うほうが安全な場合が多い。

まとめ

ジャーナリングの役割

ファイルシステムの構造を守る。アプリケーションデータは守らない。

Python で安全に書き込むには

一時ファイルに書き込み → fsync() → アトミックに rename という手順を踏む。

ジャーナリングを過信せず、アプリケーション側でも適切な対策を取ることが重要だ。