Redisの命綱「AOF」の深層:BGREWRITEAOFとfsync戦略の完全調律
テックリードの私だ。今日のコードレビューで、あるジュニアエンジニアが「Redisの永続化?とりあえずデフォルトのAOFにしておけば安全ですよね」と言った。私は即座にマージを止めた。
甘い。実に甘い。
Redisは「インメモリの高速性」という甘美な麻薬の裏で、ディスクI/Oという現実の物理法則と常に戦っている。特にAOF(Append Only File)のメカニズムを理解せず、デフォルト設定のまま本番稼働させることは、時限爆弾を抱えて高速道路を逆走するようなものだ。
今回は、AOF書き換え(`BGREWRITEAOF`)の内部挙動、ファイル圧縮のメカニズム、そしてパフォーマンスを殺さずにデータ損失を防ぐ`fsync`ポリシーの極意を、実務の現場で即座に使えるレベルで叩き込む。
—
1. なぜAOFは肥大化するのか?(AOFの根本矛盾)
Redisは、書き込みコマンドをすべてログとして追記(Append)していくことで耐久性を担保する。これがAOFだ。
しかし、考えてみてほしい。
SET user:100:name “Alice”
SET user:100:name “Bob”
SET user:100:name “Charlie”
INCR counter
INCR counter
INCR counter
最終的に必要なデータは `user:100:name = “Charlie”` と `counter = 3` だけだ。しかし、AOFファイルには過去の無駄な履歴がすべて残る。結果として、メモリ上のデータが数GBであっても、AOFファイルが数十GBに膨れ上がり、リストア(再起動時のロード)に途方もない時間がかかる事態が発生する。
この矛盾を解決するのが `BGREWRITEAOF` だ。
—
2. BGREWRITEAOFの裏側:Copy-on-Writeとフォークの魔術
「AOFファイルを小さくする」と言っても、稼働中のRedisがクライアントからのリクエストを処理しながらファイルを書き換えるのは至難の業だ。ここでRedisの美しきアーキテクチャが光る。
`BGREWRITEAOF`(Background Rewrite AOF)は、名前の通りバックグラウンドで実行される。その内部フローはこうだ。
[Client] —> SET / GET —> [Redis Main Process]
|
+— fork() —> [Child Process]
|
+— メモリのスナップショットを走査
| 現在の状態を「最小限のコマンド群」に変換
| —> 新しいAOFファイルへ書き出し
1. `fork()` の実行:メインプロセスが子プロセスをフォークする。
2. Copy-on-Write (CoW):Linuxの仮想記憶機構により、フォーク直時は親子でメモリ空間を共有する。メインプロセスがデータを更新したメモリ領域のみ、OSが裏側で複製を作る(CoW)。これにより、メモリの二重消費を最小限に抑える。
3. 子プロセスによる再構築:子プロセスは、現在のメモリ上のデータから、それを復元するために必要な最小限のコマンド群(例:大量の `SET` をひとまとめにした `RDB-in-AOF` 形式や最適化されたコマンド)を新しいAOFファイルに書き出す。
4. 差分(AOF Buffer)の追記:書き換え中にメインプロセスに届いた新たな書き込みコマンドは、Redis内部の「AOF rewrite buffer」に蓄積される。
5. アトミックな切り替え:子プロセスの書き出しが完了すると、メインプロセスにシグナルが送られ、蓄積された差分を新しいAOFファイルに追記し、ファイル名を原子的(Atomic)に置き換える。
⚠️ 実務上の罠:メモリオーバヘッド
`fork()` を行う瞬間、Linuxはページテーブルをコピーする。Redisのメモリ使用量が巨大(例: 30GB)で、かつパパッと書き換えを行おうとすると、`fork()` 自体が数秒間メインスレッドをブロック(Stall)させる危険性がある。
さらに、CoWによってメモリ使用量が最悪の場合倍増する。物理メモリの空き容量(`vm.overcommit_memory = 1` の設定含む)と、`.conf` の `auto-aof-rewrite-percentage` のチューニングはセットで行わなければならない。
例: AOFファイルが前回のサイズから100%以上大きく、かつ64MB以上の場合に自動リライト
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
プロダクション環境では、このデフォルト値(64mb)は小さすぎる。最低でも `1gb`〜`5gb` 程度に引き上げ、無駄な頻度のフォークを防ぐべきだ。
—
3. fsyncポリシーの三択:パフォーマンスと耐久性のトレードオフ
AOFは、ファイルに書き込み(`write()`)を行っただけでは、OSのページキャッシュに乗るだけで、即座に不揮発性ストレージ(SSD等)に書き込まれるとは限らない。サーバがクラッシュした際、キャッシュ上のデータは消える。
これを強制的にディスクへ書き込ませるのが `fsync` だ。
`redis.conf` の `appendfsync` パラメータには3つの選択肢がある。テックリードとして、それぞれの特性を正確に理解してほしい。
① `appendfsync always` (安全性の極み、パフォーマンスの殺人鬼)
- 挙動:コマンドが実行され、AOFに書き込まれる都度、`fsync` を呼ぶ。
- データ損失:理論上ゼロ(直前の1コマンドすら失われない)。
- パフォーマンス:最悪。ディスクの性能(特にIOPS)に完全にスループットが縛られる。NVMe SSDであっても、秒間数千QPSが限界になる。
- 判定:金融系のトランザクションなど、1バイトたりともデータを失えない極限の要件以外では使用厳禁。
② `appendfsync everysec` (実務のデファクトスタンダード)
- 挙動:1秒に1回、バックグラウンドスレッドで `fsync` を実行する。
- データ損失:最悪の場合、直近1秒分のデータが失われる。
- パフォーマンス:非常に良好。`always` に比べてオーバーヘッドが極めて少ない。
- 判定:迷ったらこれを選べ。ほとんどのWebアプリケーションにおいて、1秒のデータロス許容と引き換えに得られる高スループットは合理的だ。
③ `appendfsync no` (OS任せの野獣)
- 挙動:`fsync` を自分からは呼ばない。OSがフラッシュしたいタイミング(通常30秒おきなど)に任せる。
- データ損失:OSのクラッシュや電源断時に、数秒〜数十秒分のデータが消える可能性がある。
- パフォーマンス:最高。
- 判定:データが消えてもキャッシュとして再構築できる(例:セッションストアの裏側、ランキングのスコア等)場合を除き、設計レビューでは「リジェクト」すべき設定。
—
4. チーフアーキテクトからの設計提言(まとめ)
本番環境でRedisのAOFを扱う際は、以下の3点を必ず死守せよ。
1. `everysec` を基本としつつ、SSDの選定にこだわる
`fsync` はストレージのIOPSに依存する。AWSであれば、`gp3` などのベースラインIOPSが担保され、かつバーストが効くボリュームを選ぶこと。低品質なストレージで `everysec` を回すと、fsyncのブロックによってレイテンシスパイク(P99の悪化)を引き起こす。
2. 自動リライトの閾値をデータ量に合わせて引き上げる
小まめな `BGREWRITEAOF` はCPUとメモリ(CoW)を無駄に消費する。メモリ使用量が数十GB規模のシステムでは、`auto-aof-rewrite-min-size` は `2gb`〜`5gb` に設定し、システムがアイドルな時間帯に手動で `BGREWRITEAOF` を叩く運用も検討せよ。
3. RDB(Snapshot)とのハイブリッドを検討せよ
Redis 4.0以降では、AOFの中にRDBのスナップショットを含める AOF-RDBハイブリッド永続化(`aof-use-rdb-preamble yes`)が利用可能だ。再起動時のロード時間が劇的に短縮されるため、これを使わない手はない。
インメモリデータベースの強みを最大限に活かしつつ、障害時に絶望しないための備えをする。それこそが、プロフェッショナルなバックエンドエンジニアの仕事だ。
次のスプリントのインフラ設計書、見直しておいてくれよ。
コメント