Redisメモリ管理の深層:`INFO memory` が語る物理と論理の乖離
大規模なインメモリシステムを運用するアーキテクトにとって、Redisのメモリ管理機構の理解は避けて通れない領域だ。
「`used_memory` が物理メモリの消費量を示している」などと誤解しているうちは、本番環境で突然発生するOOM Killer(Out of Memory Killer)の悪夢を防ぐことはできない。
本稿では、`INFO memory` コマンドが返す主要なメトリクスの定義を再定義し、それらがOSのカーネル空間とどのようにインタラクションしているのか、その低レイヤのメカニズムを解き明かす。
—
1. 主要メトリクスの厳密な定義と内部構造
まずは、誰もが最初に目にする `INFO memory` の出力から、主要な三つの指標をピックアップする。
- `used_memory`: Redisがアロケータ(jemalloc等)を通じて確保した論理的なメモリ量(バイト単位)。
- `used_memory_rss`: OSの視点から見た、Redisプロセスに割り当てられた物理メモリ量(Resident Set Size)。
- `used_memory_peak`: 起動後から現在に至るまでに記録した `used_memory` の最高値。
これらは一見して単純な数値に見えるが、データベースエンジンの内部実装とOSのメモリ管理モデルを知る者にとって、それぞれの値が意味する乖離こそがシステムの挙動を予測する鍵となる。
`used_memory` の正体
`used_memory` は、データ構造そのもののサイズに加えて、Redisのオーバーヘッド(辞書構造体のメタデータ、ポインタ、各キーの有効期限を保持する構造など)を含んでいる。しかし、これはOSが実際に消費しているメモリではない。内部のアロケータが管理する「ヒープの論理サイズ」に過ぎない。
`used_memory_rss` の正体
RSS(Resident Set Size)は、ページテーブルを通じてOS(Linuxカーネル)が物理メモリ上にマッピングしているサイズだ。これは匿名メモリ(Anonymous Memory)だけでなく、共有ライブラリや後述するjemallocのアロケーション単位(チャンク/ページ)を包含している。
—
2. なぜ `used_memory` と `used_memory_rss` は乖離するのか?
大規模なRedisインスタンスを運用していると、`used_memory` が 10GB であるにもかかわらず、`used_memory_rss` が 16GB に跳ね上がっている光景によく遭遇する。この「差分」の正体こそが、メモリ最適化における最大の戦場である。
① アロケータの断片化(Fragmentation)
Redisはデフォルトで `jemalloc` をメモリメッセンジャーとして使用している。jemallocは、メモリの断片化(Fragmentation)を最小限に抑えるよう設計された極めて優秀なアロケータだが、それでも「フラグメンテーション」を完全にゼロにすることはできない。
- メカニズム: Redisが頻繁にデータの書き込み・削除(`DEL`, `EXPIRE`)を繰り返すと、アロケータの内部で細切れの空き領域(ホール)が生まれる。
- 影響: `used_memory`(実際にデータが使用している論理サイズ)は減少するが、jemallocがOSから切り離せない(返還できない)ページが存在するため、OS側から見たRSSは下がらない。
この状態を示すのが、`INFO memory` のもう一つの重要指標 `mem_fragmentation_ratio`(`used_memory_rss / used_memory`)である。
危険水域にある INFO memory の出力例
used_memory:10737418240 # 10 GB (論理)
used_memory_rss:17179869184 # 16 GB (物理)
mem_fragmentation_ratio:1.60 # 断片化率 1.6 (60%の無駄が発生)
この比率が 1.5 を超え、かつメモリプレッシャーが高まっている場合、システムは深刻なメモリ効率の低下に陥っている。
② Copy-on-Write (CoW) とフォーク
RDBスナップショット(`BGSAVE`)やAOFの書き換え時、Redisは `fork()` システムコールを発行し、子プロセスを生成する。
Linuxカーネルは仮想メモリ空間をコピーする際、物理メモリの即時コピーを行わず、Copy-on-Write メカニズムを採用する。
- メカニズム: 親プロセス(Redisメイン)が既存のキーを更新(Write)しようとした瞬間、カーネルはそのメモリページを別の物理アドレスに複製する。
- 影響: 書き込み負荷が高いワークロード下で `BGSAVE` が走ると、一時的に物理メモリ消費量(RSS)が急増する。最悪の場合、`used_memory` が物理メモリの半分程度であっても、CoWによるページ複製で物理メモリが枯渇し、OOM Killerの餌食になる。
—
3. 限界を突破するためのメモリ最適化戦略
現場のアーキテクトとして、このメモリの不整合とどう向き合うべきか。具体的な処方箋を提示する。
1. Active Defragmentation(アクティブデフラグ)の導入
Redis 4.0以降、実行中のプロセスを止めずにメモリ断片化を解消する Active Defragmentation が導入されている。
これを有効化することで、jemallocが管理するページ内のキーを再配置し、OSへメモリを返還(`madvise` の `MADV_DONTNEED` 等を利用)させることが可能だ。
redis.conf での設定例
activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 30
注意: アクティブデフラグはCPUサイクリングを消費する。レイテンシクリティカルな環境では、CPU使用率のスパイクに注意しながらチューニングを行うこと。
2. 適切なメモリ上限とエビクションポリシーの設定
`maxmemory` を物理メモリの限界値に設定する愚は犯してはならない。
OSのカーネルパニック、フォーク時のCoWバッファ、ネットワークバッファ等で消費されるマージンを必ず考慮せよ。
目安として、物理メモリの 70% 〜 75% を `maxmemory` の上限値とし、残りをOSおよびCoWのバッファとして確保するのが鉄則である。
例: 32GB 搭載マシンの場合
maxmemory 24gb
maxmemory-policy volatile-lru
3. ハッシュ構造の最適化(Hash-max-ziplist-entries)
Redisは小さなハッシュやリストを、ポインタのオーバーヘッドを削減するために `ziplist`(または `listpack`)という連続したメモリ領域にパックして保持する。
デフォルトの閾値を見直し、小規模なデータ構造がハッシュマップではなく効率的なエンコーディングで保持されるようスキーマ設計を行うことも、論理メモリ(`used_memory`)を抑制する上で極めて有効だ。
—
結言
`INFO memory` が吐き出す数値は、単なる統計データではない。それはRedisの内部で今何が起きているか、アロケータとOSカーネルが裏でどのような駆け引きを行っているかを語る、極めて雄弁なシグナルである。
「なぜRSSが高いのか?」という問いに対し、アロケータの断片化なのか、それともフォーク時のCoWによるものなのかを即座に切り分け、適切なパラメータチューニングを行えること。それこそが、インメモリデータベースを極めたエンジニアと、単なるツールの利用者を分かつ境界線である。
コメント