【実務・中級編】 Listpackの導入と利点 – Redis

Redis裏方技術のパラダイムシフト:なぜ今、Ziplistを捨ててListpackを語るべきなのか

システム開発の現場で、Redisを単なる「高速なキーバリューストア」や「お手軽なキャッシュ層」として捉えて設計しているうちは、その真のポテンシャルを引き出せているとは言えない。数千万から数億件のデータを持つハッシュやソーテッドセットを扱い始めた瞬間、メモリの断片化とGCの嵐に悩まされるのは、誰もが一度は通る通過儀礼だ。

Redis 5.0、そしてRedis 6.0以降において、内部データ構造の主役は静かに、しかし劇的に交代した。
それが今回解説する Listpack である。

かつてメモリ最適化の王様として君臨しつつも、致命的な「カスケード更新(Cascade Update)」というアキレス腱を持っていた `Ziplist`。その後継として生み出されたListpackは、単なるマイナーチェンジではない。「メモリ効率の極限追求」と「安全性の両立」という、インメモリデータベースの永遠のジレンマに対するRedisコアチームの回答なのだ。

テックリードとしてコードレビューを行う際、「なぜそのデータ構造を選ぶのか」「メモリレイアウトがCPUキャッシュにどう影響するか」を語れないエンジニアには、厳しくツッコミを入れたい。本稿では、Listpackの内部構造に踏み込み、実務の設計にどう活かすべきかをロジカルに紐解いていく。

—

1. なぜZiplistは「終わった」のか? —— カスケード更新の悪夢

Listpackを理解するには、まず前身である Ziplist(ziplist) の致命的な欠陥を知る必要がある。

Ziplistは、ポインタのオーバーヘッドを極限まで排除するため、すべての要素を「連続したメモリ領域」に隙間なくパッキングする構造体だ。ポインタを持たず、各エントリが「直前の要素の長さ」と「自身のデータ」を持つことで、前方・後方の両方向走査を可能にしていた。

しかし、ここに設計上の爆弾があった。
Ziplistのエントリのプレフィックス(前要素の長さ)は、サイズに応じて 1バイト または 5バイト に可変長エンコードされる。

もし、インデックスの途中に「わずかにサイズが大きい要素」が挿入・更新されたらどうなるか?
1. 直後の要素は、「直前の要素の長さ」を保持する領域(プレフィックス)の拡張を強いられる。
2. その拡張によって、その要素自体のトータルサイズも増加する。
3. その結果、さらに次の要素もプレフィックスの拡張を強いられる。
4. 連鎖反応(Cascading Update)がリストの末尾まで発生する。

最悪の場合、$O(N)$ の要素数に対し、各ステップでメモリの再割り当てとデータコピーが発生し、計算量は $O(N^2)$ に跳ね上がる。数万件のエントリを持つZiplistでこれが起きた瞬間、Redisのシングルスレッドイベントループは完全にブロックされ、P99レイテンシは跳ね上がり、死活監視のタイムアウトが連鎖する。これが、かつてのRedis運用者が恐れた「Ziplistの呪い」だ。

—

2. Listpackの構造的革新:カスケードの完全排除

このアーキテクチャ上の負債を清算するために投入されたのが Listpack である。

Listpackの最大にして最強の設計思想は、「各エントリが、自分以外の要素の長さを保持しない」 という点にある。

Listpackのメモリレイアウト

Listpackのバイナリ構造は非常にシンプルかつ洗練されている。

+——————-+———————————–+——–+
| Total Bytes (4B) | Num Elements (2B) | Entry 1 … Entry N | End(1B)|
+——————-+———————————–+——–+

個々のエントリ(Entry)の内部構造も見てみよう。

+———-+—————+——–+
| Encoding | Payload Data | Length |
+———-+—————+——–+

  • Encoding: データの型(整数か文字列か)と、そのペイロード長を示す。
  • Payload Data: 実データ。
  • Length: 「このエントリ自身の全長」 を表すメタデータ。

ここが核心だ。各エントリが保持しているのは「自身の長さ」であって、「前の要素の長さ」ではない。
そのため、あるエントリのサイズが変更されても、影響を受けるのはそのエントリの終端位置だけであり、隣接するエントリのプレフィックス長を再計算する必要が一切ない。

