PostgreSQL LISTEN/NOTIFY — どこまで使い、いつメッセージキューへ移行するか
LISTEN/NOTIFY は「起動の合図」としては優秀で、「メッセージキュー」としては不適切です。 この 1 行が結論で、この記事はその根拠です。PostgreSQL には別のインフラなしで使える pub/sub が内蔵されていて、ポーリングなしでイベントをリアルタイムにプッシュできます。問題はこれをキューとして誤用したときに起きる事故で、境界線を正確に知っていればとても有用な道具です。
動作: トランザクションと一体の通知 #
-- 聞く側 (セッション A)
LISTEN order_events;
-- 送る側 (セッション B)
BEGIN;
INSERT INTO orders (user_id, amount_jpy) VALUES (42, 12800);
SELECT pg_notify('order_events', '1204'); -- 新しい注文の id だけを送ります
COMMIT; -- この瞬間に通知が届きます核心の性質は、通知がトランザクションのコミット時点で、コミットされてこそ出るということです。ROLLBACK されれば通知もなかったことになります。外部のメッセージブローカーと DB の書き込みを併用するときに苦しむ「DB はロールバックされたのにメッセージは出た」という整合性の問題が、構造的にありません。これが LISTEN/NOTIFY の一番の魅力で、同じチャンネルを複数のセッションが LISTEN すれば全員に届くファンアウトもできます。
限界 3 つ: サイズ、揮発性、規模 #
- 8000 バイトの上限。 ペイロードは 8000 バイトが上限です。そもそもデータを運ぶために作られた通路ではないので、定石は本文はテーブルに、通知にはキーだけです。上の例が注文全体(JSON)ではなく id だけを送った理由です。受け取った側はキーで SELECT して最新の状態を読みます(通知の到着時点では状態がすでにさらに変わっているかもしれないので、どのみち読み直すのが正しいです)。
- 揮発性。 通知は保存されません。LISTEN 中のセッションがなければそのまま消え、リスナーが再接続する間の通知も失われます。つまり必ず処理されるべき仕事の伝達手段にはなれません。 耐久性のある伝達が必要なら、データ自体がテーブルにある必要があります。
- 規模のボトルネック。 通知はコミット順の保証のため、インスタンス全体の単一キューを通ります。発行が多くなると、コミットの通り道そのものがこの直列化に巻き込まれて DB 全体のコミットが遅くなるボトルネックになり、実際の大規模サービスの障害事例が報告されています(改善作業は進行中ですが、現行の安定版基準では実在する限界です)。高頻度の発行チャンネルには使うなという意味です。
定石の組み合わせ: 通知は起こし、キューはテーブルが受け持つ #
3 つの限界を総合すると、設計は 1 つに収束します。作業キューの本体は FOR UPDATE SKIP LOCKED のテーブルキューが受け持ち、LISTEN/NOTIFY は「新しい処理あり」をワーカーに知らせる、起動の合図としてだけ使います。
仕事の登録: INSERT INTO jobs ... ; pg_notify('jobs', ''); -- ペイロードも不要です
ワーカーのループ: 通知を待つ → 起きたら SKIP LOCKED で取れるだけ処理 → また待つ
(+ 通知の消失に備え、数十秒間隔のポーリングを安全網に)この組み合わせなら、通知が失われても安全網のポーリングが仕事を拾い、ワーカーがいなかった間の仕事もテーブルにそのまま残っています。通知は「ポーリング間隔をミリ秒に縮めてくれる加速器」にすぎず、正確性は全部テーブルキューが責任を持ちます。キャッシュ無効化のブロードキャスト、管理画面のリアルタイム更新、設定リロードの合図のように、「逃しても次の機会に追いつける」通知が LISTEN/NOTIFY 単独のふさわしい席です。
注意点を 1 つ。LISTEN はセッションに状態を残す機能なので、PgBouncer の transaction モードを通れません。リスナーのコネクションはプーラーを迂回して DB に直接、専用でつなぐのが標準です(ワーカーの数は普通少ないので負担になりません)。
本物のメッセージキューへ行く基準 #
DB の中で解決する上の組み合わせの守備範囲は広いですが、次のシグナルが見えたら専用のメッセージシステム(SQS・RabbitMQ・Kafka など)の席です。処理量が大きくてキューテーブルの書き込み・VACUUM が DB の本業を圧迫するとき、複数のサービスが購読するイベントストリーム(再生・保存の要求)が必要なとき、発行者と消費者が別々の DB・チーム・システムのとき。 逆に単一サービスの中のバックグラウンド処理なら、インフラを 1 つ増やす前にテーブルキュー + 通知の組み合わせが先です。SQS 系との比較で扱ったマネージドキューの価値は、その線を越えた後の話です。
まとめ #
- LISTEN/NOTIFY の魅力はトランザクションの整合性です。コミットされてこそ出て、ロールバックされれば通知もありません。
- 限界は 8000 バイト、揮発性、大量発行時のコミット直列化ボトルネックです。データの伝達・保証付きの伝達・高頻度チャンネルには不向きです。
- 定石は役割分担です。キューの本体は SKIP LOCKED のテーブル、通知は起動の合図 + 安全網のポーリングです。
- 通知にはキーだけ載せて、受け取った側が読み直します。リスナーのコネクションは PgBouncer の transaction モードを迂回して直接つなぎます。
- 処理量・複数購読・システムの境界が大きくなったら専用のメッセージキューへ移ります。それまでは DB の中の組み合わせが運用負担最小の答えです。