【テクニカル・上級編】 永続化のベストプラクティス – Redis

Redis永続化の限界と極野: RDBとAOFの深淵を暴く

世の中の多くのチュートリアルはこう言う。「データ損失を防ぎたければRDBとAOFを両方有効にせよ」と。
だが、大規模トラフィックを扱うインフラストラクチャの現場において、この「お題目」を思考停止で適用することがいかに危険か、君たちは気づいているだろうか?

数千万リクエスト/秒を捌くインメモリデータストアにおいて、永続化(Persistence)とは、パフォーマンスという名の神殿に爆弾を仕掛けるようなものだ。IOをブロックし、レイテンシのスパイクを生み、最悪の場合はOOM Killerによるプロセスの強制終了を誘発する。

今回は、Redisの永続化メカニズム(RDBとAOF)の内部構造を極限まで剥ぎ取り、レイテンシとデータ耐久性(Durability)のトレードオフを支配するための「チーフアーキテクトの知見」を授けよう。

—

1. RDB (Redis Database) の真実:Copy-on-Writeの暗部

RDBは、特定の時点におけるメモリ上の全データセットをコンパクトなバイナリファイル(`dump.rdb`)としてスナップショット保存するメカニズムだ。生成には `SAVE` と `BGSAVE` が存在するが、本番環境で `SAVE` を叩く人間は即座にクビになるべきだ。なぜなら、単一スレッドであるRedisのメインイベントループを完全にブロックするからだ。

我々が使うべきは `BGSAVE` だが、その内部動作の裏側で何が起きているかを理解しているか?

ForkのコストとCopy-on-Write (CoW) の罠

`BGSAVE` が実行されると、Redisは `fork()` を呼び出し、子プロセスを生成する。この子プロセスがディスクへの書き込みを担当する。

[Redis Main Process] (メモリ上に 32GB のデータ)
│
├── fork() ──> [Child Process] (親のページテーブルを共有)
│ │
│ └── 32GBのデータをディスクへ順次フラッシュ (RDB)
│
[Write発生] ──> OSのCopy-on-Write発動 ──> 物理メモリ消費が急増 (最悪 64GB)

ここで致命的な問題が発生する。Linuxの `fork()` は、ページテーブルをコピーするだけで、最初は親と子で物理メモリを共有する。しかし、親プロセス側でデータの更新(Write)が発生した瞬間、OSの Copy-on-Write (CoW) メカニズムが働き、該当メモリページの複製が作られる。

1. メモリの倍増リスク: 激しく書き込み(`HSET`, `LPUSH`など)が行われている最中に `BGSAVE` が走ると、書き込みのたびにメモリ消費量が増加し、最終的に物理メモリを食いつぶしてスワップアウト、あるいはカーネルのOOM KillerによってRedisが爆散する。
2. `fork()` 自体のレイテンシ: メモリ使用量が数十年規模(例えば50GB)に達している巨大なインスタンスでは、`fork()` 時のページテーブルコピー(特にTransparent Huge Pagesが有効な場合)だけで、メインスレッドが数百ミリ秒〜数秒間完全にフリーズする。これはレイテンシSLAを致命的に破壊する。

【知見】
巨大なRedisインスタンス(>20GB)において、素朴なRDBスナップショットは悪手である。インスタンスを適切にシャーディング(分散)するか、後述するAOFとの組み合わせを慎重に設計しなければならない。

—

2. AOF (Append Only File) の真実:fsyncの呪縛

AOFは、Redisが受け取ったすべての書き込みコマンドを、独自のプロトコル形式でファイル末尾に追記していく方式だ。RDBに比べてデータ損失のリスクが低い。しかし、ここには「ディスクI/O」という、インメモリデータベースにとって最大の天敵が潜んでいる。

AOFの肝は、OSのページキャッシュからディスク(物理ストレージ)へデータをいかに同期(`fsync`)させるか、という点にある。`appendfsync` の設定値には3つの選択肢がある。

| 設定値 | 挙動 | 耐久性 | パフォーマンス |
| :— | :— | :— | :— |
| `no` | OS任せ(通常30秒ごと) | 低い | 最高 |
| `always` | コマンド毎に同期書き込み | 極めて高い | 絶望的(I/Oバウンド) |
| `everysec` | 1秒ごとにバックグラウンドスレッドで同期 | 実用レベル(最大1秒のロス) | 高い |

`appendfsync everysec` の裏側で起きていること

