RedisのRDBスナップショットを極める:実務で踏み抜かないためのアーキテクチャ設計
こんにちは。テクニカルリードの私だ。
今日のコードレビューで、「Redisの永続化?とりあえずデフォルト設定のRDBで動かしてます」というプルリクエストを見かけた。
……待て。その設計、プロダクション環境で障害が起きたときに全データが吹き飛ぶか、あるいはピークタイムにレイテンシが跳ね上がってサービスが死ぬ未来しか見えないぞ。
Redisは「速い」からといって、脳死でデフォルト設定のまま本番稼働させていいおもちゃではない。特にRDB(Redis Database)スナップショットは、そのメカニズムとOSの挙動を理解していないと、容赦なくシステムを牙をむいて襲う。
今回は、RDBスナップショットの深層から、実務で絶対に守るべき堅牢な設計パターンまで、妥協なく解説しよう。
—
1. RDBスナップショットのメカニズム:内部で何が起きているか?
RDBは、指定した時間と変更回数の条件(例: `900秒以内に1キー以上変更`)に基づき、メモリ上のデータセット全体をコンパクトなバイナリファイル(`dump.rdb`)としてディスクに吐き出す方式だ。
バックアップや災害復旧(DR)には非常に向いている。なぜなら、単一のバイナリファイルをS3などに転送するだけでスナップショットが完了するからだ。
しかし、ここでエンジニアとして知っておくべき極めて重要な事実がある。
「Redisはシングルスレッドで動いているのに、どうやってメモリをブロックせずにディスクに書き込んでいるのか?」 という点だ。
`fork()` と Copy-on-Write (CoW) の実態
RedisはRDBを保存する際、内部で `fork()` システムコールを呼び出し、子プロセス(Child Process)を生成する。
[親プロセス (Redis Main Thread)] ──(fork)──> [子プロセス (RDB Saver)]
│ │
▼ ▼
メモリを読み書きし続ける スナップショット時点の
(Copy-on-Writeで安全に分離) メモリをディスクへダンプ
ここでLinuxの Copy-on-Write (CoW) メカニズムが働く。
`fork()` 直後、親と子は物理メモリ上のページを共有している。親プロセス(メインスレッド)が既存のキーを更新(WRITE)しようとした瞬間、OSはそのメモリページを別領域にコピーし、親だけがそのコピーをいじる。子プロセスは `fork()` した瞬間のスナップショットデータをそのまま維持し続け、安全にディスクへ書き出す。
一見、完璧な仕組みに見える。だが、ここに実務における最大の罠がある。
—
2. 実務で踏み抜く「3大アンチパターン」
このCoWと `fork()` の仕組みを理解していれば、RDB運用における地雷を踏まずに済む。よくある失敗を挙げていこう。
アンチパターン①:メモリの倍量オーバーフロー(OOM Killerの餌食)
もし、あなたがRedisに 30GB のデータを格納しており、空きメモリが 10GB しかない状態でRDBの保存が走ったらどうなるか?
`fork()` した瞬間はメモリ共有なので問題ないように見えるが、その直後に大量の書き込みが発生し、全体の半分(15GB分)のメモリ領域が書き換えられたとする。
CoWによって、OSは物理メモリ上に元の15GB + 新しい15GB = 計45GB のメモリを確保しようとする。当然、物理メモリ(RAM)とスワップ領域が枯渇し、Linuxの OOM (Out of Memory) Killer が発動し、最もメモリを食っている Redisの親プロセスが無慈悲に強制終了 される。
> 教訓: Redisが使用する最大メモリ(`maxmemory`)は、物理RAMの最大でも50%〜60%に抑え、OSのオーバーコミット設定(`vm.overcommit_memory = 1`)を確実に確認しておけ。
アンチパターン②:巨大な `fork()` レイテンシの発生
`fork()` はプロセスを複製する際、メモリの「ページテーブル」を複製する。
データ量が数十GB、あるいは数100GBに達している巨大なRedisインスタンスでは、このページテーブルの複製だけで数秒から数十秒のブロッキングが発生することがある。
シングルスレッドのRedisで数秒間メインスレッドが完全に止まるということは、その間、外部からのすべてのリクエストがタイムアウトすることと同義だ。
アンチパターン③:I/Oネックによるレイテンシスパイク
子プロセスが `dump.rdb` をOSのページキャッシュ経由でディスクに書き出す際、ディスクI/Oが飽和することがある。特に遅いHDDや、IOPS制限のあるクラウドストレージ(EBS等)を使っている場合、fsyncのタイミングで親プロセスのRedisがブロックされ、レイテンシが急激に跳ね上がる。
—
3. 堅牢なRDB設計パターンと推奨設定
では、プロダクション環境でRDBを安全に運用するにはどう設定すべきか。`redis.conf` の現実解を示そう。
① スナップショット条件のチューニング
デフォルトのままだと高頻度すぎてI/O負荷になり、かといって緩すぎるとデータ損失のウィンドウが広がる。ビジネス要件に応じた適切なバランスをとる。
900秒 (15分) 内に少なくとも 1 キー変更された場合
save 900 1
300秒 (5分) 内に少なくとも 10 キー変更された場合
save 300 10
60秒 (1分) 内に少なくとも 10000 キー変更された場合(高負荷時の保護)
save 60 10000
RDB保存失敗時に書き込みを停止するか (デフォルトはyesだが、可用性重視ならnoの検討も)
stop-writes-on-bgsave-error yes
圧縮を有効にする (CPUコストはわずかだがディスク容量とI/O時間を大幅削減)
rdbcompression yes
チェックサムの有効化 (データの破損検知)
rdbchecksum yes
ファイル名と保存先
dbfilename dump.rdb
dir /var/lib/redis
② バックアップ専用インスタンス(レプリカ)の活用
これが最もプロフェッショナルな設計だ。
マスター(書き込み・読み込み用)のRedisではRDBスナップショットを無効化(`save “”`)し、そこからデータを受け取るレプリカ(Slave)側でRDBを有効化してバックアップを取得する。
これによって、マスターインスタンスは `fork()` によるレイテンシスパイクやディスクI/Oの負荷から完全に解放され、ミリ秒単位の超低レイテンシを維持できる。
— マスター側の設定 (redis.conf) —
save “”
rdbcompression no
— レプリカ側の設定 (redis.conf) —
save 900 1
save 300 10
save 60 10000
マスターから同期されたデータを安全にディスクに落とす
—
4. 運用上の注意点とモニタリング
RDB運用において、監視すべきメトリクスは明確だ。以下の項目を必ずPrometheusなどで監視しろ。
1. `rdb_last_bgsave_status`: 最後のRDB保存が成功したか(`1`なら成功、`0`なら失敗)。ここが落ちたら即座にアラートを飛ばす。
2. `rdb_last_bgsave_time_sec`: RDBの保存にかかった時間。これが長すぎる場合、データ量が多すぎるか、ディスクI/Oがボトルネックになっている。
3. `latest_fork_usec`: `fork()` にかかったマイクロ秒単位の時間。これが数千〜数万(数秒)を超えている場合、インスタンスの肥大化を意味するため、シャーディング(Redis Cluster等への移行)を検討するタイミングだ。
現在のRDB関連のステータスをCLIで確認するコマンド
redis-cli info persistence
出力例(抜粋):
Persistence
loading:0
rdb_changes_since_last_save:1240
rdb_bgsave_in_progress:0
rdb_last_save_time:1700000000
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:2
rdb_current_bgsave_time_sec:-1
rdb_last_cow_size:10485760 # CoWで消費されたメモリ量 (バイト)
—
チーフアーキテクトからの総括
RDBスナップショットは、シンプルゆえに強力だが、その裏でOSのメモリ管理(`fork`とCoW)と密接に結びついている。
- メモリは常に余裕を持たせろ(物理RAMの半分以下が鉄則)。
- 高負荷なマスターで直接RDBを回すな。レプリカにオフロードしろ。
- `fork()` のコストとレイテンシを常に監視せよ。
この3つを守るだけで、障害耐性は劇的に跳ね上がる。
インフラの挙動をコードレベル、OSレベルで理解し、破綻のない堅牢なアーキテクチャを構築してほしい。次回のレビューでは、もっと洗練された設計を見せてもらうことを期待している。
コメント