【実務・中級編】 メモリ削除ポリシー (Eviction Policies) – Redis

Redisメモリ削除の深層:OOM Killerを回避し、キャッシュを「武器」にするアーキテクチャ設計

おい、設計レビューを始めるぞ。

お前たちが組んだキャッシュレイヤー、綺麗に動いているように見えて、実は地雷原の上を歩いていることに気づいているか?
「Redisはメモリが一杯になったら勝手に古いデータを消してくれるんでしょ?」――そんな甘い認識で本番を迎えたシステムが、ピーク時に突然のOOM (Out of Memory) Killerに刈り取られ、データベースを直撃して全システムが雪崩式に沈没していく。私はこの惨劇を幾度となく目撃してきた。

Redisは単なる「キーバリューの箱」ではない。メモリ管理の挙動を完全に支配して初めて、堅牢なインフラストラクチャと言える。
今回は、Redisのメモリ削除ポリシー(Eviction Policies)の深層と、実務の現場で絶対に踏んではいけない地雷、そしてプロフェッショナルが採用する設計パターンを叩き込む。

—

1. Redisメモリ管理の根本原則:何が起きているのか?

まず、大前提を叩き込んでおく。
Redisは、設定された上限メモリ(`maxmemory`)に達した瞬間から、新規書き込みに対してエラーを返すか、既存データを削除してスペースを空けるかの二者択一を迫られる。この挙動を制御するのが `maxmemory-policy` だ。

だが、勘違いしてはならない。TTL(有効期限)の有無 と 削除ポリシー は別軸で動いている。

  • 受動的削除(Lazy Deletion): クライアントがキーにアクセスした際、すでにTTLが切れていればその場で消す。
  • 能動的削除(Active Expiration): Redisはバックグラウンドで定期的に(デフォルトでは秒間10回)ランダムなキーをサンプリングし、期限切れのものを削ぎ落としている。
  • メモリ削除(Eviction): `maxmemory` に達した際、TTLの有無に関わらず ポリシーに従って強制的にキーを追い出す。

ここを混同していると、「なぜかメモリが解放されない」「意図しないデータが消えた」という謎のバグに悩まされることになる。

—

2. 削除ポリシーの全貌と「選んではいけない」地雷

Redis 4.0以降、選択できるポリシーは多岐にわたる。だが、実務でまともに使えるものは限られている。それぞれの本質を暴いていこう。

① `noeviction` (デフォルト)

  • 挙動: メモリ上限に達すると、書き込み系コマンド(`SET`, `HSET` 等)に対してエラーを返す。読み取りや削除は可能。
  • 判定: 絶対に使ってはならない(キャッシュ用途の場合)。
  • 理由: キャッシュサーバーとして動かしているつもりが、突然書き込み拒否を返し始め、アプリケーション層で例外が連鎖する。マスター・スプリットや障害の元凶だ。ただし、厳密なカウンターやデータストアとしてRedisを使う場合は例外的に選択肢に入る。

② `allkeys-lru` vs `volatile-lru`

  • LRU (Least Recently Used): 直近で最も使われていない(アクセスされていない)ものを消す。
  • `allkeys-lru`: データベース内のすべてのキーを対象にLRUで削除。
  • `volatile-lru`: TTLが設定されているキーの中だけで LRUで削除。
  • プロの知見: 原則として、キャッシュとして割り切るなら `allkeys-lru` 一択 だ。すべてのキーにTTLを貼り忘れるポカミスをするジュニアエンジニアがいるが、`allkeys-lru` なら「アクセス頻度の低いデータ」から自動的に追い出してくれるため、セーフティネットとして完璧に機能する。

③ `allkeys-lfu` vs `volatile-lfu` (Redis 4.0〜)

  • LFU (Least Frequently Used): アクセス頻度が最も低いものを消す。
  • プロの知見: 「最近一度だけアクセスされたが、普段は全く使われない巨大なマスターデータ」のような偏りがあるシステムでは、LRUよりも圧倒的に LFU が優れている。LRUだと、一括バッチ処理で全キーをスキャンした瞬間に、本来残すべきホットなキャッシュが追い出される事故(Cache Pollution)が起きる。LFUはこの弱点を完璧に克服している。

