Redis Pub/Subの内部構造と、実務で絶対に踏んではいけない「地雷」
こんにちは。チーフアーキテクトの私だ。
今日のコードレビューで、若手エンジニアがこんな設計を持ってきちゃってね。「リアルタイム通知基盤として、RedisのPub/Subを導入しました!」と。
……待て待て、と。動くには動くだろう。だが、「メッセージが消えても困らない」「コンシューマーが落ちて取りこぼしても自己責任」という前提を、君たちは本当に理解してこのアーキテクチャを選んだのか?
RedisのPub/Subは、魔法の杖ではない。極めてシンプルで高速な反面、その裏側の「割り切った設計思想」を理解せずに本番投入すれば、障害時にデータが蒸発し、夜中にPagerDutyが鳴り響くことになる。
今日は、RedisのPub/Subの内部構造を剥ぎ取り、実務でどう使い、どう避けるべきかをロジカルに叩き込んでいこう。
—
1. 内部構造:なぜRedisのPub/Subはあんなに速いのか?
まず大前提として、RedisのPub/Subは「ステートレス(状態を持たない)」だ。KVS(Key-Value Store)でありながら、Pub/Subのメッセージはメモリ上のどこにも永続化されない。
チャンネルベース・メッセージングの裏側
内部で何が起きているか? 仕組みは驚くほどプリミティブだ。
Redisサーバー内には、チャンネル名をキーとし、そのチャンネルを購読しているクライアントのリスト(ポインタ)を値とするハッシュテーブル(Dictionary)が存在する。
[Redis Server 内のメモリ構造]
チャンネル名 “notifications” ──> [ Client A ] [ Client B ] [ Client C ]
1. パブリッシャーが `PUBLISH notifications “hello”` を叩く。
2. Redisは単に `”notifications”` というキーをハッシュテーブルから引き、そこに紐づくクライアントのリストをイテレート(走査)して、それぞれのソケットバッファに直接データを書き込む。
3. これだけだ。ディスクI/Oもなければ、トランザクションのログも書かない。だから圧倒的に速い。
パターンマッチング購読(`PSUBSCRIBE`)のコスト
では、`PSUBSCRIBE news.` のようなパターンマッチングはどう処理されるのか?
こちらはチャンネル名によるO(1)のハッシュ参照ではなく、パターンをリスト形式で保持し、`PUBLISH` が飛ぶたびに全パターンとの文字列マッチング(ワイルドカード評価)を行っている。
【テクニカルリードからの警告】
パターンマッチングは非常に強力だが、購読するパターン数(`PSUBSCRIBE` の数)が増えると、`PUBLISH` のたびに線形探索的なオーバーヘッドが乗ってくる。数千・数万単位のワイルドカード購読を乱用すると、単一スレッドであるRedisのメインループを確実に圧迫する。設計時は必ず購読パターンの上限を見積もれ。
—
2. Pub/Subの最大の罠:「非永続性」とメッセージ消失リスク
実務で最も事故るポイントがここだ。
RedisのPub/Subには「バッファリング」も「リトライ」も「ACK(確認応答)」も一切存在しない。
メッセージが消えるシナリオ
- シナリオA(コンシューマー不在): 誰もチャンネルを購読していない状態で `PUBLISH` を実行した場合、メッセージは即座に宇宙の彼方へ消え去る(どこにも保存されない)。
- シナリオB(ネットワーク断・バッファ溢れ): クライアントが一時的なネットワーク遅延などでフリーズし、Redis側のクライアント出力バッファ(Output Buffer)の制限サイズ(`client-output-buffer-limit pubsub`)を超えた場合、Redisは容赦なくそのクライアントを切断し、メッセージを破棄する。再接続しても、切断中のメッセージは戻らない。
[Publisher] ──PUBLISH──> [Redis Server] ──(切断中/バッファ溢れ)──X [Consumer (死活・遅延)]
└─> メッセージは即座に消滅!
この仕様を理解していれば、「重要度が高いデータ(決済、在庫変動、ユーザーの監査ログなど)」を純粋な Redis Pub/Sub 流すのがいかに狂気の沙汰かが分かるはずだ。
—
3. 実務でどう設計すべきか? 堅牢なアーキテクチャパターン
では、RedisのPub/Subはどこで使うべきなのか?
答えは明快で、「ロストしても再取得が可能、あるいはリアルタイムなライブ感(捨ててもいいデータ)が重視されるユースケース」に限定すべきだ。
- ◯ チャットのタイピングインジケータ
- ◯ ダッシュボードのリアルタイムメトリクス・グラフ描画
- ◯ 複数サーバー間での軽量なキャッシュ無効化シグナル(※Redis StreamsやKeyspace Notificationsとの比較検討が必要だが)
設計パターン:Pub/Sub + 永続ストレージのハイブリッド
もし「確実にメッセージを届けたいが、リアルタイム性も欲しい」という要件であれば、Pub/Sub単体で完結させてはならない。
1. データソース(RDBやKafka、Redis Streamsなど)に書き込む。
2. 同時に、トリガーとして Redis Pub/Sub で `PUBLISH` を飛ばし、コンシューマー側に「新しいデータが来たぞ」と通知(Wakeup)する。
3. 通知を受けたコンシューマーは、信頼性の担保されたストレージからデータをフェッチして処理する。
これなら、Pub/Subのメッセージが消えようとも、ストレージ側からポーリングや再取得が可能になるため、システム全体の堅牢性が劇的に向上する。
—
コード例として、堅牢性を意識したPython(`redis-py`)でのPub/Sub購読の基本形を見ておこう。
import redis
import time
コネクションプールの設定(実務では必須)
pool = redis.ConnectionPool(host=’localhost’, port=6379, decode_responses=True)
r = redis.Redis(connection_pool=pool)
def robust_subscriber():
pubsub = r.pubsub()
pubsub.subscribe(‘system:alerts’)
print(“Listening for alerts…”)
# 無限ループでのイベント待ち受け
while True:
try:
# timeoutを設定して、死活監視やコネクション維持ができるようにする
message = pubsub.get_message(ignore_subscribe_messages=True, timeout=1.0)
if message:
data = message[‘data’]
print(f”Received: {data}”)
# ここで重い処理や外部APIコールを行う場合、
# メッセージの取りこぼしリスク(処理中のクラッシュ)を考慮すること。
except redis.ConnectionError as e:
print(f”Connection lost: {e}. Reconnecting…”)
time.sleep(2)
# 再接続のロジックをここに挟む
except Exception as e:
print(f”Unexpected error: {e}”)
if __name__ == “__main__”:
robust_subscriber()
—
4. パフォーマンス上の注意点(運用知見)
最後に、プロダクション環境でRedisのPub/Subを運用する際に、SREやインフラエンジニアと必ず合意しておくべきパラメータ設定に触れておく。
1. `client-output-buffer-limit` のチューニング
デフォルトの設定のままだと、大容量のメッセージや高頻度のパブリッシュによってコンシューマーの処理が追いつかなくなった際、予期せぬ切断が多発する。
`redis.conf` において、pubsub用のバッファ制限は明示的に監視・調整すべきだ。
例: pubsubクライアントのハードリミットを32MB、ソフトリミットを8MB(60秒継続)に設定
client-output-buffer-limit pubsub 32mb 8mb 60
2. Redis Streams という選択肢
もし「コンシューマーがオフラインでもメッセージを保持したい」「コンシューマーグループで負荷分散したい」「メッセージのオフセット管理(ACK)がしたい」という要件があるなら、素直に Redis 5 以降で導入された Redis Streams を使うべきだ。Pub/Subの限界を美しく補完してくれる。
—
まとめ
RedisのPub/Subは、「速い、軽い、シンプル」の三拍子が揃った美しいアーキテクチャだ。しかし、それは「永続性や信頼性を完全に切り捨てる」というトレードオフの上になりたっている。
エンジニアリングとは、ツールの機能表をなぞることではない。「そのアーキテクチャがどのような制約(トレードオフ)を強いてくるか」を見極め、ビジネス要件と突き合わせる作業だ。
次の設計レビューでは、「このPub/Sub、もしコンシューマーが死んだらどうなるの?」という質問を、私から投げかける前に自分の口から言えるようにしておいてくれ。期待している。
コメント