【実務・中級編】 メモリ断片化(Fragmentation) – Redis

Redisのメモリ断片化(Fragmentation):なぜ `mem_fragmentation_ratio` は罠だらけなのか

おい、設計レビューを始めるぞ。

お前らが何気なく監視しているGrafanaのダッシュボード、そこに映る `mem_fragmentation_ratio` を見て「お、1.5だからまだ安全だな」とか安心していないだろうか?
もしそうなら、今すぐその認識を改めろ。その指標単体を見ているだけでは、いつか本番環境で突然の OOM (Out Of Memory) キルを踏み抜くか、あるいは不必要なメモリ再割り当ての嵐でレイテンシの暴騰を引き起こすことになる。

今日は、Redisのメモリ管理の深淵――「メモリ断片化(Fragmentation)」のメカニズムと、その実務的な最適化戦略について、アーキテクトの視点からすべてを叩き込む。

—

1. `mem_fragmentation_ratio` の正体と、その残酷なまでの「嘘」

まず、基本を確認しよう。公式ドキュメントによれば、断片化率は以下の式で計算される。

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

  • `used_memory`: Redisが論理的にデータやメタデータのために消費していると認識しているメモリ量(アプリケーション視点のサイズ)。
  • `used_memory_rss` (Resident Set Size): OSのOS allocator(jemalloc等)が、Redisプロセスに対して実際に割り当てている物理メモリ量。

比率が `1.0` を超えていれば、OSが確保したメモリ(RSS)の方が、Redisが実際に使っているメモリよりも多い。その差分が「断片化」や「アロケータのオーバーヘッド」として消えている、という理屈だ。

なぜこれが「嘘」になり得るのか?

ここが現場のエンジニアが陥る最大の罠だ。この比率は「外部断片化」と「アロケータの内部管理(Arenaの空き領域)」を一緒くたにしている。

例えば、`used_memory_rss` が高くても、その大部分がjemalloc内部の再利用可能なキャッシュ(Dirty Pages)である場合、OSはプレッシャーがかかればそのメモリを即座に回収できる。つまり、見かけ上の比率が高くても、システムとしては健全なケースがあるのだ。
逆に、比率が `1.0` 前後であっても、物理メモリがスワップアウトし始めている地獄のような状況もあり得る。

—

2. なぜRedisでメモリ断片化が爆発するのか?(メカニズム)

Redisはシングルスレッドで動くインメモリKVSでありながら、メモリ管理の大部分を外部のアロケータ(デフォルトでは jemalloc)に依存している。このアロケータの挙動こそが断片化の根源だ。

① 頻繁な `HSET`, `ZADD`, そして容赦ない `DEL`

これが一番の悪夢だ。例えば、数百万件の巨大なHashやZSetを保持し、そこに毎秒数万件の細かい更新と削除(`HDEL` やキーの有効期限切れ `EXPIRE`)が走るワークロードを想像してほしい。

jemallocはメモリをサイズクラスごとに管理している。
1. 小さなオブジェクトが削除される。
2. そのオブジェクトが占有していたメモリチャンクの一部に「空き」ができる。
3. しかし、そのチャンク内の他の領域にまだ生きているデータがあれば、OSにメモリを返還(`munmap`)できない。
4. 結果として、「論理的にはデータが減ったのに、OS上のメモリ使用量はびくともしない」という状態が完成する。これが外部断片化の正体だ。

—

3. パフォーマンスへの致命的な影響

断片化を放置すると、システムには確実に破滅へのカウントダウンが始まる。

1. スワップの発生(Latency Spike)
RSSが肥大化し、物理メモリの限界を超えると、Linux Kernelは容赦なくRedisをスワップ領域に追い込む。シングルスレッドで動作するRedisがディスク(スワップ)にアクセスした瞬間、レイテンシはマイクロ秒オーダーからミリ秒、最悪の場合は秒オーダーへと跳ね上がり、アプリケーション側でタイムアウトが連鎖する。
2. OOM Killerによる突然の死
`used_memory` は空きがあるように見えても、RSSがサーバーの物理メモリ上限に達していれば、LinuxのOOM Killerが容赦なくRedisプロセスを刈り取る。フェイルオーバーの設計が甘ければ、数分間のサービス停止は免れない。

—

4. 実務で使える堅牢な設計と対策パターン

では、我々はどう戦うべきか。コードレビューやインフラ設計で必ず適用すべきプラクティスを授ける。

パターンA: 動的な断片化デフラグ(Redis 4.0以降の必須機能)

Redis 4.0以降、アクティブデフラグメンテーション(Active Defragmentation)機能が導入されている。これを有効化することで、Redisはメモリを再配置し、断片化をライブで解消してくれる。

redis.confの設定例:

アクティブデフラグを有効化
activedefrag yes

断片化率がこれを超えたらデフラグ開始
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100

CPUを食いすぎないための制御(重要)
active-defrag-cycle-min 5
active-defrag-cycle-max 35
active-defrag-max-scan-fields 1000

> architect’s Note: `active-defrag-cycle-max` を高くしすぎると、デフラグのメモリコピー処理がシングルスレッドをブロックし、レイテンシの悪化を招く。本番環境で負荷テストを行いながら、CPU使用率とレイテンシのトレードオフを必ず計測してチューニングしろ。

パターンB: 有効期限(TTL)の分散設計

一斉に消えるデータは、一斉に断片化を産む。
例えば、全キーに対して「24時間後」という厳密なTTLを設定するのは最悪だ。一日の終わりに一斉にキーが消滅し、jemallocのArenaがボロボロに引き裂かれる。

対策: ジッター(ランダムな揺らぎ)を加えろ。

Python (Redis-py)での実装例:TTLにランダムな揺らぎ(±10%)を付与する
import random

def set_with_jitter(client, key, value, base_ttl_sec=86400):
jitter = random.randint(-8640, 8640) # ±10%の揺らぎ
actual_ttl = base_ttl_sec + jitter
client.setex(key, actual_ttl, value)

この一手間で、メモリ解放のタイミングが時間軸上に綺麗に分散され、断片化のスパイクを防ぐことができる。

パターンC: 最大メモリポリシー(maxmemory-policy)の選定

メモリが枯渇しそうなときの挙動を正しく設計しておけ。

  • `noeviction`: デフォルト。メモリ上限を超えると書き込み系コマンドがエラーを返す(安全だが可用性が下がる)。
  • `volatile-lru` / `allkeys-lru`: LRU(最近使われたないもの)ベースで削除。断片化対策としては、キーの削除頻度が上がるため、アロケータの挙動を注視する必要がある。

実務では、揮発性のキャッシュであれば `allkeys-lru` を選びつつ、メモリ上限(`maxmemory`)を物理メモリの70〜80%程度に厳しく制限し、OS側のオーバーヘッドやRedisのfork時(RDBスナップショット生成時)のCopy-on-Write(CoW)メモリ枯渇を防ぐのが鉄則だ。

—

5. 結論:モニタリングの正しいアプローチ

監視アラートを組むときは、単一の `mem_fragmentation_ratio` だけを見るな。以下の3点セットで監視を構築しろ。

1. `mem_fragmentation_ratio` が `1.5` を超え、かつ `used_memory_rss` が右肩上がりで増加しているか?
2. `used_memory` に対する物理メモリの割合が安全圏内にあるか?
3. Redisのログに出力される `Active defrag done` の統計や、レイテンシメトリクス(`latency graph`)に異常がないか?

インフラは雰囲気で動かすな。メカニズムを理解し、コードと設定でロジカルにねじ伏せろ。
次回の設計レビューでは、これらの対策が組まれていることを期待する。解散!

コメント

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