Redisの裏側を暴け:`OBJECT`コマンドが教えてくれるメモリ最適化の極意
テックリードの私だ。コードレビューやアーキテクチャ設計をしていると、「動くけれど、裏側のコストが見えていないコード」にしばしば遭遇する。
「とりあえずStringで保存して、後からJSONでパースすればいいや」
「有効期限(TTL)をつけておけば、メモリ溢れはないだろう」
甘い。これでは大規模トラフィックを捌くシステムにおいて、ある日突然のOOM (Out of Memory) キルや、レイテンシの悪化という名の「技術的負債のしっぺ返し」を食らうことになる。
Redisはただの「速いKey-Valueストア」ではない。C言語のメモリ管理の妙技と、データ構造の変幻自在な最適化(Encoding)の上に成り立っている精緻なエンジンだ。その内部状態を暴き、我々の設計が正しくメモリ効率とパフォーマンスに寄与しているかを検証するための最も強力な武器が、`OBJECT`コマンドである。
今回は、この`OBJECT`コマンドを軸に、Redisのデータ構造の裏側と、実務で絶対に押さえておくべきメモリ設計の知見を授けよう。
—
1. `OBJECT`コマンドとは何か? なぜデバッグの域を超えて必須なのか
`OBJECT`コマンドは、一言で言えば「キーのメタデータ監査ツール」だ。
通常のアプリケーションコードから頻繁に呼び出すものではないが、キャパシティプランニング、メモリリークの調査、そしてデータ構造の選定ミス(アンチパターン)を発見するための決定版と言える。
主なサブコマンドは以下の3つだ。
1. `OBJECT ENCODING
2. `OBJECT REFCOUNT
3. `OBJECT IDLETIME
これらを使いこなすことで、Redisが裏側で何をやっているのかが手に取るようにわかる。それぞれの実務的な意味を深掘りしていこう。
—
2. `OBJECT ENCODING`: メモリ最適化の分水嶺
Redisのデータ型(String, List, Hash, Set, ZSetなど)は、抽象的なインターフェースに過ぎない。実体がメモリ上でどう表現されているか(Encoding)は、データ量や設定によって動的に変化する。
例えば、String型を見てみよう。
整数値をセット
127.0.0.1:6379> SET user:100:score 9950
OK
エンコーディングを確認
127.0.0.1:6379> OBJECT ENCODING user:100:score
“int”
おっと、文字列型に入れたはずなのに `”int”` と返ってきた。
Redisは、値が64歳の符号付き整数(`-9223372036854775808` から `9223372036854775807`)の範囲に収まる場合、わざわざ文字列としてヒープ上に確保せず、整数そのもの(Integer Encoding)として保持する。さらに、0から9999までの整数は「Shared Integer」としてあらかじめメモリ上にプーリングされており、メモリ消費量は実質ゼロに近い。
では、文字列長を増やしてみる。
長めの文字列をセット
127.0.0.1:6379> SET user:100:token “d3b07384d113edec49eaa6238ad5ff00”
OK
127.0.0.1:6379> OBJECT ENCODING user:100:token
“embstr”
`”embstr”` と出た。これは Embedded String の略だ。Redisのオブジェクトヘッダ(`robj`)と実際の文字列データ(`sds`)を単一の連続したメモリブロックとして確保する方式である。メモリ断片化(Fragmentation)を防ぎ、キャッシュヒット率を爆上げするための珠玉の最適化だ。
しかし、この文字列が一定の長さを超えると……
さらに長い文字列(通常は44バイトラップ)をセット
127.0.0.1:6379> SET user:100:payload “{\”id\”:100,\”name\”:\”Alice\”,\”roles\”:[\”admin\”,\”user\”],\”metadata\”:{\”login_count\”:42}}”
OK
127.0.0.1:6379> OBJECT ENCODING user:100:payload
“raw”
`”raw”` に変わった。これは通常の動的文字列(SDS: Simple Dynamic String)であり、メモリ上の別の場所に実体がポインタで紐づく形になる。
Hash型やZSet型における「内部昇格(Promotion)」の恐怖
このEncodingの動的変化は、Complexなデータ構造(HashやZSetなど)でより顕著になる。
初期状態ではメモリ効率の極限である `ziplist`(または `listpack`) という圧縮された連続メモリ領域で保持されていたものが、要素数やサイズが設定値(`hash-max-ziplist-entries` など)を超えた瞬間、通常の `hashtable` や `skiplist` に強制昇格(Promotion)する。
この昇格が起きた瞬間、メモリ消費量が数倍に跳ね上がることがある。
「テスト環境では数MBだったキャッシュが、本番のデータ量になった途端にメモリを食いつぶしてノードがダウンした」という障害の多くは、このエンコーディングのサイレントな切り替わりを見落としたことが原因だ。
設計時には必ず、本番相当のデータを投入した状態で `OBJECT ENCODING` を確認し、意図した省メモリ構造(`ziplist` / `listpack` / `intset`)に収まっているかを検証すべきである。
—
3. `OBJECT REFCOUNT`: メモリ共有とガベージコレクションの仕組み
次に `REFCOUNT` だ。
127.0.0.1:6379> SET system:mode “maintenance”
OK
127.0.0.1:6379> OBJECT REFCOUNT system:mode
(integer) 1
通常は `1` だ。しかし、Redis内部では、メモリ効率を最大化するためにオブジェクトの共有(Shared Objects)が行われることがある。
先ほど触れた `0` から `9999` までの整数値は、すべてのキーから共有されるため、`REFCOUNT` は数千、数万という数字叩き出すことになる。
また、実務においてこの `REFCOUNT` がヒントになるのは、Luaスクリプトの実行や、特定のメモリ最適化構成を検証する時だ。
アプリケーション側で「巨大なJSON文字列やリストを複数のキーから参照させたい」といった安易な設計(RDBの外部キー的な発想)をRedisでやろうとしても、Redisはキー・バリューのフラットなストアであり、ポインタの共有は内部の最適化(Shared Integerや子プロセスのCopy-on-Writeなど)を除き、アプリケーションレベルでは制御できない。
もし参照カウントの挙動やメモリ共有に違和感を覚えたら、それはデータモデリングのアンチポータンの赤信号だと受け取ってほしい。
—
4. `OBJECT IDLETIME` と `LFU`: 古いキャッシュを駆逐するロジック
キャッシュサーバーとしてのRedisを運用する上で避けて通れないのが、メモリ上限(`maxmemory`)に達したときのEviction(退避)ポリシーだ。
ここで役立つのが `OBJECT IDLETIME` である。
127.0.0.1:6379> SET session:999 “active_data”
OK
(しばらく放置したあと……)
127.0.0.1:6379> OBJECT IDLETIME session:999
(integer) 342
最後にアクセス(READ/WRITE)されてからの秒数が返る。これを利用することで、「どのキーがゾンビ化しているか(誰もアクセスしていないのにメモリを占有しているか)」を特定できる。
注意点:LRUからLFUへの進化を見据えろ
Redis 4.0以降、従来のLRU(Least Recently Used:最近使われていないもの)に加え、LFU(Least Frequently Used:アクセス頻度が低いもの)が導入された。
実は、`OBJECT IDLETIME` はLRUモード時のアイドル時間を返す。もしRedisの設定が `volatile-lfu` や `allkeys-lfu` になっている場合、内部ではアイドル時間だけでなく「アクセスの頻度(Counter)」も組み合わせて評価されている。
もし `OBJECT IDLETIME` が長いのに、なぜかメモリから消えないキーがあるとしたら、それは「頻繁にアクセスはされていないが、アクセスされたときの重要度(頻度カウンタの減衰アルゴリズム)」が絡んでいる可能性がある。
デバッグ時は、Redisのコンフィグ(`maxmemory-policy`)と `OBJECT` コマンドの出力をセットで確認する習慣をつけてほしい。
—
5. テックリードからの提言:実務での設計・運用チェックリスト
最後に、現場のコードレビューやアーキテクチャ設計で私が必ず確認しているチェックリストを授けよう。
1. データ型の選定は適切か?
- 「とりあえずJSONをStringで保存」していないか? フィールドの一部しか更新しないなら、`Hash` 型を使い、さらに `hash-max-ziplist-entries` の制限内に収まる設計にしているか?
2. エンコーディングの意図せぬ爆発(昇格)を防いでいるか?
- 本番データ量を見据え、`OBJECT ENCODING` で `ziplist` / `listpack` が維持されているかをストレステスト時に検証したか?
3. メモリ上限とEvictionポリシーの整合性は取れているか?
- `maxmemory` を無制限にしていないか? アプリケーションの特性(セッションストアなのか、純粋なキャッシュなのか)に合わせて `volatile-lfu` や `allkeys-lru` を適切に選択しているか?
—
結び
`OBJECT`コマンドは、日々のアプリケーションコードを書く中ではめったに使わない。しかし、「なぜこのRedisクラスターはメモリ効率が良いのか」「なぜこのクエリはレイテンシが安定しているのか」を突き詰めるプロフェッショナルエンジニアにとって、内部構造を透視するX線のような存在だ。
道具の裏側の仕組みまで理解し、コントロールすること。それこそが、障害に強く、スケールするシステムを作り上げる唯一の道である。
さあ、今すぐstaging環境のRedisに繋ぎ、主要なキーの `OBJECT ENCODING` を叩いてみるんだ。君の設計の「本当の姿」がそこにあるはずだ。
コメント