Redisのメモリ管理の闇と処方箋:`MEMORY PURGE`という劇薬をどう扱うか
こんにちは。テックリードの私だ。
今日のコードレビュー、あるいは昨夜発生したPagerDutyのアラートを思い出してほしい。
「Redisのメモリ使用量が物理リミットに張り付いている。しかし、キーの総数は減っているはずだ。なぜだ?」
この現象に直面したとき、多くのエンジニアはパニックに陥り、不必要な`FLUSHALL`を検討したり、場当たり的なインスタンスのスケールアップに走る。しかし、世界最高峰のエンジニアを目指す我々であれば、根本的な原因と、そこに潜むアロケータの挙動を理解していなければならない。
今回は、Redis 4.0以降にしれっと導入されながら、その破壊力とメカニズムの複雑さゆえに実務で誤用されがちな`MEMORY PURGE`コマンドについて、アーキテクチャの深層から解説しよう。
—
1. なぜRedisはメモリを離さないのか?(メモリ断片化のメカニズム)
まず大前提として、Redisが消費しているメモリと、OSが認識しているメモリ(RSS: Resident Set Size)には常にギャップが存在する。このギャップの主犯がメモリ断片化(Fragmentation)だ。
Redisは、メモリの割り当てと解放を効率的に行うために、OSから直接メモリを確保するのではなく、専用のメモリメディエーターであるメモリ allocator(jemalloc、tcmalloc、あるいはlibcのmalloc)を介している。デフォルトでは、多くのディストリビューションで`jemalloc`が採用されている。
jemallocの裏側:チャンクとラン
jemallocは、メモリを効率的に管理するために「チャンク」や「ラン」という単位でOSからメモリをまとめ取りし、細かいオブジェクトの割り当て要求に対してそれを切り分けて渡す。
ここで問題が発生する:
1. 大量のデータ(例: 数千万件のハッシュやZSET)を投入し、その後削除したとする。
2. Redisのロジック上、これらのキーや要素は消滅し、`used_memory`(Redisが認識しているメモリ使用量)は劇的に低下する。
3. しかし、`jemalloc`はそのメモリを「いつかまたRedisが使うかもしれない」と予測し、OSへ即座に返還せず、内部プールに保持し続ける。
4. 結果として、`used_memory`は低いのに、OS上のプロセスが占有するメモリ(`used_memory_rss`)は高いままという、「メモリの幽霊現象(高断片化率)」が起きる。
この断片化率(`mem_fragmentation_ratio = used_memory_rss / used_memory`)が1.5を超えてくたり、あるいはOSのOOM Killerの足音が聞こえてきたとき、我々が取るべき次の一手が`MEMORY PURGE`だ。
—
2. `MEMORY PURGE` コマンドの本質
`MEMORY PURGE`は、Redis 4.0で導入されたコマンドである。その役割は極めてシンプルかつアグレッシブだ。
127.0.0.1:6379> MEMORY PURGE
OK
このコマンドを実行すると、Redisは内部で利用しているメモリメディエーター(jemalloc等)に対し、「現在未使用で内部プールに溜め込んでいるメモリの解放を強制的に試みよ(Purge)」というシグナルを送る。
注意すべき「試みよ」というニュアンス
ドキュメントをよく読んでほしい。このコマンドは「解放を保証する」ものではない。「試みる(tries to free)」のだ。
アロケータの内部状態やOSのページキャッシュの状況によっては、パージを実行してもRSSがほとんど下がらないこともある。過度な期待は禁物だが、正しく条件が揃えば、OSへ数GB単位のメモリを即座に返還し、ノードの安定性を劇的に高めることができる。
—
3. 実務における設計パターン:いつ、どう使うべきか?
では、このコマンドを実務のシステムアーキテクチャにどう組み込むべきか。
結論から言えば、「アプリケーションのトラフィックが完全に枯渇しているバッチ処理の合間」、あるいは「監視システムからの自動トリガー」として非同期に実行するのがベストプラクティスだ。
アンチパターン:高負荷時の定期的実行
「断片化が怖いから、cronで毎分`MEMORY PURGE`を叩くか」――これ絶対禁止。
メモリのパージ(解放)は、アロケータ側で内部のツリー構造の再編成やOSへの`madvise`システムコールなどを伴うため、それなりのCPUコスト(ロック競合やレイテンシースパイク)が発生するシングルスレッドのRedisにおいて、高負荷時にこれを実行することは、レイテンシーの悪化(P99の跳ね上がり)を招く自殺行為である。
推奨される設計:オブザバビリティと連動した自動化スクリプト
以下は、我々のチームがプロダクション環境で採用している、安全なメモリパージ運用の設計思想だ。
1. モニタリング: Prometheus等で `redis_memory_fragmentation_ratio` と `redis_connected_clients` を監視。
2. 条件判定:
- 断片化率が `1.5` を突破している。
- かつ、コネクション数が平時の10%以下(深夜帯など)。
3. 実行と検証:
- `MEMORY PURGE` を発行。
- 実行前後の `used_memory_rss` の差分をログに記録。
—
4. コードレビューの視点:設計レビューで確認すべき項目
もし、あなたのプロジェクトでRedisのメモリ肥大化に悩むジュニアエンジニアから「`MEMORY PURGE`を定期実行するコードを書きました」とプルリクエストが来たら、以下の観点で容赦なく差し戻してほしい。
- 「アクティブデフラグ(Active Defragmentation)の検討は済んでいるか?」
Redis 4.0/5.0以降であれば、`activedefrag yes` を設定することで、Redis自身がバックグラウンドでメモリの断片化を動的に解消していく機能が使える。まずはこっちのチューニングが先だ。`MEMORY PURGE`はあくまで最後の手段である。
- 「maxmemory-policyの選定は適切か?」
そもそも、メモリが枯渇する設計になっていないか? `volatile-lru` や `allkeys-lru`、あるいは `noeviction` のどれを選択しているかによって、アロケータの挙動も変わる。
- 「`INFO memory` のメトリクスを正しく理解しているか?」
`mem_fragmentation_bytes` や `allocator_frag_bytes` など、どこにメモリが無駄に消えているのかを `MEMORY STATS` や `INFO` で定量的に証明できているか。
—
5. まとめ
Redisの運用において、メモリは最も高価であり、最もデリケートなリソースだ。
- `MEMORY PURGE` は、アロケータが抱え込んだ未使用メモリをOSへ返還させるための強力な劇薬である。
- しかし、アロケータ内部の仕組み(jemallocのプール構造)を理解せず、高負荷時に乱用すれば、かえってレイテンシー悪化という致命傷を負う。
- 本番環境での適用は、トラフィックの谷間を狙い、アクティブデフラグや適切な`maxmemory`ポリシーとの組み合わせで慎重に行うこと。
アーキテクチャの本質を知る者だけが、システムを優雅にコントロールできる。
あなたの設計するRedisクラスタが、深夜のアラートに怯えることのない堅牢なものであることを願う。
コメント