Redis Pub/Subの深淵:`PUBSUB`コマンド群が暴く内部トポロジーと限界
Redisを単なる「高速なインメモリキャッシュ」として扱っているうちは、その真価の半分も見えていない。Redisの本質は、シングルスレッドのイベントループが極限までチューニングされた、極小フットプリントのデータ駆動型エンジンにある。
中でも `PUBSUB` サブシステムは、ブローカーとしてのRedisを観測するための極めて重要な窓口だ。`CHANNELS`、`NUMSUB`、`NUMPAT`。これらは単なるデバッグ用のユーティリティではない。大規模分散システムにおいて、ノード間のメッセージングの健全性を担保し、メモリ空間の歪みを検知するためのスキャナーである。
今回は、この `PUBSUB` コマンド群の背後にあるC言語レベルのデータ構造、メモリフットプリントの現実、そして実運用で踏み抜く地雷のメカニズムを、アーキテクトの視点から完全解剖する。
—
1. 内部アーキテクチャ:Pub/Subはどこでどう動いているのか
RedisのPub/Subエンジンは、データベースのキースペース(Keyspace)とは完全に独立したメモリ空間に存在する。キー・バリューのストレージが `dict` 構造体(ハッシュテーブル)をベースにしているのに対し、Pub/Subのルーティングは以下の2つの異なるデータ構造によって支配されている。
1. チャンネル購読 (`CHANNELS` / `NUMSUB`):
グローバルな `pubsub_channels` ハッシュテーブル。キーはチャンネル名(`robjects`)、値はそのチャンネルを購読しているクライアント (`client` 構造体) の連結リスト(LinkedList)である。
2. パターン購読 (`NUMPAT`):
`pubsub_patterns` リポ(LinkedList)。各要素はパターン文字列とクライアントのペアを保持する構造体であり、ワイルドカード(“, `?` など)のマッチング評価に使われる。
このアーキテクチャにおいて、メッセージがパブリッシュされた (`PUBLISH` コマンド) 瞬間に何が起きるか?
Redisは単一のスレッドで動くため、`pubsub_channels` のハッシュマップから該当チャンネルをO(1)で引き当て、紐づくクライアントリストをイテレートし、各クライアントの出力バッファ(Output Buffer)にペイロードを書き込む。
この「出力バッファへのアペンド」こそが、のちほど解説する地雷の源泉となる。
—
2. `PUBSUB` サブコマンド群の低レイヤ実態
`PUBSUB` コマンドは、この隠されたトポロジーを安全に覗き見するためのインターフェースだ。それぞれのコマンドが内部で何をやっているのか、計算量(Big-O)とともに解剖する。
`PUBSUB CHANNELS `
アクティブな(=少なくとも1つ以上の購読者がいる)チャンネルの一覧を返す。
- 内部挙動: `pubsub_channels` ハッシュテーブルの全走査(Full Table Scan)。パターンが指定された場合は、簡易的なグロブパターンマッチングが適用される。
- 計算量: $O(N) + O(M)$ ($N$ はハッシュテーブルのスロット数、$M$ はマッチしたチャンネル数)。
- アーキテクトの警告:
プロダクション環境で `PUBSUB CHANNELS` を安易に叩いてはならない。数百万のユニークなチャンネルが存在する巨大なクラスターでこれを実行すると、シングルスレッドのRedisイベントループが数ミリ秒〜数百ミリ秒にわたって完全にブロックされる。O(N) のハッシュ全走査は、高ス負荷時には致命傷になる。
`PUBSUB NUMSUB [channel …]`
指定されたチャンネルごとの現在の購読者数を返す。
- 内部挙動: 引数で渡された各チャンネル名について、`pubsub_channels` ハッシュテーブルをO(1)でルックアップし、そこに繋がれているクライアントリストの長さを返す。
- 計算量: $O(N)$ ($N$ は指定されたチャンネルの数)。
- 実践的知見:
`CHANNELS` と異なり、こちらはターゲットを絞ったO(1)のルックアップの集合体であるため、コストは予測可能かつ低い。モニタリングスクリプトやヘルスチェックでチャンネルの生死や負荷分散の偏りを確認するためには、こちらを使うべきだ。
`PUBSUB NUMPAT`
システム全体でのパターン購読(Pattern Subscriptions)の総数を返す。
- 内部挙動: `pubsub_patterns` リストのノード数を返すだけ。
- 計算量: $O(1)$。
- 深層知見:
なぜチャンネル数(`NUMSUB` で個別に調べる必要がある)に対して、パターン数だけが単一の整数としてO(1)で即座に取れるのか? それはRedis内部でパターン購読の総数がカウンターとして直接メンテナンされているからだ。パターンマッチングはメッセージ配信時に全クライアントのパターンリストを線形探索するため、コストが高い。この `NUMPAT` で常にその数(=システムにどれだけ重い負荷要因が潜んでいるか)を監視しておく必要がある。
—
3. 実践:コマンドの挙動とメモリの現実
実際にクライアントを接続し、内部がどう変動するかを追ってみよう。
ターミナルA: クライアント1がチャンネル ‘events.feed’ を購読
127.0.0.1:6379> SUBSCRIBE events.feed
Reading messages… (press Ctrl-C to quit)
1. “subscribe”
2. “events.feed”
3. (integer) 1
ターミナルB: 別のクライアント2がワイルドカードでパターン購読
127.0.0.1:6379> PSUBSCRIBE events.
Reading messages… (press Ctrl-C to quit)
1. “psubscribe”
2. “events.”
3. (integer) 1
ターミナルC: 管理用クライアントからPUBSUBコマンド群を実行
管理用クライアント(ターミナルC)での観測結果:
1. アクティブなチャンネルの列挙
127.0.0.1:6379> PUBSUB CHANNELS
1) “events.feed”
2. 特定チャンネルの購読者数を確認
127.0.0.1:6379> PUBSUB NUMSUB events.feed
1) “events.feed”
2) (integer) 1 # チャンネル購読者数 (SUBSCRIBEしたクライアント数)
3. パターン購読の総数を確認
127.0.0.1:6379> PUBSUB NUMPAT
(integer) 1 # パターン購読を行っているアタッチメント数 (PSUBSCRIBEの総数)
この結果から重要な事実がわかる。`NUMSUB` は `SUBSCRIBE` による直接の購読数をカウントするが、`PSUBSCRIBE` による間接的な購読者は `NUMSUB` のカウントには含まれない。`events.feed` にメッセージが飛んだ際、実際にはクライアント1とクライアント2の両方に配信されるが、`NUMSUB events.feed` は `1` を返す。この仕様の乖離を見落とすと、メッセージングのトラフィック計算で痛い目をみる。
—
4. 限界突破:Pub/Sub運用におけるアーキテクチャの罠
最後に、RedisのPub/Subをプロダクションで運用する上で避けて通れない「ハードリミット」について言及する。
1. スローコンシューマ問題 (Slow Consumer) とメモリ暴走
前述の通り、Pub/Subのメッセージは、購読しているクライアントの出力バッファに蓄積される。もしクライアントのネットワーク帯域が細い、あるいはアプリケーション側の処理が詰まっていてTCPウィンドウサイズがゼロになった場合、Redis内部のそのクライアント向け出力バッファは無限に肥大化する。
結果として、何が起きるか?
- Redisのメモリ使用量が急増し、`maxmemory` 制限に到達する。
- 最悪の場合、OOM KillerによってRedisプロセスそのものが強制終了させられる。
- もしくは、`client-output-buffer-limit pubsub` の設定値に抵触し、Redis側から強制切断される。
アーキテクチャ的対策:
必ず `redis.conf` で以下のようなハード/ソフトリミットを明示的に設定し、詰まったクライアントを強制排除する設計にしなければならない。
pubsubクライアントの出力バッファが32MBを超えるか、
8MB以上が60秒間継続した場合に強制切断する
client-output-buffer-limit pubsub 32mb 8mb 60
2. クラスタ環境(Redis Cluster)におけるPub/Subの罠
Redis Clusterを構築した際、Pub/Subはクラスタ全体にどう振る舞うか?
すべての `PUBLISH` は、クラスタ内のすべてのノードにブロードキャストされる。
つまり、10台のシャードを持つRedis Clusterで1つのメッセージを `PUBLISH` すると、メッセージは全ノードに伝搬し、各ノードに接続している購読者に向けて配信される。
「シャーディングによって負荷が分散される」というRedis Clusterのメリットが、Pub/Subに関しては完全に逆効果(メッセージ爆発:Message Amplification)として働く。
大規模なイベント駆動システムを構築する際、高頻度なストリーミングをRedisのPub/Subで実装するのは悪手である。その領域が必要であれば、Pub/Subではなく Redis Streams (`XADD`, `XREADGROUP`) を採用すべきだ。Streamsであれば、パーティショニング、コンシューマグループ、永続化の恩恵を受けつつ、安全なメッセージ配送が可能になる。
—
結言
Redisの `PUBSUB` コマンド群は、システムの状態を正確に把握するためのレーダーである。しかし、レーダーが捉えるのは、シングルスレッドの限界点すれすれで疾走するメッセージングエンジンの「現実」に他ならない。
内部データ構造の非効率性(`CHANNELS` の $O(N)$)、出力バッファの物理的制約、そしてクラスタ全体のブロードキャスト特性。これらを完全に理解しコントロールできて初めて、あなたは真のRedisアーキテクトと名乗ることができる。基盤の挙動に甘えず、常にメモリとCPUの境界線を意識した設計を心がけよ。
コメント