Redis再起動の深層:クラッシュからのメモリ再構築と永続化の真実
データベースの稼働中、最も冷徹な瞬間は「突然のプロセス停止」から復旧するその一瞬だ。
OOM Killerの慈悲なき介入、カーネルパニック、あるいは電源断。Redisがシグナルを受け取って(あるいは受け取る間もなく)堕ちた後、再起動のプロセスで何が起きているか。
「RDBとAOF、どちらが優先されるのか?」
この問いに即答できるだけでは、アーキテクトとは呼べない。問題は、いかにしてメモリ空間が再構築され、その裏でOSのメモリアロケータとカーネルがどう暴れているかだ。
今回は、Redisがクラッシュから生還する瞬間のメカニズムを、ソースコードの深層レベルで解剖する。
—
1. 起動時の永続化ファイル選択:RDB vs AOFの内部ロジック
Redisインスタンスが起動した際、最初に行われる最も重要な仕事は「どの永続化ファイルをロードするか」の決定だ。ここで神話レベルの勘違いをしているエンジニアが多い。「AOFが有効なら、常にAOFが優先される」というのは、半分正しくて半分間違っている。
実際のサーバー初期化関数 `server.c` の `initServer()` から呼ばれる `loadData()` のロジックを見てみよう。
/ 起動時のデータロードの核心部(擬似コードおよびソースベースの解説) /
void loadData(void) {
long long start = ustime();
// 1. AOFが有効か?
if (server.aof_state == AOF_ON) {
// AOFファイルが存在するかチェック
if (fileExist(server.aof_filename)) {
// AOFからのロードを試みる
if (loadAppendOnlyFile(server.aof_filename) == C_OK) {
serverLog(LL_NOTICE, “DB loaded from append only file: %.3f seconds”, (float)(ustime()-start)/1000000);
return;
} else {
serverLog(LL_WARNING, “Error trying to load the AOF, data lost!”);
}
}
}
// 2. AOFが無効、またはAOFのロードに失敗した場合、RDBにフォールバック
if (fileExist(server.rdb_filename)) {
rdbSaveInfo rsi = RDB_SAVE_INFO_INIT;
if (rdbLoad(server.rdb_filename, &rsi, RDBFLAGS_NONE) == C_OK) {
serverLog(LL_NOTICE, “DB loaded from disk: %.3f seconds”, (float)(ustime()-start)/1000000);
return;
}
}
}
アーキテクトの視点:AOFが存在するが空の場合の罠
もしAOFファイルが有効でありながら、ファイルサイズが0バイトであった場合(クラッシュ時にディスクI/Oがフラッシュされずにプロセスが落ちた等)、Redisは「AOFファイルあり」と判断してAOFのロードを試みる。
ここで古いバージョンや不適切な設定の場合、パースエラーを起こして起動に失敗するか、意図しないデータロスを招く。最新のRedisでは `aof-load-truncated` パラメータによって末尾の破損したコマンドを切り捨てる挙動が制御されるが、根本的な解決にはならない。「AOFの有効化=RDBが無視される」という事実を直視せよ。
—
2. メモリ再構築の地獄:RDB/AOFロード時の内部メカニズム
ファイルからデータを読み込み、メモリ上にキーバ空间を復元するプロセスは、単なる「パースと挿入」ではない。ここにはOSレベルのボトルネックが潜んでいる。
AOFリプレイの圧倒的コスト
AOF(Append Only File)は、発行されたコマンドのテキスト(またはRESPプロトコル)のシーケンスだ。これをロードするということは、「Redisのシングルスレッドイベントループを回さずに、全てのコマンドを内部関数としてもう一度実行し直す」ことを意味する。
1. レキサーとパーサーの負荷: 文字列をパースし、引数の配列(`robj argv`)を動的にアロケートする。
2. コマンドディスパッチ: `lookupCommand()` でコマンドテーブルを引く。
3. キーの再作成とメモリ断片化(Fragmentation):
これが最大の悪夢だ。数千万件のキーを持つAOFをリプレイすると、jemalloc(またはlibc malloc)に対し、ミリ秒単位で数百万回の小規模なアロケーション要求が飛ぶ。結果として、ヒープ領域は深刻な外部断片化(External Fragmentation)を引き起こし、実際のデータ量よりもはるかに多くの物理メモリをカーネルから貪り食うことになる。
RDBロードの効率性
一方、RDBはバイナリのスナップショットである。メモリ上のデータ構造をそのままシリアライズしているため、ロード時はパースのオーバーヘッドがほぼゼロだ。
RDBのロード中、Redisは `rdbLoadRio()` を使い、ストリームから直接ハッシュテーブルのバケット、スキップリスト、ジップリストなどを復元していく。メモリレイアウトがそのままディスクから復元されるため、AOFのようなコマンド実行による無駄なアロケーションが発生しにくい。
—
3. クラッシュ復旧時のCPU・メモリ・I/Oのボトルネック
障害復旧(Disaster Recovery)の現場で最も恐ろしいのは、再起動直後の「スラッシング(Thrashing)」だ。
OOM Killerの再来
数GB規模のAOF/RDBをロードする場合、Redisのプロセスサイズは一時的に急増する。
- RDBの場合: コピーオンライト(CoW)は起きていないが、一気にデータをメモリ上に展開するため、空きメモリが逼迫する。
- AOFの場合: コマンドのパースツリーや一時的なオブジェクト生成により、ピーク時のメモリ使用量は元のデータセットサイズを容易に超越する。
もしホストの可用メモリがギリギリの状態で設計されていた場合、「クラッシュしたRedisを復旧させようとした瞬間、メモリ不足で再びOOM Killerに屠殺される」というデッドロックの無限ループに突入する。
対策:アーキテクトが設定すべきマージン
私はコンサルティングの現場で、必ず以下の原則を徹底させている。
1. `maxmemory` とOSの物理メモリの乖離:
OSのオーバーコミット設定(`vm.overcommit_memory = 1`)はもちろん必須だが、それ以上に Redisのデータサイズの最低1.5倍〜2倍の物理メモリ(またはスワップレスな余裕)をノードに割り当てなければならない。ロード時のバースト耐性のためだ。
2. AOF rewriteの常時最適化:
AOFが肥大化したままクラッシュすると、復旧に数十分を要する。`auto-aof-rewrite-percentage` を適切に設定し、常にAOFを最小限の状態に保つことは、可用性(Availability)そのものである。
—
4. 異常系シナリオ:破損した永続化ファイルの強制生還術
もし、RDBもAOFも破損(Corruption)しており、Redisが起動時にパニックを起こして終了した場合どうするか。「バックアップからの復元」が正解だが、バックアップすら古く、どうしても直近のデータを取り出したい修羅場を想定せよ。
AOFの外科手術(Manual Truncation)
AOFの末尾が中途半端に書き込まれてクラッシュした場合、Redisは起動時にエラーを吐いて終了する。
この場合、以下の手順で「外科手術」を行う。
1. 破損したAOFファイルのバックアップを必ず取得
cp appendonly.aof appendonly.aof.bak
2. redis-check-aof ツールを使用して破損箇所を修復(末尾の切り捨て)
redis-check-aof –fix appendonly.aof
実行すると、ツール対話的に不正なバイト列の手前までを切り捨てるか確認される。
本番の自動復旧スクリプトに組み込む場合は環境変数やフラグに注意せよ。
RDBの強制ロード
RDBがCRC64チェックサムエラー等で弾かれた場合、設定ファイル(`redis.conf`)で一時的にチェックサムを無効化するか、`redis-check-rdb` で修復を試みる。
RDBのチェッカーを実行
redis-check-rdb dump.rdb
しかし、プロのアーキテクトとして言わせてもらえば、「壊れたバイナリを人力でハッキングして復元を試みる時間は、レプリケーションのセカンダリからデータを救出する時間、あるいはあきらめて最新のスナップショットから復元する時間よりも常に高コストである」。
—
5. 結びにかえて:障害復旧とは「設計」である
Redisの障害復旧プロセスにおいて、運に頼る余地はない。
RDBが優先されるのか、AOFが優先されるのか、その背後でアロケータがどうメモリを断片化させるのか。これらを完全に把握している者だけが、深夜の障害アラートに対し、冷徹かつ正確なコマンド一発でシステムを蘇生させることができる。
「動いたからよし」とする素人細工のインフラは、次のクラッシュで確実に崩壊する。
メモリの内部構造を見据え、最悪のロードシナリオを逆算してリソースを設計せよ。それこそが、真のレジリエンスを宿したアーキテクチャである。
コメント