【実務・中級編】 メモリ割り当てとアロケータ – Redis

Redisのメモリ破綻を防ぐ:アロケータの解剖学とメモリ断片化の極限制御

Redisを運用していて「`maxmemory`にはまだ余裕があるはずなのに、なぜかOSのOOM Killerにプロセスが殺された」「レイテンシのスパイクが不定期に発生する」といった事態に遭遇したことはないでしょうか。

一般的なWebアプリケーションのコードレビューなら、不要なオブジェクトの参照を切るだけで解決するかもしれません。しかし、インメモリデータベースであるRedisにおいて、「メモリ管理」とは単なるデータ構造の配置ではなく、OSとメモリ割り当てライブラリ(アロケータ)との物理レベルの格闘を意味します。

Redisが裏側でどのようにメモリを要求し、なぜjemallocを選択し、どのように断片化(Fragmentation)と戦っているのか。テクニカルリードの視点から、プロダクション環境で耐えうるメモリ管理の「極限の知見」を解説します。

—

1. アロケータの解剖学:libc, tcmalloc, そしてjemalloc

RedisはC言語で書かれており、動的なメモリ確保には `malloc()` / `free()` のインターフェースを使用します。しかし、標準のCライブラリ(glibc等)が提供するデフォルトの `malloc` をそのまま超高並列・低レイテンシなインメモリDBで使うのは自殺行為です。

Redisのソースコード(`zmalloc.c`)を覗くと、コンパイル時にアロケータを選択する構造になっています。

/ zmalloc.c より抜粋:アロケータの判定ロジック /
if defined(USE_TCMALLOC)
define malloc(size) tc_malloc(size)
define calloc(count,size) tc_calloc(count,size)
define realloc(ptr,size) tc_realloc(ptr,size)
define free(ptr) tc_free(ptr)
elif defined(USE_JEMALLOC)
define malloc(size) jet_malloc(size)
define calloc(count,size) jet_calloc(count,size)
define realloc(ptr,size) jet_realloc(ptr,size)
define free(ptr) jet_free(ptr)
endif

なぜRedisはLinux環境においてjemallocを標準(デフォルト)として採用しているのか。各アロケータの特性を比較すれば、その理由が明白になります。

| アロケータ | 特徴 | Redisにおける評価 |
| :— | :— | :— |
| libc malloc (glibc) | 汎用的なアロケータ。小規模な割り当てと解放を繰り返すと、激しい外部断片化を引き起こす。 | 採用不可(非推薦)。 長時間運用でRSS(物理メモリ使用量)が肥大化し、確定でパフォーマンスが崩壊する。 |
| tcmalloc | Google製。スレッドごとのキャッシュ(Thread Cache)によりマルチスレッド下でのロック競合を極限まで減らす。 | 優秀だが、Redisのようなシングルスレッドメインのアーキテクチャでは過剰スペック。断片化抑制能力でjemallocに及ばない。 |
| jemalloc | FreeBSDやFacebookで採用。メモリを「Size Class」に細分化し、アリーナ単位で管理。アクティブデフレグ(動的断片化解消)APIを提供する。 | 最適(標準)。 外部断片化の抑制能力が極めて高く、Redis本体と密結合したメモリクリーンアップが可能。 |

jemallocの真価:Size Class(サイズクラス)

jemallocはメモリを無段階に切り出すのではなく、あらかじめ定義された「Size Class(8B, 16B, 32B, 48B, 64B…)」に丸めて割り当てます。

これにより、「小さなオブジェクトの頻繁な生成・破棄によってメモリが虫食い状態になる」現象(外部断片化)を構造的に防ぎます。 ただし、これには「要求されたサイズより少し大きめの領域を割り当てる」という内部断片化のトレードオフが伴います。このトレードオフを理解していないと、次の「断片化問題」のドミノ倒しに巻き込まれます。

—

2. メモリ断片化の病理学:RSSとused_memoryのギャップ

レビュー時にエンジニアから「`used_memory`(Redisが認識しているデータ量)は10GBなのに、`top`コマンドで見ると物理メモリ(RSS)を18GBも消費しています」という相談を受けることがあります。

これがメモリ断片化(Memory Fragmentation)です。

$$\text{mem\_fragmentation\_ratio} = \frac{\text{used\_memory\_rss}}{\text{used\_memory}}$$

この比率(`mem_fragmentation_ratio`)の読み解き方は以下の通りです。

INFO memory の出力例
used_memory:10737418240 # 10GB (Redisが実際に保持している純粋なデータ量)
used_memory_rss:19327352832 # 18GB (OSから割り当てられている物理メモリ量)
mem_fragmentation_ratio:1.80 # 1.80 = 危険水域 (物理メモリの80%が断片化やオーバーヘッドで空転)

  • ratio > 1.5: 深刻な外部断片化が発生。OSレベルではメモリが確保されているが、Redisはそれを連続した領域として再利用できていない。
  • 1.0 <= ratio <= 1.5: 正常範囲(jemallocの内部断片化と管理オーバーヘッドの範疇)。
  • ratio < 1.0: 緊急事態。 物理メモリを使い果たし、OSがスワップ(Swap)を発生させている。レイテンシが数十ミリ秒〜数秒に跳ね上がり、サービスは事実上停止する。

暗殺者「Transparent Huge Pages (THP)」を殺せ

Linuxカーネルの機能であるTHP(Transparent Huge Pages)は、Redis運用における最大の敵の一つです。通常4KBのページサイズを2MB単位で管理することでTLBキャッシュミスを減らす機能ですが、RedisにおいてはCoW(Copy-on-Write)時のメモリ爆発を引き起こします。

