Redisの腹の内:BGSAVEの裏側と、大規模システムを落とさないための設計哲学
こんにちは。テックリードの私だ。
今日のコードレビュー、あるいはアーキテクチャ設計レビューで気になる点があったので、この話をさせてもらう。
「とりあえずデータが消えると困るから、`BGSAVE` を定期的に叩いておけば安心だよね」
――おいおい、待ってくれ。その設計、本番環境で本当に耐えられるか?
Redisの公式ドキュメントやネットの入門記事には「`BGSAVE` はバックグラウンドでスナップショットを作るから、メインスレッドをブロックしなくて安全」と書いてある。それは事実だ。だが、「安全」の定義を履き違えると、高負荷なプロダクション環境で容赦なくシステム全体を沈めることになる。
今日は、Redisの永続化メカニズムの心臓部である `BGSAVE` について、単なるコマンドの使い方ではなく、OSの挙動、メモリ管理、そして我々エンジニアが本番を守るためにどう設計すべきかという「極限の知見」を叩き込む。
—
1. `BGSAVE` のメカニズム:なぜメインスレッドはブロックされないのか?
まず、おさらいだ。`SAVE` コマンドが実行されると、Redisのメインスレッド(シングルスレッド)が完全に同期的にディスクへの書き込みを行う。つまり、その間、一切のリクエスト(GET/SETなど)が処理できなくなる。データ量が数十GBに膨れ上がったモダンなシステムでこれをやったら、クライアントからのタイムアウトの嵐でアラートが鳴り響く。
そこで登場するのが `BGSAVE` だ。
これの内部挙動を正確に言えるだろうか?
1. `fork()` の呼び出し:
Redisの親プロセスは、自身のメモリ空間をコピー(厳密には仮想メモリのページテーブルを複製)するために、OSに対して `fork()` システムコールを発行する。
2. 子プロセスの誕生:
フォークされた子プロセスは、`fork()` が実行された「その瞬間」のメモリのスナップショットを保持したまま、ディスクへのRDBファイル書き込み(シリアライズ)を開始する。
3. メインスレッドの解放:
親プロセス(メインスレッド)は、フォークが完了した瞬間から、通常通りクライアントからのリクエストをノンストップで処理し続ける。
「完璧じゃないか」と思うかもしれない。しかし、この `fork()` と Copy-on-Write(CoW)の仕組みこそが、実務で牙をむくポイントなのだ。
—
2. 現場の罠:OSレベルの「見えないコスト」
`fork()` は高速だと言われる。しかし、それは「メモリが少ない時」の話だ。
Redisが保有するメモリが 32GB あったとしよう。`fork()` を呼ぶとき、OSは何をしているのか?
物理メモリそのものをコピーするわけではないが、32GB分のメモリページテーブルの複製と、それを管理するためのメタデータのコピーが発生する。さらに厄介なのが、Copy-on-Write(CoW)だ。
- 子プロセスがRDBを書き出している最中(数秒〜数十秒)に、メインスレッドが既存のキーを更新(WRITE)したとする。
- OSは、その変更されたメモリ領域(ページ)が子プロセスに影響を与えないよう、裏側でメモリの物理コピー(Duplication)を行う。
- 結果として、書き込み負荷(Write-heavy)が高いシステムで `BGSAVE` を走らせると、メモリ使用量が一時的に跳ね上がり、最悪の場合、OSのメモリが枯渇(OOM Killerの餌食)するか、`fork()` 自体の処理に数百ミリ秒〜数秒のブロック(レイテンポスパイク)が発生する。
レビューで「なんでこのバッチ時間帯にレイテンシが跳ね上がってるの?」と聞かれたら、大抵犯人はこれだ。
—
3. 実践:堅牢な設計とパラメータチューニング
では、我々はこの強力かつ危険な `BGSAVE` とどう向き合えばいいのか?
設計レイヤーで講じるべき対策をいくつか伝授しよう。
① `save` ディレクティブの魔力を見直す
`redis.conf` にはデフォルトで以下のような記述がある。
900秒(15分)以内に1回以上の変更があればBGSAVE
save 900 1
300秒(5分)以内に10回以上の変更があればBGSAVE
save 300 10
60秒以内に10000回以上の変更があればBGSAVE
save 60 10000
書き込みが頻発するキャッシュ層やセッションストアにおいて、このデフォルト設定は時限爆弾になり得る。無意味な高頻度のスナップショットは、I/O帯域を圧迫し、前述のCoWコストを無限に支払い続けることになる。
【設計指針】
- データを完全に失ってもDBから再構築できる「純粋なキャッシュ」であれば、RDBの永続化自体をオフ(`save “”`)にする勇気を持て。
- 永続性が必要な場合でも、システムのRTO(目標復旧時間)とRPO(目標復旧時点)を定義し、ビジネス要件に合わせた適切な閾値に調整する。
② `stop-writes-on-bgsave-error` の死守
`redis.conf` に以下の設定がある。
stop-writes-on-bgsave-error yes
もし、ディスク容量の枯渇や権限エラーなどで `BGSAVE` が失敗した場合、Redisが書き込み(SET等)を拒否するかどうかの設定だ。
これを `no` にしている現場をたまに見かけるが、即座に `yes` に変えろ。
`no` にすると、永続化が失敗していることに気づかないまま運用し続け、いざという時のリストアで「データが空っぽでした」という絶望的な状況を生む。Fail-Fastの原則に従い、異常検知したら即座に書き込みを止めるのがプロのインフラ設計だ。
③ モニタリングの徹底
`BGSAVE` が裏でどう動いているか、メトリクスを取っていないなら論外だ。
`INFO persistence` コマンドを定期的に叩き、以下の項目を監視しろ。
実行結果例 (INFO persistence の一部)
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:4
rdb_current_bgsave_time_sec:-1
rdb_last_save_time:1680000000
- `rdb_last_bgsave_status` が `err` になっていないか?
- `rdb_last_bgsave_time_sec` が徐々に延びていないか?(メモリ肥大化のサイン)
- `rdb_current_bgsave_time_sec` が常に値を持っている場合、スナップショット生成がボトルネックになっている。
—
4. コードレビューの視点:こう書け、こう設計しろ
もし君のチームのジュニアエンジニアが、アプリケーションコード側から頻繁に手動で `BGSAVE` を叩くような仕組みを実装しようとしていたら、こう言って差し戻してほしい。
> 「アプリ層から非同期だからといって軽率に `BGSAVE` をトリガーするな。Redisのメモリサイズ、OSの `overcommit_memory` 設定、ディスクI/Oの帯域を考慮しているか? 永続化のタイミングはインフラ・SREの管轄としてredis.confのポリシーで制御し、アプリ層からは原則触るな」
まとめ
- `BGSAVE` はメインスレッドをブロックしない「魔法のコマンド」ではない。裏では `fork()` と Copy-on-Write の物理的なコストが発生している。
- 大規模データ(数十年GB以上)を扱う環境では、メモリ増加に伴う `fork()` のレイテンシススパイクとOOMのリスクを常に念頭に置け。
- 設定ファイルを盲信するな。システムの特性(キャッシュか永続ストアか)に合わせて `save` 条件をチューニングし、監視を怠るな。
アーキテクチャの本質は、「仕組みの裏で何が起きているか」を正確に把握し、最悪のシナリオをコントロールすることにある。
今日のレビューから、この視点を忘れずにコードと向き合ってくれ。期待している。
コメント