実務において選択肢は実質的に `everysec` 一択だ。しかし、このモードにも落とし穴がある。

バックグラウンドのI/Oスレッドが `fsync` を実行する際、もしストレージ(NVMe SSD等)の書き込みキューが飽和していると、メインスレッドの `write()` システムコール自体がブロックされる。
Redisはシングルスレッドで動いているため、`write()` がブロックされた瞬間、すべてのクライアントリクエストが停止する。

redis.conf の重要チューニング
no-appendfsync-on-rewrite yes

この設定(AOFリライト中に `fsync` を抑制する)は必須だ。AOFリライト(後述のBGREWRITEAOF)が走っている最中に重い `fsync` が重なると、レイテンシスパイクが跳ね上がる。これを防ぐため、リライト中のディスクI/O競合を意図的に回避する防衛策である。

—

3. AOFリライトのメカニズム:差分マージの魔術

AOFはコマンドの履歴をそのまま溜め込むため、何もしなければファイルサイズが無限に肥大化する。
例えば、`SET key 1` のあとに `SET key 2` を100回実行した場合、AOFには101行のコマンドが残る。しかし、最終的に必要なのは `SET key 2` の1行だけだ。

ここで発動するのが `BGREWRITEAOF` である。

1. Redisは現在のメモリ上のデータを、RDBと同様にコンパクトなコマンド群に変換し、新しいAOFファイルを裏で書き始める。
2. この書き込み中、新しく入ってきた書き込みコマンドは、「AOFリライトバッファ」 に蓄積される。
3. 新しいAOFファイルの書き込みが完了すると、バッファに溜まった差分コマンドが追記され、アトミックにファイルが置き換えられる。

しかし、このリライト処理もまた、`fork()` を伴うため、RDBと同様にメモリ消費とCPU負荷のスパイクを引き起こす。

—

4. チーフアーキテクトが導く:究極の永続化ベストプラクティス

では、データ損失リスク、可用性、そしてレイテンシのバランスを極限まで最適化するにはどうすればよいか。私の現場での最終結論を提示する。

結論1: 本番キャッシュ層(Cache Tier)では「永続化を完全無効化」せよ

Redisを「単なる高速なキャッシュ(Session StoreやFull-Page Cacheなど、DBから再構築可能なデータ)」として使っているならば、RDBもAOFも両方オフ(Disable)にしろ。

キャッシュとしての完全無効化
save “”
appendonly no

これにより、`fork()` によるレイテンシスパイク、CoWによるメモリ枯渇、ディスクI/Oによるブロックを完全に排除できる。可用性は、Redis Clusterのレプリケーション(Master-Replica)とSentinelによるフェイルオーバーで担保する。

結論2: 永続データ層(Primary Data Store)では「RDB + AOFハイブリッド」を適用せよ

Redisをプライマリのデータストア(リーダーボード、リアルタイムカウンターなど)として使う場合は、両者を併用しつつ、以下の極限チューニングを施す。

1. RDBはバックアップのベースとして低頻度で回す
save 900 1
save 300 10
save 60 10000

2. AOFは everysec で耐久性を担保
appendonly yes
appendfsync everysec
no-appendfsync-on-rewrite yes

3. AOFリライトの閾値を調整し、無駄なI/Oを防ぐ
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

結論3: OSレイヤーのインフラチューニングを怠るな

Redisの永続化性能は、Linuxカーネルの設定に依存している。これを怠ると、いかにRedis側の設定を詰めても意味がない。

1. Transparent Huge Pages (THP) の無効化
THPが有効なままだと、`fork()` 時のCoWの粒度が大きくなり(2MB単位)、レイテンシが桁違いに悪化する。

echo never > /sys/kernel/mm/transparent_hugepage/enabled

2. Overcommit Memoryの設定
`fork()` 時にメモリが足りなくて失敗する悲劇を防ぐため、カーネルのメモリオーバーコミットを適切に設定する。

sysctl -w vm.overcommit_memory=1

—

終わりに

Redisの永続化を制することは、OSのメモリ管理とストレージI/Oの限界を制することと同義である。
「安全だから」という理由だけで設定をデフォルトから変更せず放置することは、エンジニアとしての怠慢でしかない。

システムの特性を見極め、メモリ、CPU、ディスクの挙動を完全に支配した上で、最適な永続化戦略をコードと設定に落とし込め。それこそが、真のアーキテクチャのあり方である。

コメント

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