【実務・中級編】 メモリ使用量メトリクス – Redis

—

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` の数字の裏にあるカーネルとアロケータの動態を理解し、真に「崩れない」インフラを設計してほしい。

コメント

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