伝説のチーフアーキテクトが説く、`SAVE`コマンドの罠:なぜ本番環境でそのボタンを押してはいけないのか
設計レビューの場で、もしジュニアエンジニアが「バックアップを取るために、cronで定期的またはデプロイ前に `SAVE` を叩く仕組みを作りました」とドヤ顔で報告してきたら、君はどう反応するだろうか。
「よくやった」と褒めるなら、君のチームのRedisはいつか必ず、本番障害の濁流に飲み込まれることだろう。
私はこれまで数々の大規模分散システムの修羅場をくぐり抜けてきた。その中で、Redisの単一スレッドモデルの急所を突いた障害をいくつも目撃してきた。その最たる原因の一つが、この `SAVE` コマンドの誤用 だ。
今回は、Redisの永続化メカニズムの核心に迫り、なぜ `SAVE` がプロダクション環境で「禁忌」とされているのか、そして真に堅牢なアーキテクチャはどうあるべきなのかを、容赦なくロジカルに伝授しよう。
—
1. 原理:なぜ `SAVE` はシステムを沈黙させるのか
まず、Redisの根本思想を思い出してほしい。Redisのコアロジックはシングルスレッドで稼働している。つまり、クライアントからのリクエスト(コマンド)は、イベントループ上で1つずつ直列に処理される。
ここで `SAVE` コマンドが実行されたとき、何が起きるか?
[Client A] —> (GET key) \
[Client B] —> (HGETALL hash) —> [ Redis Single-Thread Event Loop ] —> 【SAVE 実行!】 —> (完全ブロック:他のすべてが停止)
[Client C] —> (LPUSH list) /
`SAVE` は、同期的に(Synchronously) 現在のメモリ上の全データセットをディスク(RDBファイル:`dump.rdb`)に書き出す。
OSのファイルI/Oが完了するまでの間、Redisのシングルスレッドは完全にブロックされる。
もし君のRedisが持つメモリサイズが 50GB だとしたらどうなるか?
現代の高速なNVMe SSDであっても、50GBのデータをシリアライズしてディスクにフラッシュするには数秒〜十数秒の時間がかかる。その間、Redisは何百万という他のクライアントからのリクエストを一切処理できなくなる。
- APIサーバーからのセッション確認がすべてタイムアウト。
- ヘルスチェック(Liveness Probe)が失敗し、Kubernetes等のオーケストレーターがPodを強制再起動(Liveness Loop地獄)。
- 上流のアプリケーション群が雪崩式にコネクションプールを枯渇させ、全システムが沈黙。
これが、本番環境で `SAVE` を叩いてはいけない物理的な理由だ。
—
2. 比較:`SAVE` vs `BGSAVE`(非同期の救世主)
では、どうやって安全にスナップショットを取ればいいのか?
その答えが `BGSAVE`(Background Save) コマンドだ。
| 特性 | `SAVE` | `BGSAVE` |
| :— | :— | :— |
| 実行方式 | 同期(Blocking) | 非同期(Non-blocking / Fork) |
| イベントループ | 停止する(ブロック) | 停止しない(即座に制御を返す) |
| 内部メカニズム | メインスレッド自身が書き込み | `fork()` による子プロセスの生成 |
| 本番適性 | 嚴禁(NG) | 推奨(標準) |
`BGSAVE` の裏側:Copy-on-Writeの魔術
`BGSAVE` を実行すると、Redisは親プロセスから `fork()` を呼び出し、子プロセスを生成する。
この子プロセスが、親プロセスのメモリイメージをそのままディスクに書き出す。
ここでOSの Copy-on-Write (CoW) メカニズムが機能する。
`fork()` 直後は、親と子は同じ物理メモリ領域を参照している。書き込みが発生しない限り、メモリが二重に消費されることはない。子プロセスがディスクへの書き出しを行っている最中に、親プロセス(メインスレッド)に書き込み(`SET` や `DEL` など)が入ると、その変更されたメモリページだけが新しく複製される。
[Redis Parent Process (Main Thread)] ──(fork)──> [Redis Child Process (RDB Writer)]
│ │
├─ 書き込み発生! ──> メモリページを複製して更新 │
│ └──> 静的なスナップショットを
└────────────────────────────────────────────────────────> dump.rdb へ書き出し
これにより、メインスレッドをブロックすることなく、バックグラウンドで安全にスナップショットが作成できるのだ。
—
3. 堅牢な設計パターン:本番運用のベストプラクティス
では、実務の現場ではどのように永続化を設計すべきか。設計レビューで私が見るべきポイントを挙げる。
① `SAVE` はコンフィグから「無効化」または厳重管理する
設定ファイル (`redis.conf`) では、デフォルトで自動スナップショットの条件が定義されている。
例: 900秒(15分)以内に1回以上の変更があれば BGSAVE
save 900 1
例: 300秒(5分)以内に10回以上の変更があれば BGSAVE
save 300 10
例: 60秒以内に10000回以上の変更があれば BGSAVE
save 60 10000
もし自動スナップショットを完全に無効化したい(あるいはインメモリキャッシュとして完全に割り切り、クラッシュ時はデータロストを許容する)場合は、明示的に空の `save` ディレクティブを指定する。
自動スナップショットを完全に無効化する場合
save “”
間違っても、ここに `SAVE` が絡むようなカスタムスクリプトを組み込んではならない。
② `fork()` のコスト(Latnecy Spikes)を見積もる
`BGSAVE` はブロックしないとはいえ、`fork()` 自体にコストがかかる。
巨大なデータセット(例えば60GB超)を持つRedisインスタンスで `fork()` を行うと、ページテーブルのコピーに数百ミリ秒から数秒のCPU停止(プロセス停止)が発生することがある。
チーフアーキテクトからの設計指針:
- メモリ肥大化の防止: Redisのインスタンスは適切にサイジングし、1インスタンスあたりせいぜい 10GB〜20GB 程度に収める(シャスディングを検討せよ)。
- Linuxカーネルのチューニング: `vm.overcommit_memory = 1` を確実に設定し、`fork()` 時のメモリ不足エラー(Out Of Memory)を防ぐ。
- Transparent Huge Pages (THP) の無効化: THPが有効なままだと、`fork()` 時のCopy-on-Writeのオーバーヘッドが劇的に跳ね上がり、レイテンシがスパイクする。必ず無効化(`never`)しておけ。
THPが無効化されているか確認するコマンド
cat /sys/kernel/mm/transparent_hugepage/enabled
出力例: always madvise [never] ([never]になっていればOK)
③ 災害復旧(DR)とAOFの併用
RDB(`SAVE` / `BGSAVE`)は、あくまで「ある時点のスナップショット」であり、直前のデータロスト(RDB保存間隔のズレによる消失)を防ぐことはできない。
堅牢性が求められるシステムでは、AOF (Append Only File) を有効にするか、Redis Replication / Redis Cluster による冗長化を組み合わせるのが定石だ。
—
4. まとめ:コードレビューで指摘すべき一言
もし、次のスプリントで誰かが `SAVE` コマンドを使ったコードや設定を書いてきたら、こう言って差し戻してほしい。
> 「この `SAVE` を `BGSAVE` に変更するか、そもそもなぜ同期ブロッキングが必要なのかアーキテクチャから説明してくれ。メモリサイズと `fork()` のコストを計算したのかい?」
技術の本質を知る者だけが、システムをダウンタイムから守ることができる。
表面的なAPIの使い方に終始せず、その背後で動くOSのメモリ管理やシングルスレッドの挙動まで見通すエンジニアであれ。君たちの手で、強靭なシステムを構築し続けることを期待している。
コメント