Redisメモリ最適化の極意:データ型別の内部表現と「数千万件の壁」を突破する設計戦略
テックリードの私だ。本日は、コードレビューで最も指摘が多く、かつプロダクトの運命を分ける「Redisのメモリ管理とデータ型最適化」について徹底的に解説する。
「とりあえず文字列でJSONを突っ込んでおくか」
「数千万件のユーザーデータを普通にHashで持たせよう」
……もし君のチームでこんな設計をしているなら、今すぐそのコードを止めてほしい。RedisはRAMという最も高価で有限なリソースを極限まで効率よく使うためのミドルウェアだ。その内部構造(エンコーディング)の理解なしにRedisを語ることは許されない。
今回は、Redisがメモリをどのように食いつぶし、どうすればそれを極限まで圧縮できるのか、そのメカニズムと実務で使える設計パターンを叩き込む。
—
1. Redisメモリ管理の核心:エンコーディングの動的切替
Redisの各データ型(String, Hash, List, Set, ZSet)は、抽象的なデータ構造の皮を被っているに過ぎない。実体は、データ量や要素のサイズに応じて動的にエンコーディング(内部表現)を切り替える極めてアグレッシブな設計になっている。
[クライアントのコマンド]
↓
[抽象データ型 (String / Hash / List / Set / ZSet)]
↓ (データ量やサイズを評価)
[物理エンコーディング (int / embstr / raw / hashtable / ziplist / intset / quicklist / skiplist)]
この切替条件を把握しているか否かで、メモリ使用量が数倍〜数十倍変わる。さっそく各データ型の深淵を覗いていこう。
—
2. データ型別の内部構造と最適化戦略
① String型:最強にして最大の罠
Stringは最もシンプルだが、最もメモリを無駄遣いしやすい。Stringのエンコーディングは3つ存在する。
1. `int`: 整数値の場合。64bit(8バイト)の整数として直接保持される。
2. `embstr`: 44バイト以下の文字列。Redisの内部オブジェクトヘッダと文字列本体が連続したメモリ領域にアロケートされる(キャッシュヒット率が極めて高い)。
3. `raw`: 45バイト以上の文字列。メモリが別々にアロケートされる。
【設計の鉄則】数値を文字列として保存するな
例えば、ユーザーIDやカウンタを文字列 `”123456789″` として保存すると、`raw` や `embstr` になり、無駄なメタデータやポインタのオーバーヘッドが発生する。整数として扱えるものは、可能な限りRedis側でも数値としてインクリメント等を行え。
—
② Hash型:メモリ効率の「隠し王」
「複数のフィールドを持つオブジェクト」を表現する際、まさかJSON文字列をStringで入れていないだろう? それはRedisの良さをドブに捨てる行為だ。
Hash型は、要素数が少なく、各フィールド・値が小さい場合、`ziplist`(またはRedis 7以降の `listpack`)という連続したメモリチャンクに圧縮されて格納される。
- ziplistのメリット: ポインタを持たず、メモリの断片化(Fragmentation)が圧倒的に起きにくい。
- ziplistのデメリット: 要素数が多くなったり、値が大きくなったりすると、`hashtable` へ昇格(Promote)する。昇格するとメモリ消費量が跳ね上がる。
設定パラメータのチューニング (`redis.conf`)
これを超えるとziplistからhashtableに昇格する(デフォルト)
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
もし「1つのHashに1,000フィールド持たせたい」「値が100バイトを超える」という要件があるなら、あえてハッシュを分割するか、この閾値をチューニングする必要がある。ただし、CPU負荷とのトレードオフになる点に注意せよ。
—
③ List型:Linked Listの呪縛からの解放
List型はかつて二重連結リスト(Linked List)とziplistを組み合わせていたが、現在のバージョンでは`quicklist`というデータ構造が標準だ。これは「ziplistをノードとした連結リスト」である。
- なぜquicklistなのか?
純粋なLinkedListは、各ノードごとにポインタ(前後)のオーバーヘッド(64bit環境なら1ノードあたり16バイト以上)が発生し、さらにメモリ断片化の元凶になる。quicklistは、ある程度の塊(ziplist)ごとにデータをパッキングすることで、ポインタのオーバーヘッドを劇的に削減している。
実務でのアンチパターン
「無限にログを流し込むキュー」としてListを使い、数千万件を1つのキーに溜め込む設計は最悪だ。メモリ断片化を引き起こし、LRANGEでのO(N)走査がメインスレッドをブロックする。
対策: キーを日付やIDのレンジで分割し、1つのListあたりの要素数を数千〜数万件程度にコントロールせよ。
—
④ Set型:O(1)の魅力とintsetの極限圧縮
重複のない要素を管理するSet型は、要素がすべて整数であり、かつ一定数以下の場合、`intset`という特殊なエンコーディングを採用する。
- intsetの仕組み: 整数をソートされた連続したメモリ領域に保持し、バイナリサーチで要素の有無を判定する。2バイト(INT16)、4バイト(INT32)、8バイト(INT64)のいずれかに動的にサイズが最適化される。
[INT16のintset例] -> [ 10 | 20 | 30 | 40 ] (極めてコンパクト)
注意点
Setに1つでも「文字列」が混入した瞬間、intsetから通常の`hashtable`へ強制変換される。数値IDの集合を管理するつもりが、うっかり文字列の `”123″` を混ぜてしまい、メモリが爆発したという事故をコードレビューで何度も見てきた。型の一貫性には細心の注意を払え。
—
⑤ Sorted Set (ZSet):スコープ付き集合の裏側
ランキング機能などで多用されるZSetは、`ziplist`(または `listpack`) と `skiplist` (Skip List) + `hashtable` のハイブリッド構造を持つ。
- skiplistの役割: スコア順での高速な範囲検索(`ZRANGEBYSCORE`など)をO(log N)で実現する。
- hashtableの役割: 要素の存在確認やスコアの取得をO(1)で行う。
要素数や値のサイズが小さいうちは `ziplist` で省メモリに保たれ、閾値を超えると一気に重い `skiplist` へ移行する。
ZSetのziplist昇格閾値
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
ランキングの保持数がこの数値を少し超えるだけで、メモリフットプリントが跳ね上がる。数百万ユーザーのスコアを保持する場合、この閾値設計が生死を分ける。
—
3. 実務で直面する「メモリ断片化(Fragmentation)」の恐怖
データ型をどれだけ最適化しても、OSレベルのメモリ管理機構(glibcのptmalloc等)との間でメモリ断片化が発生する。
- 症状: Redisの `INFO memory` で `used_memory` は2GBなのに、`used_memory_rss`(OSが実際に割り当てているメモリ)が8GBに達している現象。
- 対策:
1. Redis 4.0以降であれば、アクティブメモリデフラグ(Active Defragmentation)を有効化する。
# redis.conf
activedefrag yes
active-defrag-ignore-bytes 100mb
active-defrag-threshold-lower 10
active-defrag-threshold-upper 30
2. プロダクション環境のピークタイムを避け、定期的に `MEMORY PURGE` を発行する設計を組み込んでおく。
—
4. チーフアーキテクトからの設計チェックリスト
最後に、実際の設計レビューで私が必ず確認しているチェックリストを授けよう。
1. JSONをそのままStringに入れていないか?
→ フィールドアクセスが必要ならHash型を検討し、エンコーディングが `ziplist` 維持できるサイズ・件数に収まるか見積もっているか?
2. キー設計にプレフィックス階層(コロン区切り)を使っているか?
→ `user:1000:profile` のように適切に構造化し、`SCAN` やメモリ分析ツール(redis-rdb-tools等)でプロファイリングしやすいか?
3. 数値データが文字列として扱われていないか?
→ 型の混入によって `intset` や `int` エンコーディングがパージされていないか確認したか?
4. 巨大なコレクションを作っていないか?
→ 1つのList/Hash/ZSetに数百万件を詰め込まず、シャーディング(ハッシュタグの活用など)を考慮しているか?
—
結び
Redisは「メモリに入るデータなら爆速で動く」という甘い幻想を抱かせやすい。しかし、その裏側にあるエンコーディングの挙動を理解し、ハードウェアの制約とアルゴリズムの特性をコントロールして初めて「プロのエンジニアが組んだ堅牢なシステム」と言える。
次の設計からは、メモリメータの針が跳ね上がる音を想像しながら、優美かつ極限まで圧縮されたデータ構造を組み上げてくれ。期待している。
コメント