【テクニカル・上級編】 AOFリライト予約状態 – Redis

AOFリライト予約状態の深層:Redis単一スレッドの裏で蠢く非同期プロセスの支配構造

Redisを「ただのインメモリKVS」と捉えているうちは、ジュニアクラスの域を出ない。ミリ秒単位のレイテンシと数千万QPSをハンドリングする極限の環境において、Redisの心臓部である単一スレッド(Event Loop)モデルと、OSのシステムコール、そしてバックグラウンドワーカーの相互作用を理解していなければ、いつか必ず致命的な障害に足元をすくわれる。

今回は、Redisの永続化戦略の要であるAOF(Append-Only File)の中でも、特にプロフェッショナルが注視すべき「AOFリライト予約状態(AOF rewrite scheduled / in-progress)」の内部メカニズムにメスを入れる。

リファレンスには載っていない、ソースコードの深層とメモリモデルの真実を紐解こう。

—

1. AOFリライトとは何か:概念の破壊と再構築

AOFは、Redisに対するすべての書き込みコマンドをログとして逐次追記していく永続化方式である。しかし、この方式には致命的な弱点がある。サーバーの再起動、あるいはクラッシュからの復旧時、膨大なログを最初から最後まで再実行(Replay)するため、起動に絶望的な時間がかかるのだ。

さらに、ログは「不要になった古いキーの更新履歴」も含めて無限に肥大化する。

ここで登場するのが AOFリライト(`BGREWRITEAOF`) である。
これは、現在のメモリ上に存在するデータ構造をスキャンし、「そのデータを再構築するために必要な最小限のコマンド群」へとAOFファイルを圧縮・再生成するプロセスである。

しかし、このリライト処理は、単にファイルを書き換えるだけの単純な仕事ではない。ここには、Redisの非同期処理とOSの仮想記憶方式が織りなす、極めて洗練されたアーキテクチャが隠されている。

—

2. 内部状態の解剖:なぜ「予約」が必要なのか

Redisのサーバー構造体(`server` struct、`redis.h`に定義)には、AOFリライトの状態を管理するためのフラグや変数が存在する。

AOFリライトが実行される際、Redisは単一スレッドのイベントループをブロックしてはならない。そのため、基本的には `fork(2)` を呼び出して子プロセス(Child Process)を生成し、その子プロセスにデータセットのシリアライズを委譲する。

だが、ここで問題が生じる。
すでにRDBのスナップショット生成(`BGSAVE`)が実行中である場合、さらにもう一つの `fork(2)` を同時に走らせるべきか?

OSレベルで考えてほしい。数10GBを抱えたプロセスが `fork` を実行すると、たとえCopy-on-Write(CoW)があるとはいえ、ページテーブル(Page Table)の複製コストやメモリ消費の急増(最悪の場合、メモリオーバーコミットによるOOM Killerの標的)を引き起こす。

そのため、Redisは排他制御を行う。
ここで登場するのが、今回のテーマである「AOFリライト予約状態」だ。

状態遷移のメカニズム

1. `BGSAVE` が実行中の場合、`BGREWRITEAOF` がリクエストされても、即座に子プロセスをフォークすることはできない。
2. その代わり、Redisは 「AOFリライトの予約(Scheduled)」 フラグを立てる。
3. 現在実行中の `BGSAVE` が終了した際、イベントループのバックグラウンド処理(`backgroundSaveDoneHandler`)において、「あ、そういえばAOFリライトが予約されていたな」と検知され、自動的に遅延実行される。

この「予約状態」の有無は、`INFO persistence` コマンドの出力にある以下のフィールドで観測できる。

観測例:INFO persistence の出力断片
aof_enabled:1
aof_rewrite_in_progress:0 # 現在リライト実行中か? (0 or 1)
aof_rewrite_scheduled:1 # リライトがスケジュール(予約)されているか? (0 or 1)
aof_last_rewrite_time_sec:14
aof_current_rewrite_time_sec:-1

この `aof_rewrite_scheduled: 1` が立っている瞬間、Redisの内部では「次のバックグラウンドタスク枠が空き次第、即座に重いフォーク処理が発動する」という緊迫した状態が維持されている。

—

3. ソースコードから読み解く調停ロジック

実際のコード(`rewrite.c` および `server.c` 周辺)の精神構造を見てみよう。C言語のミニマルかつ冷徹な実装だ。