—

3. Redisの「近似LRU/LFU」という割り切り

ここで、Redisのアーキテクチャにおける「強烈な割り切り」を共有しておこう。
Redisは、真のLRU/LFUアルゴリズムを実装していない。

厳密なLRUを実装しようとすると、すべてのアクセス順に二重連結リストを組み替える必要があり、マルチスレッド環境(あるいはシングルスレッドのイベントループ)において莫大なメモリオーバヘッドとCPUコスト(ロック競合やポインタ追跡のキャッシュミス)が発生する。

そのため、Redisは 「近似アルゴリズム(Approximated LRU/LFU)」 を採用している。

redis.conf でのサンプル設定
maxmemory 4gb
maxmemory-policy allkeys-lfu
maxmemory-samples 10

  • `maxmemory-samples` の意味: メモリ上限に達した際、Redisは全キーから総当たりで最も古いものを探すのではなく、指定したサンプル数(デフォルトは5、高精度にしたいなら10程度)だけランダムにキーを抽出し、その中で最も条件に合うものを1つ削除する。
  • 設計上の注意: サンプル数を上げすぎるとCPU使用率が跳ね上がる。逆に小さすぎると削除精度が落ちる。デフォルトの `5` は非常に絶妙なバランスだが、ミリ秒単位のレイテンシシビアな環境では、この削除処理(EvictionによるCPUブロック)がレイテンシスパイクの原因になることを覚えておけ。

—

4. 実務で直面するトラブルと「堅牢な設計パターン」

コードレビューをしていると、「とりあえず `maxmemory` だけ設定しました」というPRが後を絶たない。甘い。以下の3点を必ず設計に組み込め。

パターンA:OOMを防ぐための安全なメモリマージン設計

物理メモリが16GBのインスタンスがあるとする。OSやRedis自体のプロセスオーバーヘッド、スワップ、fork時のCopy-on-Write(RDBスナップショット取得時)のメモリ急増を考慮しろ。

  • 鉄則: `maxmemory` は 物理メモリの 60% 〜 70% に設定しろ。
  • RDBの `bgsave` や AOFの重水素化(rewrite)の際、Redisはメモリ消費量が一時的に最大2倍(COW領域)に膨れ上がる。これを忘れてメモリを90%まで割り当てているシステムは、バックアップの瞬間に確実に死ぬ。

パターンB:キャッシュ雪崩(Stampede)を防ぐTTLジッター

`volatile-lru` やTTLベースの削除を行っているシステムで、何万件ものキーに「全く同じTTL(例: 3600秒)」を設定するな。
一斉に期限が切れた瞬間、RedisのCPU使用率が100%に張り付き、バックエンドのDBへリクエストが雪崩を打って押し寄せる。

悪い例:一斉に失効する
redis.set(key, value, ex=3600)

良い例:TTLにランダムな揺らぎ(ジッター)を混ぜて失効タイミングを分散させる
import random

jitter = random.randint(-300, 300) (-5分〜+5分の揺らぎ)
redis.set(key, value, ex=3600 + jitter)

この一手間だけで、プロダクション障害の確率を桁違いに下げられる。

—

5. チーフアーキテクトからの最終通告

メモリ削除ポリシーの選定は、そのシステムのビジネスロジックの特性をコード以上に雄弁に物語る。

1. キャッシュとして割り切るなら: `allkeys-lfu` または `allkeys-lru` を使え。TTLの設定漏れをRedisのポリシーに肩代わりさせるのだ。
2. マスターデータやセッションなど、絶対に消えては困るデータが混ざるなら: Redisをキャッシュとして使うな。永続化ストアとして扱うか、キーのプレフィックスでインスタンスを完全に分離しろ。
3. モニタリングを怠るな: `INFO stats` の `evicted_keys` メトリックを常に監視しろ。ここが意図せず急増している場合、それは「メモリサイジングの失敗」または「悪質なキャッシュ・ポルーション(不要なデータがホットデータを追い出している現象)」のシグナルだ。

Redisを使いこなせるか否かで、バックエンドエンジニアとしての格が決まる。
お前たちの次の設計書、この知見が反映されていることを期待している。さっさとコードを修正しろ。

コメント

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