Redisメモリ管理の真実:期限切れキーと淘汰ポリシーの裏側で何が起きているのか
テックリードの私だ。今日のコードレビューで、あるジュニアエンジニアが書いたRedisの設計書を見て、思わず赤ペンを入れた。
> 「`EXPIRE`を設定しているから、メモリは自動で綺麗に保たれるはずです」
……甘い。実に甘いと言わざるを得ない。
Redisのメモリ管理は、そんなお伽話のように美しく動いてはいない。
なぜ、十分にメモリの空きがあるはずのクラスタでOOM(Out of Memory)が発生するのか? なぜ、特定のキーにアクセスが集中した瞬間にレイテンシが跳ね上がるのか?
その答えはすべて、「期限切れキーの削除メカニズム」と「`maxmemory-policy`の相互作用」の深淵にある。
今日は、Redisのメモリ管理の裏側で何が起きているのか、実務で絶対に知っておくべき極限の知見を授けよう。
—
1. 期限切れキーの削除は「魔法」ではない
まず大前提として、RedisはCPUコアを無駄に浪費してまで、ミリ秒単位の正確さで期限切れキーをバックグラウンドで消し去ったりはしない。もしそんなことをすれば、シングルスレッドのイベントループがブロックされ、Redisのアイデンティティである超高速性が死んでしまう。
Redisにおける期限切れキーの削除は、主に以下の2つのアプローチの組み合わせで成り立っている。
① パッシブ削除(Lazy Deletion)
クライアントがそのキーにアクセスしようとした瞬間、Redisは「おっと、このキーはもう期限切れだな」と検知し、その場で削除して`nil`を返す。
- メリット: 無駄なCPUサイクルフックを使わない。
- デメリット: 二度とアクセスされない期限切れキーは、永遠にメモリ上に残り続ける。
② アクティブ削除(Active Deletion / 期限切れサンプリング)
Redisは、バックグラウンドで定期的に(デフォルトでは1秒間に10回=10Hz)サンプリングを行い、期限切れキーをパージしている。
このアクティブ削除のアルゴリズムは以下の通りだ:
1. 期限切れ情報を持つキーの空間から、ランダムに20個のキーをサンプリングする。
2. その中で期限切れになっているものをすべて削除する。
3. 期限切れのキーの割合が 25%を超えていた場合、即座にステップ1に戻る。
[アクティブ削除のイメージ]
DBからランダムに20キーをピックアップ
↓
[ 期限切れ判定 ]
├─ 期限切れ → 削除
└─ 生存 → そのまま
↓
期限切れが 25% 超過?
├─ YES ──> すぐに次の20キーをサンプリング(ループ)
└─ NO ──> 終了(次の周期まで待機)
ここでエンジニアとして察しがつくはずだ。
「書き込みや読み込みが激しく、かつ寿命の短いキーが大量に生成されるシステムでは、アクティブ削除のCPU負荷が跳ね上がり、レイテンシスパイクを引き起こす」ということに。
—
2. メモリが限界を迎えたとき:`maxmemory-policy` の残酷な現実
`maxmemory`を設定し、メモリ上限に達したとき、Redisは暴徒と化す。どのキーを犠牲にしてメモリを空けるかを決めるのが `maxmemory-policy` だ。
実務で選定すべきポリシーは、大別して以下の3つに絞られる。
A. LRU系 (`volatile-lru`, `allkeys-lru`)
- 挙動: 最近最も使われていない(Least Recently Used)キーを淘汰する。
- 実務での評価: 王道。アクセス局所性(Locality of Reference)があるワークロードでは最も安全。ただし、RedisのLRUは完全なLRUではなく近似LRU(ランダムサンプリングによる近似)である点に注意せよ。(`maxmemory-samples`で精度調整可能だがCPUを食う)
B. LFU系 (`volatile-lfu`, `allkeys-lfu`) —— ★現代の最適解
- 挙動: 使用頻度が最も低い(Least Frequently Used)キーを淘汰する。
- 実務での評価: 迷ったらこれを選べ。 従来のLRUの弱点は、「1時間前にバーストアクセスされて以来、全く使われていないキー」がメモリに居座り続けることだった。LFUはアクセス頻度(カウンター)を見るため、真にホットなデータを見極めて保護できる。
C. volatile-ttl
- 挙動: 期限(TTL)が最も近いものを淘汰する。
- 実務での評価: 「とりあえず古い順に消す」という発想で安易に選んではならない。TTLが短いからといって、それが「重要でないデータ」とは限らないからだ。
—
3. 【最重要】「期限切れ削除」と「メモリ淘汰」の致命的な相互作用
ここからが本題だ。多くのエンジニアがハマる罠がここにある。
メモリ上限に達したRedisインスタンスにおいて、「まだ期限が切れていないが、メモリ淘汰ポリシーの対象となるキー」と「すでに期限が切れているのにまだパッシブ削除されていないキー」が混在するカオス状態が生まれる。
ここで `allkeys-lru` や `allkeys-lfu` が動いた場合、何が起きるか?
Redisは、「まだ期限が残っている重要なセッションデータ」を、期限切れ間近のゴミデータよりも先に容赦なくパージすることがある。
なぜなら、LRU/LFUアルゴリズムは「TTLがいつ切れるか」など一切考慮せず、純粋にアクセスの履歴だけで冷酷に淘汰対象を選ぶからだ。
対策:設計時のアーキテクチャパターン
この悲劇を防ぐため、我々テクニカルリードは設計時に以下のパターンを厳守させている。
パターン1: データベースの分離(物理的または論理的)
キャッシュ(TTLあり・揮発性)と、マスターデータや永続化セッション(TTLなし、あるいは淘汰されたくないデータ)を同じRedisインスタンス、同じDB番号に同居させるな。
特に `allkeys-` 系ポリシーを使う場合、そのインスタンス全体が「いつ消えてもいいキャッシュ専用」でなければならない。
パターン2: `volatile-` ポリシーの適切な選択とTTLの活用
淘汰させたくない永続データと、消えてもいいキャッシュがどうしても混ざる場合は、`volatile-lru` や `volatile-lfu` を採用し、残したいデータには一切 `EXPIRE` を設定しないこと。これにより、`volatile-`系ポリシーはTTLを持つキー(=キャッシュ)の中からのみ淘汰を行うため、永続データを守ることができる。
—
4. 現場で使える!堅牢なメモリ監視とチューニングの作法
最後に、プロダクション環境で事故を起こさないための実践的な知見をコードとコマンドで授ける。
① 設定の確認と動的変更
本番稼働中にポリシーを変更する必要が生じた際、わざわざRedisを再起動する必要はない。`CONFIG SET` で即座に反映できる。
現在のメモリ淘汰ポリシーを確認
127.0.0.1:6379> CONFIG GET maxmemory-policy
1) “maxmemory-policy”
2) “noeviction” # デフォルト。メモリ上限に達すると書き込みエラー(OOM)を返す最悪の初期値
LFUポリシーへ動的変更(再起動不要)
127.0.0.1:6379> CONFIG SET maxmemory-policy allkeys-lfu
OK
メモリ上限を 2GB に設定
127.0.0.1:6379> CONFIG SET maxmemory 2147483648
OK
※ `noeviction` のまま運用しているサービスがあれば、今すぐ夜間メンテナンスの計画を立てて変更しなさい。メモリ溢れで書き込み断絶を起こすのはプロの仕事ではない。
② メモリ使用量のインスペクション
定期的に `INFO memory` を叩き、以下のメトリクスを監視せよ。
127.0.0.1:6379> INFO memory
Memory
used_memory:1073741824 # Redisが消費している実際のメモリバイト数(約1GB)
used_memory_human:1.00G
maxmemory:2147483648 # 制限値(2GB)
maxmemory_policy:allkeys-lfu # 適用中のポリシー
evicted_keys:14209 # ⚠️ここが重要:メモリ上限を理由に淘汰された累計キー数
expired_keys:589201 # TTLによって正常に期限切れ削除された累計キー数
- `evicted_keys` が右肩上がりで爆増している場合: メモリサイジングが間違っているか、TTLの設計が破綻している。アプリケーション側のキャッシュ戦略を根本から見直す必要がある。
—
結びにかえて
Redisは、メモリという限られた資源の上で高速性を極限まで高めたシビアなミドルウェアだ。
「期限を設定しているから大丈夫」「メモリが余っているから大丈夫」という甘い前提は、トラフィックが急増した本番環境で確実に牙をむく。
- キャッシュと永続データの棲み分けを徹底すること。
- ワークロードに合わせた `maxmemory-policy`(迷ったら `allkeys-lfu`)を明示的に設定すること。
- 期限切れ削除のコストとメモリ淘汰のメカニズムをコードレベルで理解しておくこと。
この3点を抑えて初めて、あなたはRedisを真に手懐けたと言える。
さあ、今すぐ自社システムのRedis設定ファイルを開き、`maxmemory-policy` が `noeviction` のままになっていないか確認し給え。
コメント