Redis Pub/Subの深淵:PUNSUBSCRIBEの内部メカニズムとパターンルーティングの限界
RedisのPub/Sub機能は、その圧倒的なスループットとレイテンシの低さから、リアルタイムメッセージングのプリミティブとして広く採用されている。しかし、`SUBSCRIBE` や `PUBLISH` が頻繁に議論される一方で、購読解除、特にパターンマッチングを用いた `PUNSUBSCRIBE` の内部挙動とメモリ管理のメカニズムについて正確に理解しているエンジニアは少ない。
本稿では、単なるコマンドのリファレンスではない。Redisのソースコード(`pubsub.c`)の深層に踏み込み、`PUNSUBSCRIBE` が実行された瞬間にサーバー内部で何が起きているのか、そのデータ構造とアルゴリズムの極限の知見を暴く。
—
1. Pub/Subルーティングの内部データ構造
RedisのPub/Subシステムは、完全なインメモリ・ハッシュテーブル駆動型である。クライアントがパターンベースの購読を行うとき、Redisサーバーはそれをグローバルなデータ構造に登録する。
パターン購読の管理:`pubsub_patterns` リスト
チャネル名による直接購読(`SUBSCRIBE`)がクライアントごとのハッシュセットで管理されているのに対し、パターン購読(`PSUBSCRIBE`)は、サーバー全体で共有される単方向連結リスト(Linked List)である `server.pubsub_patterns` によって管理されている。
このリストの各ノードは、`pubsubPattern` 構造体(内部表現)を保持している。
typedef struct pubsubPattern {
client client; // 購読しているクライアントへのポインタ
robj pattern; // マッチングパターン(RedisObject)
} pubsubPattern;
この設計から導き出されるアーキテクチャ上の事実がある。パターン購読の数は、サーバー全体のスループットに直接的な影響を与える。 `PUBLISH` が実行されるたび、Redisは通常のチャネル辞書を引くだけでなく、すべての登録済みパターンを線形走査(あるいはそれに類する処理)し、`stringmatchlen` によるグロブパターンマッチングを実行するからだ。
—
2. PUNSUBSCRIBEの実行フローと計算量
`PUNSUBSCRIBE ]` が発行されたとき、Redisサーバー内では以下の精密なステップが実行される。
1. 引数のパース: 指定されたパターン群をイテレートする。引数が省略された場合、そのクライアントが現在保持しているすべてのパターン購読が対象となる。
2. リストの走査と削除: `server.pubsub_patterns` を先頭から走査し、該当するクライアントかつパターンに一致する `pubsubPattern` ノードを特定・unlinkする。
3. クライアント側の状態更新: クライアント構造体(`client`)が持つパターン購読リストからも該当エントリを削除する。
4. 応答の送信: 購読解除完了の確認メッセージ(`punsubscribe` メッセージ)をクライアントの出力バッファに書き込む。
計算量の罠:$O(N \times M)$ の現実
ここでアーキテクトとして警鐘を鳴らしたい。`PUNSUBSCRIBE` の計算量は、サーバー全体に存在する全クライアントのパターン購読数 $N$ と、コマンドで指定されたパターン数 $M$ の積に依存する。
[server.pubsub_patterns] —> [Pattern A (Client 1)]
—> [Pattern B (Client 2)]
—> [Pattern A (Client 3)] <-- PUNSUBSCRIBE対象
数千のクライアントがそれぞれ数十のパターンを購読している巨大なトポロジにおいて、雑な `PUNSUBSCRIBE`(特に引数なしの全解除)や、高頻度な動的チャンネル増減は、サーバーのメインスレッドをブロックし、レイテンシスパイクを引き起こす主因となる。
---
3. パターンマッチングのアルゴリズムとワイルドカードの代償
`PUNSUBSCRIBE` 自体はマッチングを行わず、登録情報の「除去」を行うコマンドであるが、その前提となるパターンマッチングの仕様を理解していなければ、予期せぬメモリリークやルーティングミスを誘発する。
Redisのパターンマッチングは、`Tcl` のスタイルを踏襲した簡易的なグロブパターンを使用する。
- `h?llo` subscribes to `hello`, `hallo` and `hxllo`
- `hllo` subscribes to `hllo` and `heeeello`
- `h[ae]llo` subscribes to `hello` and `hallo`, but not `hillo`
危険なパターン設計:“ の乱用
アーキテクチャ設計において、“ や `.` のような広範なパターンを `PSUBSCRIBE` するアプローチはアンチパターンである。
なぜなら、`PUBLISH` 時にすべてのパターンとの照合が行われるだけでなく、`PUNSUBSCRIBE` 時にも、その曖昧なパターン文字列の厳密なオブジェクト比較(`equalStringObjects`)が必要になるからだ。
特に、アプリケーション側で動的に生成されたUUIDなどをパターンに含め、それを適切に `PUNSUBSCRIBE` し忘れた場合、`server. pubsub_patterns` にゾンビのような不要なノードが残り続け、メモリ(および不要なパターンマッチングのCPUサイクル)を静かに蝕んでいく。
—
4. 実戦的コード例:Node.js / ioredis による安全なパターン購読管理
プロダクション環境において、`PSUBSCRIBE` と `PUNSUBSCRIBE` を扱う際のベストプラクティスをコードで示す。接続のライフサイクルと購読解除の確実な実行が求められる。
const Redis = require(‘ioredis’);
// メインのパブリッシャーと、専用のサブスクライバー接続
const subClient = new Redis({ host: ‘localhost’, port: 6379 });
async function setupSubscription() {
try {
// パターン購読の設定
// 例: ログイベントの監視 “logs..error”
const pattern = ‘logs..error’;
await subClient.psubscribe(pattern);
console.log(`Successfully subscribed to pattern: ${pattern}`);
subClient.on(‘pmessage’, (pattern, channel, message) => {
console.info(`[PUNSUBSCRIBE Demo] Pattern: ${pattern}, Channel: ${channel}, Msg: ${message}`);
// 条件を満たしたら動的に購読解除
if (message.includes(‘FATAL’)) {
tearDown(pattern);
}
});
} catch (err) {
console.error(‘Subscription error:’, err);
}
}
async function tearDown(pattern) {
console.log(`Initiating graceful PUNSUBSCRIBE for: ${pattern}`);
try {
// PUNSUBSCRIBEの実行
// ※ 戻り値として、現在アクティブなチャンネル/パターンのカウントが返る
const count = await subClient.punsubscribe(pattern);
console.log(`PUNSUBSCRIBE completed. Remaining patterns: ${count}`);
// 接続を閉じる場合
// await subClient.quit();
} catch (err) {
console.error(‘PUNSUBSCRIBE failed:’, err);
}
}
setupSubscription();
—
5. チーフアーキテクトからの提言:Pub/Subの限界を見極めよ
Redisの `Pub/Sub`、そして `PUNSUBSCRIBE` は非常に洗練された機能だが、これは「ファイア・アンド・フォーゲット(送信して忘れる)」のパラダイムに基づいている。
メッセージの永続化はなく、クライアントが切断されたり、購読解除(`PUNSUBSCRIBE`)を行ってバッファがあふれたりした場合、メッセージは完全に消失する。
大規模分散システムにおいて、動的なトピックルーティングに `PSUBSCRIBE` / `PUNSUBSCRIBE` を多用しすぎると、前述した `server.pubsub_patterns` の肥大化によるCPUバウンドなボトルネックに直面する。もし、数千・数万の動的なパターンが必要なユースケースに直面したならば、RedisのPub/Subを捨て、Redis Streams の消費者グループ(Consumer Groups)や、専用のメッセージングブローカー(Apache Kafka, RabbitMQなど)への移行を検討すべきだ。
Redisを極限まで使い倒すとは、単にコマンドを知ることではない。その背後にあるデータ構造のメモリフットプリントと、計算量のオーダーを脳内に完全同期させることに他ならない。
コメント