これにより、カスケード更新の可能性は数学的に完全に排除された。アルゴリズムの複雑性は常に $O(1)$ または安全な範囲に抑えられ、予測可能な低レイテンシが担保される。

—

3. 実務へのインプリケーション:どう設計に落とし込むか

Redis 7.0以降、Ziplistは内部実装のほとんどの場所でListpackに置き換えられた(例えば、Hash型の内部表現は完全にListpackベースに移行している)。

では、実務のシステム設計において、我々エンジニアはこの事実をどう意識すべきだろうか?

アンチパターン:不適切なデータ型の選択による「巨大なHash/ZSet」の生成

メモリ効率が良いからといって、1つのHashやSorted Setに数百万件の要素を詰め込む設計は依然としてNGだ。
Listpackはメモリ効率の面でポインタのオーバーヘッド(通常64bit環境ではリンクドリストだと1ノードあたり16〜32バイトのオーバーヘッドが生じる)を劇的に削減するが、メモリ上で「単一の連続した巨大なチャンク」として存在するため、以下のトレードオフが存在する。

1. メモリの再割り当てコスト:
Listpackが巨大化(数MB超)すると、要素の追加・更新時にRedisが大きなメモリブロックを確保し直してデータをコピーするコスト(`realloc` のオーバーヘッド)が無視できなくなる。
2. CPUキャッシュの効率と検索コスト:
Listpack内の検索は、ポインタジャンプではなく「先頭からのシーケンシャルスキャン」またはバイナリサーチ(型による)になる。要素数が数千を超えると、CPUキャッシュヒット率や走査コストがリニアに悪化する。

【設計の鉄則】

  • 1つのHash / Sorted Set / List あたりの要素数は、概ね数千〜1万件程度を上限とするよう、アプリケーション側でキーを分割(シャスディング)する。
  • Redisのコンフィグにある以下のパラメータチューニングを理解しておく(デフォルト値で十分最適化されているが、データ特性に応じて確認が必要)。

hash-max-listpack-entries 512
hash-max-listpack-value 64
zset-max-listpack-entries 128
zset-max-listpack-value 64

※ これらの閾値を超えると、よりスケーラブルなハッシュテーブルやスキップリスト構造に昇格(Promote)する。Redisはこの「小さいうちはListpackで省メモリに、大きくなったら標準構造で高速に」というハイブリッド戦略を自動で行っている。

—

4. コードレビューで使える「Redisデータ構造のチェックリスト」

実際のプロジェクトでRedisを組み込む際、レビュアーとして私は以下のポイントをチームメンバーに確認する。

1. 「巨大なリスト/ハッシュを単一キーで保持していないか?」

  • ログやイベント履歴を延々と一つのList型にLPUSHし続けていないか?(→ 時系列データならTimeSeriesモジュールや、日付・IDごとのキー分割を検討すべき)

2. 「数値データを文字列として無駄にパディングしていないか?」

  • Listpackは整数を自動的に最小のバイト数(int16, int32, int64など)に圧縮して格納する。アプリケーション側で数値を文字列(`”12345″`)として扱っていると、このバイナリ最適化の恩恵を受けそこねるケースがある。型の一致に気を配れ。

3. 「メモリ断片化(Fragmentation)を監視しているか?」

  • `INFO memory` の `mem_fragmentation_ratio` を注視せよ。Listpackは連続したメモリを好むため、頻繁な巨大要素の追加・削除はアロケータ(jemalloc)レベルでの断片化を招くことがある。

—

5. チーフアーキテクトからのメッセージ

Redisの進化は止まらない。ZiplistからListpackへの移行は、「省メモリ化」という至上命題を、システムの安定性(レイテンシの予測可能性)を犠牲にせずに達成した歴史的なマイルストーンだ。

しかし、どれほど優れたデータ構造が背後で稼働していようとも、それを呼び出すアーキテクトの設計思想が未熟であれば、システムのパフォーマンスは簡単に崩壊する。

「なぜこのデータ型を使うのか」
「メモリ上でどのようにレイアウトされ、CPUにどう負荷がかかるのか」

ここまで見通して初めて、プロフェッショナルなバックエンドエンジニアと言える。
日々の設計やコードレビューにおいて、このListpackが持つ「安全な省メモリ構造」の哲学をぜひ活かしてほしい。君たちのシステムのP99レイテンシは、そこから劇的に改善するはずだ。

コメント

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