【実務・中級編】 PUBLISHコマンド – Redis

Redis PUBLISHの深層:その「1行の送信」がシステム全体をどう揺るがすか

こんにちは。テックリードの私だ。
今日のコードレビューで、あるジュニアエンジニアが書いたPub/Subのコードを見つけた。「チャネルにイベントを流すだけだから、`PUBLISH`を叩けば終わりですよね?」と彼は言った。

……甘い。非常に甘い。

Redisの `PUBLISH` は、単なる「メッセージの送信」ではない。クライアントライブラリのバッファ溢れ、スローサブスクライバー問題、そしてクラスタ環境におけるネットワークトポロジの負荷まで、分散システムの地雷原をノーガードで歩くようなものだ。

今回は、Redisのデータ構造とI/Oの核心を知り尽くしたアーキテクチャの視点から、`PUBLISH` コマンドの真の挙動、そして実務で絶対に踏み抜いてはならない設計パターンを伝授する。

—

1. `PUBLISH` の裏側:O(N) の残酷な現実

まず、Redisのドキュメントを思い出してほしい。`PUBLISH` コマンドの計算量はどう定義されているか?

> Time complexity: `O(N+M)` where `N` is the number of clients subscribed to the channel and `M` is the number of total matched patterns subscribed by the clients.

そう。`PUBLISH` はO(1)ではない。
指定されたチャネルを購読しているクライアント(Subscriber)の数、そしてパターンマッチング(`PSUBSCRIBE`)でそのチャネルにヒットする購読者の数に比例して、CPUコストとネットワークI/Oの負荷が増大する。

Redisの内部配信メカニズム

Redisのシングルスレッドイベントループ内において、`PUBLISH` が実行されると何が起きるか?

1. 辞書ルックアップ: チャネル名(文字列)をキーとして、内部のサブスクライバリスト(ハッシュテーブル / リスト)を O(1) で引く。
2. メモリバッファへの書き込み (Fan-out): 該当するチャネルを購読しているすべてのクライアント接続の出力バッファ(Output Buffer)に、メッセージのペイロードをコピーする。

ここでボトルネックになるのが「出力バッファ」だ。

—

2. 実務の罠:スローサブスクライバー問題とメモリ枯渇

もし、1万台のワーカーが `SUBSCRIBE` しており、そのうちの1台がGCの停止やネットワークの遅延によってメッセージの処理が追いつかなくなったとする。

RedisはノンブロッキングI/Oで動作するため、クライアントへの書き込みが追いつかない場合、そのクライアント用の出力バッファをメモリ上に肥大化させ続ける。

クライアント出力バッファ制限(Client Output Buffer Limits)

Redisには、これを防ぐための安全装置がある。`redis.conf` の設定を見てほしい。

pubsub クライアントに対する制限設定の例
client-output-buffer-limit pubsub
client-output-buffer-limit pubsub 32mb 8mb 60

  • Hard Limit: 出力バッファが 32MB を超えた瞬間、Redisはその接続を強制切断(強制Kill)する。
  • Soft Limit: 8MB を超えた状態が 60 秒継続した場合に強制切断する。

`PUBLISH` を安易に高頻度で叩き、サブスクライバー側の処理が詰まると、「たった1台の遅いワーカーのせいで、Redis全体のメモリが圧迫され、他の健全な接続まで強制切断される」というドミノ倒しが発生する。これが、Pub/Sub設計における最大のアンチパターンだ。

—

3. 実践:堅牢な `PUBLISH` の使い方とエラーハンドリング

では、Python (Redis-Py) を使った堅牢なパブリッシャーの実装例を見てみよう。単に `r.publish()` を呼ぶだけのコードは、本番環境では通用しない。

import redis
import logging
from redis.exceptions import RedisError

ロガーの設定
logger = logging.getLogger(“OrderSystem”)

class RobustPublisher:
def __init__(self, redis_client: redis.Redis):
self.client = redis_client

