Redis AOFリライトバッファの深層:フォークの裏側で何が起きているのか
大規模なRedisインスタンスを運用するアーキテクトにとって、AOF(Append-Only File)の肥大化とリライトの制御は避けて通れない実務の壁だ。`BGREWRITEAOF` は、バックグラウンドで安全にAOFファイルを再構築するためのエレガントなメカニズムに見える。
だが、その裏側で何が起きているかを知る者は少ない。
OSの `fork()`、Copy-on-Write(CoW)、そして今回焦点を当てる「AOFリライトバッファ(AOF rewrite buffer)」。このメモリ領域の挙動を誤認すれば、本番環境での突然のレイテンシスパイク、さらには OOM Killer による容赦ないプロセス強制終了(SIGKILL)を引き起こす。
本稿では、リファレンスの表面をなぞるような解説はしない。Redisのソースコードの深部に踏込み、AOFリライトバッファの内部メカニズム、メモリ構造、そして限界を突破するための実践的チューニングの極意を授けよう。
—
1. 2つのバッファの混同を断つ:AOFバッファ vs AOFリライトバッファ
まず、アーキテクチャの前提として明確に区別しなければならないのが、「通常のAOFバッファ」と「AOFリライトバッファ」の差異だ。
- AOFバッファ(`server.aof_buf`):
クライアントからの書き込みコマンドをディスク(OSページキャッシュ)へフラッシュするまでの間、一時的に保持するリングバッファ。
- AOFリライトバッファ(`server.aof_rewrite_buf_blocks`):
`BGREWRITEAOF` が実行され、子プロセスがディスク上に新しいAOFファイルを作成しているまさにその最中に、メインプロセス(親プロセス)に到達した新規の書き込みコマンドを蓄積するための専用領域。
なぜこの2つが分かれているのか? 答えはシンプルだ。親プロセスがファイル再構築(リライト)の作業から関与せず、非同期に処理を進めるためである。
—
2. 内部メカニズム:`BGREWRITEAOF` のライフサイクルとバッファの変遷
AOFリライトがトリガーされた瞬間から完了するまでの内部シーケンスを、レイヤードに分解する。
[Client] —> (Write Commands) —> [Redis Main Process]
|
(1. fork())
|
+———————-+———————-+
| |
[Child Process] [Main Process]
(Snapshot Dump) (Continues Serving)
| |
| (2. AOF Rewrite Buffer)
| [新規書き込みを蓄積]
| |
(3. EOF Marker) |
|<--- (4. Pipe / Socket / Temp File) ---------+
| (溜まった差分コマンドを流し込む)
|
(5. Rename Temp File) ---> [New AOF File]
ステップ1: `fork()` と CoW の発動
`BGREWRITEAOF` が呼ばれると、Redisは `fork()` を実行する。これにより子プロセスが生成され、現在のメモリ上のデータセットをシリアライズして新しいAOFファイルの書き込みを開始する。この時点では、親プロセスは通常のクライアントからの読み書きリクエストを処理し続ける。
ステップ2: AOFリライトバッファの活性化
`fork()` が成功した瞬間から、Redisのメインループは新規に受け付けたすべての書き込みコマンド(データ変更を伴うもの)を、通常のAOFバッファだけでなく、AOFリライトバッファにも複製して送り込むようになる。
ここで重要となるのが、AOFリライトバッファのデータ構造だ。単なる連続したメモリブロックではなく、効率的な動的メモリ管理を行うためにリンクリスト(連結リスト)形式のブロック構造を採用している。
/ Redisのソースコード(aof.c周辺)の概念的構造 /
typedef struct aofrwBlock {
unsigned long used, free;
char buf[AOF_RW_BUF_BLOCK_SIZE]; / 通常は10MBなどの固定サイズ /
} aofrwBlock;
なぜ単一の巨大な動的配列(`sds`など)ではなく、ブロックのリストなのか?
それは、リライト処理が数分から数時間に及ぶ場合、書き込みの総量が数GBに達することがあるためである。連続した巨大なメモリ領域を確保しようとすると、メモリの断片化(Fragmentation)やアロケーションのオーバーヘッドが致命的になる。ブロック単位でオンデマンドにアロケーションする設計は、極めて理にかなったデータベース・エンジニアリングの所産である。
ステップ3: 差分の流し込みとアトミックな切り替え
子プロセスがメモリ上のスナップショットの書き出しを完了すると、親プロセスに対してパイプ等を通じて完了を通知する。
この瞬間、親プロセスは以下のクリティカルな処理を実行する。
1. AOFリライトバッファに溜まったすべてのコマンドを、新しいAOFファイルへ追記(Flush)する。
2. 新しく作成した一時AOFファイルを、本来のAOFファイルにアトミックにリネーム(`rename()`)する。
このステップ1の最中、新規書き込みはAOFリライトバッファの末尾に追加され続ける。Redisはバッファが空になるまでこの追記ループを高速に回し、最終的にバッファが空になった時点で古いAOFファイルと新しいAOFファイルを差し替える。
—
3. 限界領域:AOFリライトバッファが引き起こす「悪夢」
この仕組みを理解していれば、大規模環境でどのような障害が起きるかが予測できるはずだ。
1. メモリの二重消費と OOM Killer の罠
AOFリライト中、親プロセス側では以下のメモリが同時に専有される。
- データセット本体のメモリ
- OSの CoW(Copy-on-Write)によって発生する、ページ書き換えに伴う追加メモリ
- AOFリライトバッファが消費するメモリ
もし、リライト処理中に高頻度の書き込み(例:数万 QPS の `HSET` や `LPUSH`)が継続した場合、AOFリライトバッファのサイズはギガバイト単位で肥大化する。
物理メモリの限界を超えてバッファが拡大した結果、Linuxカーネルの OOM Killer が発動し、Redisプロセスが強制終了させられる――これが、熟練エンジニアが最も恐れる障害シナリオの一つだ。
2. リライト完了時のレイテンシスパイク(Stall)
AOFリライトが完了し、バッファに溜まった膨大な差分コマンドを新しいAOFファイルに書き込む瞬間、Redisのメインスレッドは一時的にブロック(Stall)される。
シングルスレッドで動作するRedisにおいて、数GB規模のバッファをディスクに吐き出す同期書き込み(あるいはそれに準ずる処理)が発生すると、数ミリ秒から場合によっては数秒間のレイテンシスパイクが発生し、フロントエンドのアプリケーション群が一斉にタイムアウトエラーを吐き出すことになる。
—
4. チューニングの極意:アーキテクトが取るべき実践的対策
このメカニズムを踏まえ、プロダクション環境でAOFリライトバッファを制御し、システムの限界を突破するための実践知を提示する。
1. ワークロードに応じたリライトの抑制
無闇に `auto-aof-rewrite-percentage` をデフォルト(100%など)のまま小さく設定してはならない。
データセットが大きければ大きいほど、リライト頻度が高くなり、システム全体のリソースが圧迫される。
redis.conf の推奨指針
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64gb # 小さなインスタンスでの頻繁なリライトを防ぐ
書き込みが多い巨大インスタンスでは、`auto-aof-rewrite-min-size` を十分に大きく設定し、リライトの発生回数そのものを抑制することが鉄則だ。
2. Linuxカーネルパラメータの最適化(Overcommit & Transparent Huge Pages)
AOFリライトバッファの肥大化やCoWによるメモリ枯渇を防ぐため、OS側の設定は以下の状態に固定せよ。
- `vm.overcommit_memory = 1`:
`fork()` 時のメモリ確保失敗を防ぐため、カーネルのメモリ過剰コミットを許可する。
- Transparent Huge Pages (THP) の無効化 (`never`):
THPが有効な状態でCoWが発生すると、2MB単位の巨大なページ単位でメモリコピーが走り、AOFリライト中のメモリ使用量が跳ね上がる。必ず無効化すること。
カーネルパラメータの即時適用例
sudo sysctl -w vm.overcommit_memory=1
echo never > /sys/kernel/mm/transparent_hugepage/enabled
3. AOFから RDB / Redis on SSD / Hybrid Persistence への移行検討
もし、書き込みスループットが極限まで高く、AOFリライトバッファの管理が運用上のボトルネックになっているのであれば、そもそもAOFの純粋な運用を見直すべきだ。
Redis 6以降で導入されたハイブリッド永続化(`aof-use-rdb-preamble yes`)を活用せよ。
これにより、AOFファイルのベース部分にバイナリのRDBスナップショットを使用し、リライト時のデータ構造の肥大化とバッファの負荷を劇的に軽減することが可能となる。
redis.conf
aof-use-rdb-preamble yes
—
5. 結びにかえて
AOFリライトバッファは、Redisという極限まで最適化されたインメモリデータベースの「泥臭い整合性維持の努力」が凝縮された領域である。
表面的な設定値を調整するだけのエセエンジニアリングは今日で終わりにしよう。内部のメモリ構造、`fork()` との同期、OSカーネルとの相互作用を完璧に理解した上でデザインされたアーキテクチャこそが、真に堅牢な分散システムの土台となる。
コードを信じるな、メカニズムを理解せよ。それが、限界なきシステムを構築唯一の道である。
コメント