【実務・中級編】 ストリームの永続化とトリミング – Redis

Redis Streamsの極限:メモリ爆発を防ぐ『MAXLEN』と『XTRIM』の要塞設計

チームの皆さん、お疲れ様です。テクニカルリードの私だ。

今回のコードレビューで、実時間メッセージング基盤としてRedis Streamsを採用したモジュールを確認したが……正直に言おう。このまま本番稼働させたら、数週間でOOM(Out of Memory)でノードがクラッシュし、インシデント対応で深夜に叩き起こされる未来しか見えない。

なぜか? Streamsは「無限のログ」を書けるかのように錯覚させる魔力を持っているが、Redisはメモリデータベースである。 ディスクではない。ストレージ容量が無限にあるわけがないのだ。

今回は、Redis Streamsにおけるデータの寿命管理、すなわち「MAXLENによる自動トリミング」と「XTRIMによる明示的なメモリ防衛」について、実務の現場で即座に使える極限の知見を授けよう。

—

1. Redis Streamsのデータ構造とメモリの現実

RedisのStreams(`XADD`)は、KafkaのトピックやKinesisのシャードにインスパイアされた非常に強力なアペンドオンリーログ構造だ。消費者グループ(Consumer Groups)と組み合わせることで、堅牢な非同期メッセージングパイプラインを構築できる。

しかし、ここでエンジニアが陥りがちな最大の罠がある。
「データを入れ続ける限り、メモリは肥大化し続ける」 という物理法則だ。

Kafkaであればセグメントファイルがディスクに溢れ、古いものはOSによってスワップアウト、あるいは設定された保持期間(Retention)で自動削除される。しかし、RedisのデータはすべてRAM(DRAM)上に存在する。何もしなければ、毎秒数万件のログが流れ込むストリームは、数日でRedisインスタンスのメモリ上限を突き破る。

ここで私たちが使うべき武器が、「トリミング(Trimming)」である。

—

2. `MAXLEN`:書き込みと同時にメモリを支配する

もっともエレガントな防衛策は、データ書き込み(`XADD`)の瞬間にサイズを制限することだ。`MAXLEN` オプションを使えば、ストリームの長さを常に一定の範囲に収めることができる。

正確なトリミング vs 近似トリミング

ここで設計上の重要な選択がある。`MAXLEN` には2つのモードが存在する。

1. 厳密なトリミング (`MAXLEN = N`)
指定した長さを正確に維持する。ただし、古いエントリを削除するために内部でマクロノードの再構築が発生するため、高ス負荷な環境ではCPUコスト(O(N)の削除コスト)を支払うことになる。
2. 近似トリミング (`MAXLEN ~ N`)
チルダ(`~`)を付与することで、厳密な長さを保証せず、Redisの内部マクロノードの単位(通常は約64エントリ単位)で「おおむねN件」に抑える。

【リードからの設計指針】
高スループットなシステムでは、必ず 近似トリミング (`~`) を選択せよ。CPUサイクルを無駄な厳密さに費やすな。数件の誤差など、分散システムのストリームにおいては「誤差の範囲内」だ。

近似トリミングを用いて、最大でおおむね 1000 件のエントリを保持するストリームに書き込む
127.0.0.1:6379> XADD mystream MAXLEN ~ 1000 sensor_id “A-42” temperature 23.5
“1718000000000-0”

—

3. `XTRIM`:バックグラウンドで自律的に肥大化を防ぐ

`XADD` 時にトリミングを行っていなかった場合、あるいは予期せぬトラフィック急増でストリームが膨れ上がった場合、救世主となるのが `XTRIM` コマンドだ。

ストリームの長さを厳密に 5000 件に切り詰める
127.0.0.1:6379> XTRIM mystream MAXLEN 5000

近似値で 5000 件程度に切り詰める(パフォーマンス重視)
127.0.0.1:6379> XTRIM mystream MAXLEN ~ 5000

実務での罠:消費者グループ(Consumer Groups)とのデッドロック

ここで、アマチュアのエンジニアがやりがちな致命的なミスを指摘しておこう。

`XTRIM` や `MAXLEN` で古いデータを強制削除するとき、「まだどのコンシューマーグループからも読まれていない(Pending状態になっていない、あるいは未配信の)メッセージ」が容赦なく消し飛ぶ。

もし、コンシューマーがダウンしていて、その間に `XTRIM` が走ったらどうなるか?
復旧したコンシューマーは「あれ、私の処理すべきメッセージが消えている……?」となり、データロスト(不整合)を引き起こす。

【堅牢な設計パターン】
`XTRIM` を無思考のCronジョブなどで定期実行してはならない。もしサイズ制限による自動削除を行う場合は、以下のいずれかの対策を講じよ:
1. ストリームの最大スループットとコンシューマーの処理速度を完全に計算し、コンシューマーが絶対に遅れをとらない設計にする。
2. 「時間ベース(Min-ID)」のトリミングを活用し、保持期間(例: 過去24時間分)を担保する。

例: タイムスタンプベースで古いデータを削除する(Redis 6.2以降)
ミリ秒タイムスタンプベースで、特定のIDより古いものを削除
127.0.0.1:6379> XTRIM mystream MINID ~ 1717990000000

—

4. プロダクション環境におけるメモリ管理のベストプラクティス

最後に、私がコードレビューで必ずチェックする「Redis Streams運用の鉄則」をまとめる。

1. `maxmemory` との併用は絶対条件
Streamsで `MAXLEN` を入れていても、予期せぬキーの増加や他のデータ構造(HashやString)の肥大化でRedis全体のメモリが枯渇することがある。`redis.conf` の `maxmemory` を適切に設定し、`maxmemory-policy` は `noeviction` ではなく `volatile-lru` や `allkeys-lru`(Streamsをメインにするなら注意が必要だが)を検討、あるいはメモリ上限に達した際に書き込みを拒否してアラートを飛ばす設計にせよ。
2. IDの単調増加性を理解する
Redis StreamsのID(例: `1718000000000-0`)はミリ秒単位のタイムスタンプを含む。古いデータを削除しても、新しいデータのIDは常に未来へ進む。この特性を利用して、時間軸に基づいたクレンジング設計を心がけよう。
3. モニタリングの徹底
`XINFO STREAM mystream` コマンドを定期的に叩き、現在の `length`(エントリ数)や `radix-tree-keys`、そして最も古いエントリのタイムスタンプをメトリクスとして収集しろ。可観測性(Observability)のないストリーム運用は、目隠しで爆弾処理をするようなものだ。

—

結びにかえて

アーキテクチャとは、「動くものを作る技術」ではなく、「壊れないもの、スケールするものを作る哲学」だ。

今回解説した `MAXLEN` と `XTRIM` は、単なるコマンドの使い方の話ではない。「有限のハードウェアリソースの上で、無限のデータの流れをどう調停するか」という、インフラエンジニアリングの核心そのものである。

本日のレビュー対象コードの修正を求める。

  • すべての `XADD` に適切な `MAXLEN ~` が付与されていること。
  • メモリ上限を超えた場合の挙動がテストされていること。

以上だ。次のコミットを楽しみにしている。

コメント

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