Redisメモリ管理の深層:アロケータの選択と断片化という名の不可避なエントロピー
Redisを単なる「高速なインメモリKVS」として扱っているうちは、中規模までのシステムでしか通用しない。ミリ秒単位のレイテンシとペタバイト級のデータセットを支える大規模アーキテクチャの最前線において、真にエンジニアの腕が試されるのは「メモリの物理的な挙動とアロケータの制約を完全に掌握できているか」という一点である。
OSの仮想メモリ空間から物理的なRAMのチップに至るまで、データがどのように配置され、どのように解放されるのか。今回は、Redisの心臓部を支えるメモリ割り当て(アロケータ)のメカニズムと、避けて通れない「メモリ断片化(Fragmentation)」の真実について、限界まで踏み込んで解説する。
—
1. アロケータの三相:libc, tcmalloc, そして jemalloc の思想
Redisは、コンパイル時に使用するメモリ割り当てライブラリを選択できる。デフォルトである `jemalloc` のほか、システムの標準である `libc (glibcのptmalloc)`、そしてGoogle製の `tcmalloc` が存在する。
これらは単に「メモリを確保する関数」のラッパーではない。それぞれが異なる「並行性と断片化に対する哲学」を持っている。
[Redis Core]
│
├─> jemalloc (Default): 内部クラスタリングとサイズクラスによる徹底的な断片化抑制
├─> tcmalloc (Google): スレッドローカルキャッシュによる極限のロック競合削減
└─> libc (glibc ptmalloc): 汎用OS標準。マルチスレッド環境でのスケーラビリティに限界
libc (ptmalloc) の呪縛
多くのLinuxディストリビューションでデフォルトの `glibc` に含まれる `ptmalloc` は、汎用目的としては優れているが、Redisのような「高頻度で多様なサイズのオブジェクトを生成・破棄し続けるシングルスレッド(I/O多重化)ベースのワークロード」においては悪夢を生む。
特に、ヒープ領域の境界を動かす `sbrk()` や `mmap()` の使い方が粗く、一度確保したメモリをOSになかなか返さない(Arenaの肥大化)。結果として、Redisプロセスが保持する仮想メモリサイズ(VSZ)と実メモリサイズ(RSS)が乖離し、OOM Killerの標的になりやすい。
tcmalloc の高速性とトレードオフ
Googleの `tcmalloc` (Thread-Caching Malloc) は、スレッドローカルなキャッシュ(Thread Cache)を持ち、ロック競合を極限まで排除することで知られている。
マルチスレッドで動作するアプリケーションでは圧倒的なパフォーマンスを発揮する。しかし、Redisの本質はシングルスレッドのイベントループである。tcmallocの恩恵(スレッド間ロックの回避)はRedisでは薄く、逆に大量の小さなキーバリューストアを扱う際のメタデータオーバーヘッドが、jemallocに比べて断片化を悪化させるケースが実務上確認されている。
勝利者としての jemalloc
Redisが標準アロケータとして `jemalloc` を採用しているのには、明確な理由がある。
`jemalloc` は、メモリをサイズクラス(Size Classes)と呼ばれる細かなバケツに分類し、チャンク(Chunk / 現在のバージョンではExtent)単位でOSから効率的にメモリを管理する。さらに、スレッド(Redisの場合は内部のバックグラウンドスレッド群)ごとのキャッシュ機構を持ちつつ、外部断片化(External Fragmentation)を最小限に抑えるための巧妙なアルゴリズムが組み込まれている。
—
2. 内部メカニズム:jemallocがいかにして「断片化」と戦うか
メモリ断片化には、主に2つの種類がある。
1. 内部断片化 (Internal Fragmentation): アプリケーションが要求したサイズよりも、アロケータが割り当てたサイズが大きい場合に生じる無駄。
2. 外部断片化 (External Fragmentation): メモリの総空き容量は十分にあるが、それらが連続していない(細切れになっている)ため、大きなサイズの連続したメモリ要求を満たせない現象。
Redisにおいて最も恐ろしいのは、「Redisがキーを削除している(メモリを解放している)にもかかわらず、OS上のRSS(Resident Set Size)が全く減らない」という現象だ。これはバグではなく、アロケータの仕様とOSのメモリ管理のトレードオフによって発生する。
jemallocのArenaとExtent構造
jemallocは、メモリを「Arena」と呼ばれる独立した管理単位に分割する。各Arenaは、OSからまとまったサイズのメモリ(Extent)を仮想アドレス空間上で確保し、それを細かく分割してRedisに提供する。
Redisが `DEL` コマンドや有効期限切れ(TTL)によって数ギガバイトのデータを削除した際、Redisコードベースからは `free()` が呼び出されている。しかし、`jemalloc` はその解放されたメモリを即座にOSへ返却する(`munmap` を呼ぶ)とは限らない。将来の高速な再割り当てのために、Arena内のビン(Bin)に保持し続けるのだ。
これにより、以下の状態が完成する。
- Redisの内部統計 (`used_memory`): 減少している。
- OSが認識するプロセス使用量 (`used_memory_rss`): 高止まりしている。
この乖離こそが、運用現場を混乱させる「見えないメモリ肥大化」の正体である。
—
3. 限界突破のプラクティス:アクティブ断片化排除の調律
Redis 4.0以降、メモリの断片化を動的に検知し、プロセスを再起動することなくコンパクト化を行う「Active Memory Defragmentation(アクティブデフラグ)」機能が導入された。これはアーキテクトにとって福音であるが、設定を誤るとCPUリソースを食いつぶす諸刃の剣となる。
以下の設定は、極限環境で運用されるプロダクション環境の `redis.conf` における現実的なチューニング指針の一例である。
メモリ断片化が「比率 1.5」を超え、かつ「絶対量 500MB」を超えた場合にデフラグを開始
activedefrag yes
active-defrag-ignore-bytes 500mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 30
active-defrag-cycle-min 5
active-defrag-cycle-max 50
active-defrag-max-scan-fields 1000
パラメータの深層解説
- `active-defrag-threshold-lower 10` / `upper 30`
- 断片化率(`mem_fragmentation_ratio` = RSS / used_memory)が1.1(10%増)を超えるとデフラグのバックグラウンド処理が微弱に働き始め、1.3(30%増)を超えるとCPUリソースを最大限(`cycle-max` まで)使って積極的にメモリの再配置(ポインタの付け替えとコピー)を行う。
- `active-defrag-cycle-min 5` / `max 50`
- Redisのシングルスレッドイベントループをブロックしないよう、CPU時間の何パーセントをデフラグに割くかの下限と上限。上限を50%に設定しているのは、過負荷時にクライアントのレイテンシ(LATENCY)をスパイクさせないための防衛策である。
—
4. 現場のシグナル:INFO memory の読み方
アーキテクトとして、日々のモニタリングで見るべきは単純なメモリ使用量ではない。`INFO memory` コマンドが返すメトリクスの相関関係だ。
実行例(redis-cli: INFO memory)
125# redis-cli -a “your_password” info memory
Memory
used_memory:10737418240 <-- Redisが論理的に使用しているバイト数 (10GB)
used_memory_human:10.00G
used_memory_rss:18253611008 <-- OSがプロセスに割り当てている物理メモリ (約17GB)
used_memory_rss_human:17.00G
used_memory_peak:19327352832
used_memory_peak_human:18.00G
used_memory_lua:45056
mem_fragmentation_ratio:1.70 <-- RSS / used_memory = 1.7 (断片化率 70%増)
mem_allocator:jemalloc-5.3.0 <-- 使用中のアロケータ
`mem_fragmentation_ratio` が `1.5` を超え、かつ `used_memory_rss` が物理マシンの空きメモリを圧迫し始めた場合、以下のエスカレーションパスを取る。
1. アクティブデフラグが有効になっているか確認する。
2. jemallocの挙動を強制的に外部へパージさせたい場合、Redis 4.0以降であれば `MEMORY PURGE` コマンドを発行する(jemallocに対してパージを促すが、OSが即座に回収する保証はない)。
3. 最終手段として、Replica(スラード)側でフェイルオーバーを計画的に行い、Masterを安全に再起動する(再起動によってアロケータの状態は完全に初期化され、RSSは `used_memory` の値へと収束する)。
---
結言
Redisのメモリ管理は、OS、アロケータ(jemalloc)、そしてRedis自身のデータ構造(Dict, SkipList, QuickList等)の三位一体で成り立っている。
「なぜメモリが減らないのか」という疑問に直面したとき、それはRedisのバグではなく、高パフォーマンスを維持するためにアロケータが選択した「戦略的なメモリ保持」の結果であることがほとんどだ。このメカニズムを解剖し、制御下における者だけが、真に安定したペタスケールのインメモリ基盤を架构(アーキテクト)することができる。
コメント