`BGSAVE` や AOF書き換え時に、ほんの数バイトの書き込みがあっただけでOSは2MB全体のコピーを強制されます。これにより、アロケータの制御を超えた激しい断片化とメモリ不足が発生します。

【必須設定】本番環境の起動スクリプトまたはsysctlで必ずTHPを無効化すること
echo never > /sys/kernel/mm/transparent_hugepage/enabled

設定が反映されているか確認([never] となっていればOK)
cat /sys/kernel/mm/transparent_hugepage/enabled
出力例: always madvise [never]

—

3. 極限の最適化:Active Defragmentation(アクティブ・デフレグ)の深層

かつて、Redisで断片化が限界に達した場合の解決策は「Redisプロセスの再起動」または「FailoverさせてClusterノードを入れ替える」しかありませんでした。

しかし、Redis 4.0以降、jemallocとRedis本体が深く連携したActive Defragmentation(動的断片化解消)が導入されました。

Active Defragのメカニズム

1. Redisのメインループ(またはバックグラウンドスレッド)がjemallocの特殊API(`je_get_allocated_size`等)を呼び出し、特定のポインタが指すメモリ領域が断片化しているかチェックする。
2. 断片化していると判断された場合、新しい連続したメモリ領域を別途確保し、既存データをそこにコピーし、古いポインタを付け替えて元の領域を解放する。
3. この処理を、キーの走査とともに少しずつ(CPU使用率の制限枠内で)実施する。

これを実現できるのは、Redisが内部でjemalloc専用のフックAPIを利用しているからです。`libc` では絶対に不可能な芸芸です。

本番環境向け `redis.conf` チューニングレシピ

デフォルトの設定では、Active Defragが甘すぎるか、あるいは割り込みが強すぎて本業務のレスポンスタイムを阻害する可能性があります。高トラフィック環境で推奨される設計パラメータを以下に示します。

===================================================================
Active Defragmentation 設定 (jemalloc使用時のみ有効)
===================================================================

動的断片化解消を有効化
activedefrag yes

断片化による無駄なメモリ消費が「100MB」を超えたらデフレグを開始
active-defrag-ignore-bytes 100mb

mem_fragmentation_ratio が 1.15 (15%の断片化) を超えたらデフレグ候補とする
active-defrag-threshold-lower 15

mem_fragmentation_ratio が 1.30 (30%の断片化) に達したら最大速度でデフレグを実行
active-defrag-threshold-upper 30

デフレグ処理に使用するCPU使用率の最小値 (%)
CPUに余裕がある平常時のバックグラウンド処理速度
active-defrag-cycle-min 5

デフレグ処理に使用するCPU使用率の最大値 (%)
メインスレッドのレイテンシ(1-thread)に悪影響を与えないよう、最大でも25%〜30%以下に抑えるのが鉄則
active-defrag-cycle-max 25

1回のメインループ走査で評価するSET/HASH/LIST等の内部要素の最大数
active-defrag-max-scan-fields 1000

—

4. 現場で使える実務診断・運用プロトコル

設計レビューや障害解析の現場で、直ちに状況を把握し対処するためのアクションプロトコルです。

プロトコル1:アロケータ状態の精緻なモニタリング

単に `INFO memory` を見るだけでなく、jemalloc自身の内部統計情報を出力させて問題のレイヤー(Redis層か、アロケータ層か、OS層か)を特定します。

Redis CLIからjemallocの内部統計情報を取得(詳細なアリーナ情報が出力される)
redis-cli INFO memory

注目すべきメトリクス:

  • `allocator_frag_bytes`: jemallocが認識している断片化バイト数。
  • `allocator_rss_bytes`: jemallocがOSから借りている物理メモリ。
  • `rss_overhead_bytes`: OSとjemalloc間の乖離(THPやカーネルのページテーブルオーバーヘッド)。

プロトコル2:手動による強制パージ(緊急回避)

Active Defragの処理が追いつかず、物理メモリが圧迫された場合の「応急手当」として、jemallocの未使用ページをOSに即座に返却させるコマンドを発行します。

jemallocに対して、保持しているが使っていない空きページ(Unused Dirty Pages)をOSに即座に返却するよう要求
redis-cli MEMORY PURGE

診断コマンド:現在のメモリ状態についてRedis自体の助言を聞く
redis-cli MEMORY DOCTOR

—

結論:テクニカルリードが示すべきメモリ設計の鉄則

Redisのメモリ設計において、`maxmemory 32gb` とだけ書いて満足している設計書は不合格です。プロフェッショナルとして、以下の原則を設計レビューの基準として定めてください。

1. アロケータはjemalloc一択であることを保証せよ
コンパイル時オプションを変更しないこと。Alpine Linuxなどの軽量イメージを使う際、標準Cライブラリ(musl libc)の動作によってアロケータ挙動が変化していないかを必ず検証せよ。
2. OSカーネルのパラメータ(THP無効化、overcommit_memory=1)をコード(IaC)で強制せよ
アプリ層だけで解決しようとするな。インフラ層(Ansible, Terraform, K8s DaemonSet等)で `transparent_hugepage=never` を強制すること。
3. `mem_fragmentation_ratio` に基づくアラート閾値を二重に設定せよ

  • `ratio > 1.4` (Warning): Active Defragの稼働状況を確認。
  • `ratio < 1.05` かつ `used_memory` 近傍 (Critical): スワップ発生の恐怖。即時スケールアウトまたはノード再構築。

物理メモリの振る舞いを完全に制してこそ、Redisはその真のパフォーマンスを発揮します。抽象化されたレイヤーの裏側にある「バイトの配列」に思いを馳せ、堅牢なシステムを構築してください。

コメント

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