Redis Pub/Sub徹底解剖:`PUBSUB`コマンドが握る、オブザーバビリティの生命線
アーキテクトの佐藤だ。本日のコードレビューは、システム全体の神経系とも言える「Redis Pub/Subサブシステム」のモニタリング設計についてだ。
「とりあえずメッセージをパブリッシュして、購読すれば動く」――そんな表面的な理解で実装されたコードを、私は何度もプロダクション環境の障害で見てきた。
メッセージがどこで滞留しているのか、ゾンビ化したクライアントがどれだけリソースを食いつぶしているのか。それらを可視化できなければ、あなたのシステムは「ブラックボックス」を抱えたまま爆弾を抱えて走っているようなものだ。
今回は、Pub/Subの内部状態を丸裸にする`PUBSUB`コマンド群(`CHANNELS`, `NUMSUB`, `NUMPAT`)に焦点を当て、実務の現場で生き残るための堅牢な設計と運用知見を授けよう。
—
1. そもそもRedis Pub/Subの「裏側」で何が起きているのか?
RedisのPub/Subは、KVSとしてのデータ永続化やトランザクションとは完全に切り離された、「ファイア・アンド・フォーゲット(送信して忘れる)」の非同期通信機構だ。
内部的には、クライアントがチャネルを購読(`SUBSCRIBE`)すると、Redisサーバー内のメモリ上にグローバルなハッシュテーブルとしてチャンネル名とクライアントのリストが保持される。メッセージがパブリッシュ(`PUBLISH`)されると、そのチャンネルを購読している全クライアントの出力バッファ(Output Buffer)へ直線的にデータがコピーされる。
ここでエンジニアが絶対に知っておくべき残酷な事実がある:
- メッセージの永続性ゼロ: 購読していない瞬間に飛んだメッセージは永遠に消える。
- クライアントバッファ溢れによる切断: 購読者の処理が追いつかず、出力バッファが制限(`client-output-buffer-limit`)を超えると、Redisは容赦なくその接続を切断する。
この「目に見えない動脈」の健康状態を診断するための聴診器が、他ならぬ `PUBSUB` コマンドなのだ。
—
2. `PUBSUB` サブコマンドの核心と実務での使いどころ
`PUBSUB` は単体のコマンドではなく、内部状態を多角的に覗き見るためのディスパッチャブルなコマンド群だ。
① `PUBSUB CHANNELS ` :今、何がアクティブなのか?
現在アクティブな(=1つ以上のサブスクライバーが存在する)チャンネルのリストを取得する。パターンマッチング(“, `?` など)も可能だ。
アクティブなチャンネルを全取得
127.0.0.1:6379> PUBSUB CHANNELS
1) “chat:room:101”
2) “notifications:user:”
“chat:” で始まるチャンネルに絞り込む
127.0.0.1:6379> PUBSUB CHANNELS chat:
1) “chat:room:101”
【テクニカルリードの視点】
本番環境での安易な `PUBSUB CHANNELS ` の叩きすぎに注意せよ。このコマンドの計算量は $O(N)$($N$ はアクティブなチャンネルの総数)。数百万のチャンネルが存在する巨大なクラスタでこれを高頻度で実行すると、シングルスレッドのRedisがブロッキングを起こし、レイテンシが跳ね上がる。モニタリングツールでポーリングする際は、必ずインターバルを適切に取れ。
—
② `PUBSUB NUMSUB [channel …]` :熱量を測る
指定したチャンネルを現在購読しているクライアントの数を返す。
127.0.0.1:6379> PUBSUB NUMSUB chat:room:101 chat:room:999
1) “chat:room:101”
2) (integer) 3 # 3つのクライアントが購読中
3) “chat:room:999”
4) (integer) 0 # 誰も購読していない(=メッセージは誰にも届かない)
5) “chat:room:999” が存在しない場合でも、結果は返る
【現場のユースケース】
「メッセージを流したのに、誰も受け取っていない」というサイレント障害を防ぐためのヘルスチェックに使える。バッチ処理やワーカー起動の完了確認として、自前で `NUMSUB` を叩いて購読者が1以上であることを確認してからメインのロジックを走らせる、といった堅牢な初期化フローが組める。
—
③ `PUBSUB NUMPAT` :パターンマッチ購読の負荷を暴く
`PSUBSCRIBE` を使って、ワイルドカード(パターン)ベースで購読を行っているクライアントの「パターン数」を返す。チャンネルごとの数ではない点に注意せよ。
127.0.0.1:6379> PUBSUB NUMPAT
(integer) 5
【アーキテクトの警告:パターン購読の罠】
`PSUBSCRIBE` は強力だが、設計を誤ると地獄を見る。
Redisはメッセージがパブリッシュされると、通常のチャンネル名との一致だけでなく、登録されているすべてのパターン(`PSUBSCRIBE`)との照合をO(N)(Nはパターン数)で行う。
`NUMPAT` で返る数値が異常に高い場合、システム全体でワイルドカード購読が乱立しており、CPU使用率を不当に押し上げている可能性が高い。コードレビューでは「本当に `PSUBSCRIBE` が必要か? 完全一致の `SUBSCRIBE` に置き換えられないか?」を厳しく問いただしてほしい。
—
3. 堅牢な設計パターン:Pub/Subとオブザーバビリティの融合
「作って終わり」のシステムにしないために、実務でどうこれらのコマンドを組み込むべきか。私が推奨する設計パターンを提示する。
パターンA:デッドレター・プレチェック監視
Pub/Subはメッセージのロストが起きる前提で設計すべきだが、重要なイベント通知などでは「購読者が0人なのにパブリッシュし続ける」という無駄打ちを検知したい。
import redis
r = redis.Redis(host=’localhost’, port=6379)
def publish_with_guard(channel: str, message: str):
# NUMSUBで購読者数をチェック
numsub_result = r.pubsub_numsub(channel)
subscriber_count = numsub_result.get(channel, 0)
if subscriber_count == 0:
# 警告ログの出力、あるいはフォールバック処理(Streamへの切り替えなど)
print(f”[WARN] Channel {channel} has no subscribers. Message dropped or needs fallback.”)
return False
r.publish(channel, message)
return True
パターンB:メトクス収集デーモン(Promexporter等への統合)
定期的に `PUBSUB CHANNELS` と `PUBSUB NUMSUB` を非同期で集計し、DatadogやPrometheusにメトリックとして流し込む。
- `redis.pubsub.channels.active` (Gauge)
- `redis.pubsub.subscribers.count` (Gauge per channel)
- `redis.pubsub.patterns.count` (Gauge)
これにより、「夜間帯に特定のワーカーチャンネルの購読数がゼロに落ち込んでいる(=クラッシュしている)」といった異常を、ユーザーからのクレームの前に検知することが可能になる。
—
4. チーフアーキテクトからの最終提言
RedisのPub/Subは、その圧倒的なシンプルさとスピードゆえに、安易に使われがちだ。しかし、メッセージの配送保証が必要な領域においては、Redis Streams などのモダンなデータ構造を使うべきケースが多々ある。
もしあなたが現在進行形のプロジェクトで Pub/Sub を採用しているなら、以下の問いに即答できなければならない:
1. 「今、この瞬間にいくつのチャンネルがアクティブで、誰が購読しているか」を把握できているか?
2. `PSUBSCRIBE` の乱用による CPU 枯渇のリスクをコントロールできているか?
`PUBSUB` コマンド群は、単なるデバッグの道具ではない。「Redisの動脈の脈拍を測る医療機器」である。これを使いこなせずして、プロダクションの信頼性を語ることは許されない。
次のレビューでは、君たちのコードに適切なメトリック収集とガードロジックが組み込まれていることを期待する。健闘を祈る。
コメント