// 疑似コードおよび実際のRedis内部ロジックの概念
int rewriteAppendOnlyFileBackground(void) {
// すでにBGSAVEまたはAOFリライトが走っている場合
if (server.child_pid != -1) {
// 現在何らかの子プロセスが生存しているため、即座にフォークできない
// AOFリライトの予約フラグを立ててリターンする
server.aof_rewrite_scheduled = 1;
updateDictResizePolicy();
return C_OK;
}

// 子プロセスが存在しない場合、直ちにfork()を実行しリライトを開始する
if (serverCairoFork() == 0) {
// — 子プロセス側の処理 —
// メモリ上のデータを走査し、一時的なAOFファイルに吐き出す
rewriteAppendOnlyFile(aof_child_filename);
exitFromChild(0);
} else {
// — 親プロセス(メインスレッド)側の処理 —
server.aof_rewrite_scheduled = 0; // 予約をクリア
server.aof_stat_fork_time = ustime();
// … 統計情報や状態変数の更新 …
}
return C_OK;
}

この調停ロジックがあるおかげで、オペレーターがうっかり `BGSAVE` の最中に `BGREWRITEAOF` を叩いたとしても、Redisが二重フォークによるリソース枯渇(Thundering HerdならぬFork Storm)を起こすのを未然に防いでいる。

—

4. 運用上の罠:AOFリライト予約状態がもたらす「見えない遅延」

熟練エンジニアであれば、この「予約状態」が引き起こす実務上のリスクに気づくだろう。

1. ディスクI/O飽和時のゾンビ化

`BGSAVE` が何らかの理由(ディスクの帯域枯渇、EBSのiops制限など)で長引き、子プロセスが終了しない場合、`aof_rewrite_scheduled = 1` の状態が延々と維持される。
この間、メモリへの書き込み(Write/Update)が大量に行われていると、親プロセスのメモリマップが汚され、いざ `BGSAVE` が終わってからAOFリライトのための `fork()` が走った瞬間、Copy-on-Writeによるページコピーの爆発が発生する。結果として、メインスレッドが数秒間完全にフリーズ(Latency Spike)する。

2. コマンド遅延の連鎖

`INFO` を監視していれば、`aof_delayed_fsync` や `aof_rewrite_scheduled` の変化に気づけるが、安価な監視ツールではこの「予約状態」の滞留を見落としがちだ。
「なぜか定期的にレイテンシが跳ねるが、CPU使用率は低い」という現象に直面した時、その原因の多くは、このバックグラウンドプロセスの排他制御とOSのページテーブル管理の摩擦にある。

—

5. チーフアーキテクトからの提言:極限環境でのベストプラクティス

1. RDBスナップショットとAOFリライトの同時多発を避けよ
もしディスクI/Oやメモリ帯域に余裕がないシステムであれば、RDB(`save` ディレクティブ)を無効化し、AOFリライトのみ、あるいはその逆の設計に割り切るべきだ。無駄な重複処理や「予約状態への依存」を排除せよ。

2. `auto-aof-rewrite-percentage` のチューニング
デフォルトの `100`(前回のサイズから2倍に膨らんだらリライト)は、小規模なインスタンスでは妥当だが、数10GB〜100GBを超える巨大インスタンスでは大惨事を引き起こす。メモリ使用量の絶対値(例: `auto-aof-rewrite-min-size 64gb` など)を厳密に定義し、意図しないタイミングでのフォークを防げ。

3. 監視の解像度を上げろ
単に「Redisが生きているか」を見るのではなく、Prometheus等で `redis_aof_rewrite_scheduled` や `redis_fork_time` のメトリクスをスクレイピングし、「予約状態が何分間継続しているか」をアラートのトリガーに組み込め。

—

結び

AOFリライト予約状態。それはRedisという洗練されたエンジンの内部で、限られたシステム資源(CPU、メモリ帯域、ディスクI/O)を極限まで効率よく調停するために張り巡らされた、数少ない「渋滞のチケット」に他ならない。

これを「ただのフラグ」として片付けるか、「システム全体の負荷のバロメーター」として読み解くか。それこそが、ただのオペレーターと、真のインフラストラクチャー・アーキテクトを分かつ境界線である。

コメント

タイトルとURLをコピーしました