def publish_order_event(self, channel: str, message: str) -> int:
“””
堅牢なパブリッシュ処理
戻り値: メッセージを受信したサブスクライバの数
“””
try:
# PUBLISHコマンドの実行
# 戻り値として「何台のクライアントに配信されたか」が返ってくる
subscriber_count = self.client.publish(channel, message)

if subscriber_count == 0:
logger.warning(f”Channel ‘{channel}’ had no subscribers. Message dropped into void.”)

return subscriber_count

except RedisError as e:
# ネットワーク切断、タイムアウト、クラスタのフェイルオーバー中のエラーなどを捕捉
logger.error(f”Failed to publish message to {channel}: {e}”)
# ここでサーキットブレーカー発動やフォールバック処理を記述する
raise

— 使用例 —
if __name__ == “__main__”:
pool = redis.ConnectionPool(host=’localhost’, port=6379, decode_responses=True)
r = redis.Redis(connection_pool=pool)

publisher = RobustPublisher(r)

# イベント発火
# 戻り値(subscriber_count)をモニタリングのメトリクスとして利用する知見を持て
receivers = publisher.publish_order_event(“orders:created”, ‘{“order_id”: 12345, “amount”: 5800}’)
print(f”Message successfully delivered to {receivers} subscribers.”)

チーフアーキテクトからの知見:戻り値を監視せよ

`PUBLISH` コマンドが返す整数値(サブスクライバ数)をログに記録するか、メトリクス(Prometheus等)として外に出すこと。
もしこの数値が意図せず `0` になり続けた場合、それは購読側(コンシューマー)のアプリケーションがクラスタ全体で死んでいることを意味する。死活監視の早期発見シグナルとして極めて有効だ。

—

4. Redis Cluster における `PUBLISH` の仕様の罠

ここで、システムをスケールアウトさせたときの致命的な落とし穴について話しておこう。
多くのエンジニアが勘違いしていることだが、Redis Cluster において、`PUBLISH` はクラスタ全体にブロードキャストされる。

Redis Clusterはデータを 16,384 のハッシュスロットに分割して保持しているが、Pub/Subのチャネルはハッシュスロットに依存しない。

  • あるノードに対して `PUBLISH channel_A “hello”` を実行すると、そのノードはそのメッセージをクラスタ内のすべてのマスター・レプリカノードへ転送(Gossip/Cluster bus経由)する。
  • 結果として、クラスタ内のどのノードに接続して `PUBLISH` を叩いても、全ノードで購読しているクライアントへメッセージが届く。

設計上の注意:

クラスタのノード数が増え、かつ `PUBLISH` のスループットが数万QPSを超えてくると、Redis Clusterの内部ネットワーク(Cluster Bus)がPub/Subのトラフィックだけで飽和する。
もしビッグデータや高頻度のストリーミングデータをRedisのPub/Subで処理しようとしているなら、今すぐその設計を捨て、Redis Streams や Kafka への移行を検討すべきだ。

—

5. まとめ:プロフェッショナルとしてどう設計すべきか

Redisの `PUBLISH` は、軽量で即時性の高いイベント通知(Websocketのバックエンド、キャッシュ無効化の伝播、簡易的なタスクのトリガーなど)において圧倒的なパフォーマンスを発揮する。しかし、以下の鉄則を破ってはならない。

1. メッセージの永続性は求めない: RedisのPub/Subは「Fire-and-forget(撃ちっ放し)」。サブスクライバがオフラインの間に流れたメッセージは永遠に消える。
2. ファンアウトの規模を見積もる: 購読者数が数千を超えるようなシステムで単純な `PUBLISH` を使うな。出力バッファの肥大化による障害を招く。
3. 高スループットなら Streams を使え: メッセージの永続化、コンシューマーグループによるロードバランス、オフセット管理が必要な要件であれば、`PUBLISH/SUBSCRIBE` ではなく `XADD/XREADGROUP`(Redis Streams)を採用すること。

道具の特性を正しく理解し、限界点を見極めた上でコードを書く。それこそが、我々エンジニアが守るべきプロフェッショナリズムだ。

今日のレビューはここまでにする。さて、君のコードの修正版を見せてもらおうか。

コメント

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