RedisのOOM(Out Of Memory)はなぜ厄介なのか?:本番障害を防ぐためのメモリ管理と堅牢なクライアント設計
アーキテクトの佐藤だ。コードレビューや設計レビューで、こんなコードを見かけたことはないだろうか?
レビューでよく見かける「祈りながら書かれた」コード
try:
redis_client.set(f”user:session:{user_id}”, session_data)
except Exception as e:
# 「まあ、キャッシュだし失敗してもいっか」
logger.warning(f”Redis write failed: {e}”)
一見、例外処理が書いてあるので安全に見えるかもしれない。しかし、このコードが稼働するシステムは、Redisが `maxmemory` に到達した瞬間から静かに、だが確実に崩壊へのカウントダウンを始める。
今回は、Redisのメモリ管理の核心、特に `maxmemory` 満杯時の挙動(OOM)と、それに対するアプリ側の正しい迎撃方法について、実務で使える極限の知見を授けよう。
—
1. RedisがOOMに直面したとき、内部で何が起きているのか?
Redisはインメモリデータベースである以上、物理メモリ(あるいはOSの仮想メモリ)の制約を受ける。`redis.conf` の `maxmemory` ディレクティブを設定し忘れるか、甘い見積もりで運用していると、いつかは必ずその瞬間が訪れる。
メモリが上限に達したとき、Redisは設定された `maxmemory-policy`(逐出ポリシー) に従って既存のキーを削除し、新しい書き込みのための領域を捻出しようと試みる。
しかし、問題はここからだ。
- `noeviction`(デフォルト、または書き込み系の一部ポリシー)の場合、あるいはすべてのキーに有効期限(TTL)が設定されておらず、evictionが機能しない状態では、Redisは新しいメモリを確保できない。
この状態のとき、Redisに書き込みコマンド(`SET`, `HSET`, `LPUSH`, `ZADD` など)を投げると、容赦なく以下のエラーが返される。
(error) OOM command not allowed when used memory > ‘maxmemory’
チーフアーキテクトの視点:OOMの何が「厄介」なのか?
読者の中には「エラーを返してくれるなら、DBのディスクフルよりマシじゃないか」と思う者もいるだろう。甘い。
RedisのOOMが厄介なのは、「読み取り(`GET`, `HGET` など)は正常に動作し続ける」という点だ。そのため、死活監視(`PING`)や参照系のヘルスチェックは緑(正常)を返しつつ、ユーザーのログイン(セッション書き込み)、カートへの追加、決済処理といった核心的な書き込みがすべて失敗するという、システムにとって最もタチの悪い「半死半生の状態」を作り出す。
—
2. 実務で遭遇するアンチパターン:なぜ障害が拡大するのか?
OOM発生時、アプリケーション側で適切にハンドリングされていないと、障害は雪だるま式に拡大する。
アンチパターンA:エラーの握りつぶしとリトライストーム
先ほどのPythonの例のように、OOMエラーを単なる「一時的な通信エラー」と誤認し、指数バックオフもなしに即座にリトライを繰り返すコードがあるとどうなるか?
満杯のRedisに対して、何百ものワーカースレッドが一斉に `SET` コマンドを叩きつける。Redisはシングルスレッドで動いているため、この無意味なOOMコマンドの処理だけでCPUコアが張り付き、読み取りクエリのレイテンシすらも悪化(あるいはタイムアウト)していく。結果として、Redis単体のメモリ枯渇が、アプリケーション全体の完全なダウンタイムへと昇華される。
アンチパターンB:LUAスクリプトやトランザクションでの爆発
OOMの最中に複雑な `EVAL`(Luaスクリプト)や `MULTI/EXEC` が実行されると、スクリプト内の最初の書き込みでOOMエラーが発生し、トランザクション全体がアボートする。依存関係のある複数のキー更新が中途半端に失敗し、データ整合性が破壊されるリスクがある。
—
3. 堅牢な設計パターン:OOMを前提としたシステム構築
では、我々テクニカルリードはどう設計すべきか。アプローチは大きく分けて「Redis側の防衛」と「クライアント側の迎撃」の2つがある。
A. Redis側の防衛:適切なポリシーとアラートの自動化
1. `maxmemory-policy` の見直し
純粋なキャッシュとして使っているなら、`volatile-lru` や `allkeys-lru`、あるいはより高度な `allkeys-lfu` を選定すべきだ。マスターデータやセッションなど「消えては困るデータ」を扱う場合は `noeviction` が正しい選択肢だが、その代わりメモリ監視が必須となる。
2. メモリ使用率 80% での早期検知(SLAの死守)
OOMになってから慌てるのは素人だ。Prometheus + Alertmanager等で `used_memory / maxmemory` のメトリクスを監視し、80%を超えた時点でPagerDuty等のアラートを飛ばすフローを構築すること。
B. クライアント側の迎撃:OOM例外の厳密なハンドリング
アプリケーションコードでは、RedisからのOOMエラーを他の例外(接続断やタイムアウト)と明確に分離し、専用のサーキットブレーカーやフォールバック機構を働かせる必要がある。
以下に、堅牢なエラーハンドリングの実装例を示す(Python + `redis-py` の例)。
import redis
from redis.exceptions import ResponseError
class RedisOOMError(Exception):
“””RedisがOOM状態であることを示すカスタム例外”””
pass
def safe_redis_write(client: redis.Redis, key: str, value: str, ex: int = 3600):
try:
# 書き込み実行
client.set(key, value, ex=ex)
except ResponseError as e:
# エラーメッセージに ‘OOM’ が含まれているか厳密にチェック
if “OOM command not allowed” in str(e):
# 1. ログに重大アラートとして記録
logger.critical(f”[CRITICAL] Redis OOM detected during write to {key}: {e}”)
# 2. サーキットブレーカーをオープンするなどの防衛策を発動
circuit_breaker.trip()
# 3. 上位レイヤーへ専用例外として伝播
raise RedisOOMError(“Redis memory exhausted. Writes are blocked.”) from e
else:
# その他のRedisエラー(構文エラーなど)
logger.error(f”Redis command error: {e}”)
raise
except (redis.ConnectionError, redis.TimeoutError) as e:
# ネットワーク起因のエラー(リトライ対象)
logger.warning(f”Redis network transient error: {e}”)
raise
設計上のキモ:OOM発生時のフォールバック戦略
セッションストアでOOMが発生した場合、書き込みを完全に失敗させる(ユーザーを強制ログアウトさせる)のか、あるいは「今回はセッションの有効期限延長をスキップし、読み取り専用モードで継続させる」のか。
システムの要件定義の段階で、「Redisが書き込みを拒否した瞬間、アプリのどの機能がデグレしても許されるか」をプロダクトオーナーと合意しておくこと。これが真のアーキテクチャ設計だ。
—
4. チーフアーキテクトからの最終提言
RedisのOOMは、単なる「容量不足」ではない。それは「インメモリDBとしての設計の甘さを告発する、システムからの最後通牒」である。
1. 全てのキーに適切なTTL(有効期限)を設計せよ。 永続化すべきでないデータがメモリを圧迫していないか、定期的に `MEMORY USAGE` や `SCAN` で監査する文化を作れ。
2. OOMエラーを「想定内」としてコードに組み込め。 握りつぶすな、リトライするな。OOMを検知したら即座にアラートを上げ、書き込みを安全にデグラデーション(機能縮退)させろ。
3. キャパシティプランニングを怠るな。 ピーク時のメモリ増加量を予測し、スケールアップやクラスター化(Redis Cluster)の閾値をあらかじめ定めておけ。
コードレビューで「とりあえず `try-except` で囲みました」というプルリクエストを見つけたら、今日の話を思い出してほしい。そのコードは、障害時にシステムのとどめを刺す兇器になり得るのだから。
コメント