—
Redisのメモリ指標を「雰囲気」で監視するな:`INFO memory` から紐解くカーネル・アロケータ・OS物理メモリの真実
「`used_memory` はまだ上限の60%程度。にもかかわらず、なぜかカーネルの OOM Killer によって Redis プロセスが殺された」
本番運用において、このような地獄を経験した(あるいは引き起こした)エンジニアは少なくないはずだ。多くの開発者が Monitor ツールや APM のダッシュボード上で `used_memory` のグラフだけを眺め、「メモリにはまだ余裕がある」と高括っている。
結論から言おう。`used_memory` だけを見る監視は、エンジニアリングにおける怠慢であり欠陥設計だ。
Redisはシングルスレッドのインメモリデータベースであり、メモリ管理の大部分をアロケータ(主に `jemalloc`)と Linux カーネルの仮想メモリ機構に依存している。Redisの真のメモリ状態を把握し、堅牢なシステムを構築するには、`INFO memory` が出力する各指標が 「OSの物理メモリ(RAM)とどう連動しているか」 をカーネルレベルで理解しなければならない。
この記事では、テクニカルリードの視点から、`used_memory`、`used_memory_rss`、`used_memory_peak` などの本質的な意味と、それらを元にした堅牢な運用・設計パターンを叩き込む。
—
1. 指標の定義とOS物理メモリとの階層構造
まず、Redisがメモリを確保する際のレイヤー構造を脳内に叩き込んでほしい。
+——————————————————–+
| Linux Kernel |
| System Physical RAM (e.g., 32GB) |
+——————————————————–+
|
v Page Allocation (4KB Pages / Huge Pages)
+——————————————————–+
| OS Resident Set Size (used_memory_rss) |
| (Redisプロセスに実際に割り当てられている物理メモリ領域) |
+——————————————————–+
|
v Bins / Arenas / Chunk Management
+——————————————————–+
| Memory Allocator (jemalloc / tcmalloc / libc) |
+——————————————————–+
|
v zmalloc (Redis wrapper)
+——————————————————–+
| Redis Core Data Structures |
| (used_memory / used_memory_peak) |
+——————————————————–+
Redisは直接物理メモリを操作しない。`zmalloc` というラッピング関数を介して `jemalloc` などのメモリアロケータにメモリ要求を出し、アロケータが Linux カーネルからページ単位で領域を確保する。この構造を踏まえた上で、主要指標を紐解く。
各主要メトリクスの厳密な定義
1. `used_memory`
- 定義: Redisのデータ(Key, Value, データ構造のオーバーヘッド, 内部状態など)を保持するために、アロケータを通じて割り当てられたロジカルな総メモリ使用量。
- 物理メモリとの関係: これは「Redisが要求したメモリ量」に過ぎない。物理RAMの占有量そのものではない。
2. `used_memory_rss` (Resident Set Size)
- 定義: OS(Linuxカーネル)の見地から、Redisプロセスに実際に割り当てられている物理メモリのサイズ(ページ数 × ページサイズ)。
- 物理メモリとの関係: これが「物理RAMをどれだけ食っているか」の正解である。 ここにはRedisのデータだけでなく、以下の要素がすべて含まれる。
- メモリアロケータの断片化(Fragmentation)
- アロケータ内部のメタデータ領域
- C言語のコールスタック、共有ライブラリ等
3. `used_memory_peak`
- 定義: Redisプロセスが起動して以降、記録された `used_memory` の歴史的最高値。
- 運用の意味: 過去に一度でもこのピークに達したということは、アロケータはそのサイズ(あるいはそれ以上)の領域をOSから確保した実績があることを意味する。データの削除(`DEL` / Expire)を行っても `rss` が下がらない問題の原因を調査する重要手がかりとなる。
4. `mem_fragmentation_ratio`
- 定義: `used_memory_rss / used_memory` の計算値。
- 評価基準:
- `> 1.5`(高断片化): アロケータがOSから確保した物理メモリ領域の多くが、Redisの実際のデータに使われず空腹状態(空きスロット)になっている。メモリの浪費。
- `1.0 〜 1.5`(正常領域): アロケータのオーバーヘッドを考慮すると非常に健全な状態。
- `< 1.0`(極限状態・危険): 物理RAMが不足し、OSがスワップ(Swap)を発生させている。Redisの応答速度は壊滅的(ミリ秒→数百ミリ秒)になる。
—
2. アロケータ(jemalloc)とカーネルが引き起こす「乖離」のメカニズム
なぜ `used_memory` と `used_memory_rss` の間に巨大な乖離(ギャップ)が生じるのか。その理由は3つある。
① jemalloc の「スラブ割り当て(Slab Allocation)」と内部断片化
`jemalloc` はメモリ獲得の高速化とロック競合の回避のため、メモリを固定サイズ(8Byte, 16Byte, 32Byte, … 4KBなど)の「ビン(Bin)」に分類して管理する。
例えば、Redisが 33 Byte の文字列構造体を確保しようとした場合、jemalloc は 48 Byte のビンを割り当てる。差分の 15 Byte は無駄になる。これが大量のキーで発生すると、`used_memory` は 33 Byte 基準で計算される一方で、`used_memory_rss` は 48 Byte 基準で増えていき、断片化率(`mem_fragmentation_ratio`)が跳ね上がる。
② Linux カーネルの Copy-on-Write (CoW) と BGSAVE
RDBのスナップショット作成(`BGSAVE`)や AOF rewrite(`BGREWRITEAOF`)を実行する際、Redisは `fork()` システムコールを発行して子プロセスを生成する。
Linuxの `fork()` は親プロセスと物理メモリページを共有(Copy-on-Write)する。しかし、親プロセス(メインスレッド)に書き込みリクエストが走ると、カーネルは書き込みが発生した 4KB のページ全体を別領域にコピー(Duplicate)する。
[BGSAVE実行中のメモリ増大の挙動]
親プロセス (Redis Main) 子プロセス (RDB Saver)
| |
+———-> [ Page A ] <----------+ (参照のみ: メモリ増加なし)
|
(Write発生!)
|
v
[ Page A' ] (新規コピー作成)
[ Page A ] --------------------------> (古いページを維持)
高スループットな環境で更新処理(`SET`, `INCR`等)が激しく発生すると、膨大な数のページがデュプリケートされ、`used_memory_rss` が一気に通常時の1.5倍〜2倍近くに膨れ上がる。 `used_memory` に変化がないにもかかわらず、物理メモリを使い果たして OOM Killer が起動する最大の原因がこれだ。
③ Transparent Huge Pages (THP) の害悪
Linuxカーネルの機能である THP が有効になっていると、デフォルトの 4KB ページではなく 2MB ページ 単位でメモリが扱われる。
CoW 発生時、たった 1 Byte の更新のために 2MB 全体がコピー される。これにより CoW のオーバーヘッドが爆発し、RSSの急増とレイテンシスパイクを引き起こす。
—
3. 実機ログから紐解く `INFO memory` の診断ノウハウ
実際の `INFO memory` 出力をベースに、リードエンジニアとしてどこを注視すべきかを解説する。
redis-cli INFO memory の実行結果例
127.0.0.1:6379> INFO memory
Memory
used_memory:10737418240 # 10.00 GB (Redisがロジカルに保持しているデータ量)
used_memory_human:10.00G
used_memory_rss:18253611008 # 17.00 GB (OSがRedisに割り当てている実際の物理RAM)
used_memory_rss_human:17.00G
used_memory_peak:15032385536 # 14.00 GB (過去最高のused_memory)
used_memory_peak_human:14.00G
used_memory_lua:37888 # Luaスクリプトエンジンが消費するメモリ
maxmemory:12884901888 # 12.00 GB (設定上の上限値)
maxmemory_human:12.00G
maxmemory_policy:volatile-lru # メモリ溢れ時の退避ポリシー
mem_fragmentation_ratio:1.70 # 17.00G / 10.00G = 1.70 (危険度の高い断片化)
mem_allocator:jemalloc-5.3.0 # 使用中のメモリアロケータ
診察インプレッション
- 問題点: `used_memory` (10GB) に対し、`used_memory_rss` (17GB) となっており、`mem_fragmentation_ratio` が 1.70 に達している。7GB近くの物理メモリがアロケータの断片化によって空費されている。
- 危険性: `maxmemory` は 12GB に設定されているため、`used_memory` (10GB) は制限以下と誤認しやすい。しかし、実際の物理メモリ使用量(RSS: 17GB)はすでにそれを遥かに凌駕している。ここで `BGSAVE` が走れば、CoWによって物理RAM(例えばホストが24GB積んでいても)を食い潰し、カーネルにKillされる。
—
4. 堅牢な本番システムを構築するための設計パターン&運用ルール
この問題を防ぎ、予測可能で安定した Redis インフラを維持するための「鉄則」を授ける。
設計パターン1: `maxmemory` の正しい算出方程式
ホストの物理RAMの総量を $RAM_{total}$ としたとき、`maxmemory` を $RAM_{total}$ の 90% などに設定するのは素人の設計だ。`BGSAVE` や断片化率を考慮した健全な設計標準は以下の通りである。
$$maxmemory = (RAM_{total} – OS\_Reserved) \times \frac{1}{1 + (FragRatio_{est} – 1) + CoW_{margin}}$$
実務的な安全策として、以下のルールを固守せよ:
1. BGSAVE/AOF rewrite を行うノード(Primaryやバックアップ用Replica)
- `maxmemory` は物理RAMの 50% 〜 65% に抑える。
- 残りの 35% 〜 50% は、CoW発生時のページコピー領域およびアロケータの断片化のバッファとしてOSに捧げる必要がある。
2. Persistence(永続化)を完全にオフにし、純粋なキャッシュとして使うノード
- `maxmemory` は物理RAMの 70% 〜 80% に設定可能。
設計パターン2: Active Defragmentation(動的デフラグ)の有効化
Redis 4.0以降、`jemalloc` と密に連携したアクティブデfrag機能が実装されている。メインスレッドの空き時間(CPUに余裕がある時)を使って、メモリ配置を再構成し、断片化を解消する。
redis.conf 設定例
動的デフラグを有効化
activedefrag yes
断片化率が 1.5 (150%) を超えたらデフラグ開始を検討
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
断片化率が 3.0 (300%) に達したら全力でデフラグを実行
active-defrag-threshold-upper 30
デフラグ処理が使用するCPUパワーの下限と上限(%)
active-defrag-cycle-min 5
active-defrag-cycle-max 50
> 注意(リードエンジニアの助言): Active Defrag はCPUリソースを消費する。レイテンシに極限までシビアなシステムでは、トラフィックの少ない時間帯に CLI から手動で `MEMORY PURGE` コマンドを発行し、アロケータに未使用領域を即座に解放させる運用の方が安全なケースもある。
アロケータ(jemalloc)に対し、保持している空きページを即座にOSへ返還させる
127.0.0.1:6379> MEMORY PURGE
OK
設計パターン3: OSカーネルパラメータの必須チューニング
Redisのインフラ(EC2, Bare-metal, Dockerコンテナ等)をプロビジョニングする際は、以下のカーネルパラメータ設定を IaC (Terraform / Ansible等) に必須組み込み とすること。
1. Overcommit Memory の変更
Linuxカーネルが「物理メモリ以上のメモリ割り当て要求」を許可するように設定する。これを怠ると `BGSAVE` 時の `fork()` が `Cannot allocate memory` で失敗する。
sysctl.conf に記述
sysctl vm.overcommit_memory=1
2. Transparent Huge Pages (THP) の無効化
前述の通り、CoW時の増大を防ぐために 絶対無効化 する。
OS起動スクリプト等で実行
echo never > /sys/kernel/mm/transparent_hugepage/enabled
—
5. 結論(TL;DR)
コードレビューおよびインフラ設計レビューにおいて、以下のチェックリストをパスしないRedis構成は本番投入を拒否すべきである。
1. `used_memory` ではなく `used_memory_rss` をアラートのPrimaryメトリクスに設定しているか?
- ホスト物理メモリに対する `used_memory_rss` の割合が 80% を超えたら PagerDuty を鳴らせ。
2. `mem_fragmentation_ratio` を監視しているか?
- `> 1.5` で警告、`< 1.0` はスワップ発生(緊急異常)として検知せよ。
3. `maxmemory` はホスト物理RAMの「全量」になっていないか?
- 永続化ありなら 50-65%、キャッシュ特化なら 70-80% が限界値である。
4. THP(Transparent Huge Pages)は無効化されているか?
Redisの高速性は、OS物理メモリという有限で最も貴重なリソースの上に成り立っている。`INFO memory` の数字の裏にあるカーネルとアロケータの動態を理解し、真に「崩れない」インフラを設計してほしい。
コメント