【テクニカル・上級編】 save設定ディレクティブ – Redis

Redisの心臓部を暴く:`save`ディレクティブと写実的永続化の限界

データベースエンジニアとして数千台規模のRedisクラスタを渡り歩いてきた私だが、未だにプロダクション環境の初期設定で首を傾げたくなるコードに出会うことがある。その最たるものが、`redis.conf`に鎮座する`save`ディレクティブのデフォルト値の放置、あるいは場当たり的なチューニングだ。

save 900 1
save 300 10
save 60 10000

このお馴染みの設定は、一見して無難な自動スナップショット(RDB)のトリガーに見える。しかし、大規模かつ高スループットなワークロードを扱うアーキテクトにとって、このディレクティブの挙動を根本から理解していないことは、時限爆弾を抱えて寝るようなものだ。

今回は、RedisのRDB永続化メカニズム、とりわけ`save`ディレクティブがOSのメモリ管理層とどのようにインタラクトし、システム全体にどのような代償をもたらすのか。その極限の低レイヤ知見を紐解いていこう。

—

1. `save`ディレクティブの正体:定期実行ではなく「カウンタの閾値」

まず前提として正確な共通認識を持っておこう。`save ` は、指定された秒数「ごとに」定期実行されるタイマー割り込みではない。

Redisのメインイベントループ内では、ミリ秒単位のティックごとに dirty カウンタ(前回のセーブ以降にキーが変更された数)とタイムスタンプが評価されている。

/ server.c のバックグラウンドタイマー処理の概念的イメージ /
if (server.dirty >= changes &&
now – server.lastsave > seconds) {
rdbSaveBackground(backgroundSaveFileName, &attr);
}

つまり、この条件式を満たした瞬間、Redisは非同期でのRDBファイル生成、すなわち `fork()` システムコールを発動する。ここからが、パフォーマンス・エンジニアリングの真髄、そして悪夢の始まりだ。

—

2. `fork()` の残酷な現実:Copy-on-Writeとメモリ倍増の罠

`save`条件がトリガーされると、Redisは自身の子プロセスを生成するために `fork()` を呼び出す。シングルスレッドでインメモリデータを高速に処理するRedisの美学が、ここでOSの仮想記憶管理機構(MMU)と激しく衝突する。

Page Tableの複製とRSSの肥大化

Linuxの `fork()` は、プロセスの子を瞬時に生成するように設計されているが、それは「アドレス空間のページテーブルがコピーされる」という意味であって、物理メモリ(RAM)が瞬時に2倍になるわけではない。

しかし、ここに Copy-on-Write (CoW) の罠がある。

1. `fork()` 直後: 親(Redisメイン)と子(RDBセーバー)は、同一の物理メモリページを共有する。この時点ではメモリ使用量は実質増えない。
2. 書き込みの発生: クライアントからの書き込み(`SET`, `HSET` 等)により、親プロセスが既存のメモリページの内容を書き換えようとした瞬間、OSのMMUはページフォルト(Copy-on-Writeフォルト)を発生させる。
3. ページの複製: OSは該当ページを別の物理メモリ領域に複製し、親プロセスはその複製先に対して書き込みを行う。

もし、あなたが100GBの巨大なデータセットを保持しており、RDBのバックグラウンドセーブ中に高頻度な書き込み(例: 50,000 QPS)が発生した場合どうなるか?

セーブ処理が完了するまでの間に、大量のメモリページがCoWによって複製され、最悪の場合、OSの物理メモリおよびスワップ領域を食いつぶし、Linux KernelのOOM KillerによってRedisのメインプロセスが無慈悲に刈り取られることになる。

—

3. `save`設定のアンチパターンと限界

多くの開発者は「データを失いたくない」という恐怖から、以下のようなアグレッシブな設定を入れたがる。

絶対にやってはいけないアンチパターンの例
save 60 1

60秒以内に1回でも変更があればRDBを吐く。これを高負荷なプロダクション環境でやるとどうなるか。
毎分のように `fork()` が走る。`fork()` 自体は高速だが、大規模ヒープを持つプロセスでの `fork()` は、ページテーブルのコピーそのものに数ミリ秒から数十ミリ秒のCPU時間を要する。

この間、Redisのメインスレッドは完全にブロックされる(Latencies Spikes)。
数万QPSを捌くシステムにおいて、数十ミリ秒の完全停止は、上流アプリケーション層でのコネクションプールの枯渇、タイムアウト、そして連鎖的な障害(Cascading Failure)を引き起こす十分な引き金となる。

—

4. チーフアーキテクトが推奨する「極限の設計指針」

では、我々プロフェッショナルはこの`save`ディレクティブとどう向き合うべきか。結論から言えば、高スループットシステムにおいて、RDBの自動保存(`save`ディレクティブ)は原則として「オフ」にするのが、モダンな大規模アーキテクチャの標準プラクティスである。

① `save`を無効化し、明示的制御へ移行する

`redis.conf` からすべての `save` 行をコメントアウト、または明示的に空にする。

自動スナップショットを完全に無効化
save “”

データ消失のリスクはどう担保するのか?
答えは AOF(Append Only File)とレプリケーションの組み合わせ、あるいはクラウドネイティブなインフラ層での冗長化である。

② AOF(Append Only File)の適切なチューニング

RDBが「ある時点のスナップショット」であるのに対し、AOFはすべての書き込みコマンドをログとして記録する。

appendonly yes
appendfsync everysec

`appendfsync everysec` は、1秒に1回ディスクへの同期を行う。これにより、最悪でも過去1秒間のデータ損失(RPO = 1秒)に抑えつつ、`fork()` によるメモリ倍増リスクを回避できる。
(※ AOFリライト時にも `fork()` は発生するが、その頻度や制御はRDBの自動セーブよりもハンドリングしやすい)

③ 手動・運用のセーブ

どうしてもRDBのスナップショットが必要な場合は、cronや監視エージェントから `BGSAVE` コマンドをシステム負荷の低い時間帯(ピークオフタイム)に明示的に叩くべきだ。

ピークオフタイムに安全にBGSAVEを実行する例
redis-cli -a “${REDIS_PASSWORD}” BGSAVE

この際、`LASTSAVE` コマンドや `INFO persistence` の `rdb_bgsave_in_progress` を監視し、多重実行を防ぐロジックを必ず組み込むこと。

INFO persistence の出力例(一部抜粋)
rdb_changes_since_last_save:12450
rdb_bgsave_in_progress:0
rdb_last_save_time:1700000000
rdb_last_bgsave_status:ok

—

結び:ツールの限界を知る者がシステムを制す

`save 900 1` という一行は、Redisの入門書を開けば必ず載っている。しかし、その裏側で何が起きているのか――`fork()` のメカニズム、仮想記憶のCoW、メインスレッドのレイテンシへの影響を理解していない者が、安易にこの数値をいじった結果、本番障害を引き起こしてきた歴史を私は数多く見てきた。

道具の便利さに甘えるな。
データベースエンジニアたるもの、設定ファイルに記述された一つのディレクティブが、Linuxカーネルのメモリ管理機構にどのようなパルスを伝えるのかを常に脳裏に描きながら、インフラストラクチャをデザインしなくてはならない。

コメント

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