概要
- Postgres の LISTEN/NOTIFY はスケーラビリティに関する誤解が多い機能。
- 低レイテンシー通知 や ストリーム 用途で非常に有用。
- 実際のボトルネックは グローバルロック によるもの。
- バッファリングとバッチ化 で大幅な性能改善が可能。
- 最適化により 6万件/秒 のスループットと ミリ秒単位 の遅延を実現。
LISTEN/NOTIFYのスケーラビリティ問題
- PostgresのLISTEN/NOTIFY は、通知ベースのストリームやpub/subに利用可能な強力な仕組み。
- 一部ブログ記事により「スケールしない」との悪評が広まった経緯。
- 問題の本質は 直感的でない性能特性 と ドキュメント未記載の挙動。
- NOTIFY はグローバルロックを利用し、これがスケーラビリティの障害となる要因。
- 「直感的でない挙動」と「スケールしない」は同義ではない点に注意。
Postgresストリーム設計の基本
- ストリーム用のテーブルを作成し、各チャンク(例:LLMのレスポンストークン)を新規行として保存。
- 読み取り側は、次のチャンク到着時期が不明なため、ポーリング方式を採用しがち。
- ポーリング間隔が長いとレイテンシー増大。
- 短いと同時ポーラーがDB負荷を増大。
- LISTEN/NOTIFY により、リソースを無駄に消費せず、即座に通知受信が可能。
LISTEN/NOTIFYのパフォーマンス問題
- 初期実装では、ストリームテーブルのトリガーで毎回NOTIFYを発行。
- リーダーは通知待機し、通知到着で即座にデータ取得。
- 正しく低レイテンシーだが、スループットは 2,900件/秒 で頭打ち。
- CPUやIOPS等のリソース消費は目立たず、ボトルネックは グローバルロック。
グローバルロックの仕組み
- NOTIFYを含むトランザクションのコミット時 に、グローバル排他ロックを取得。
- このロックは、トランザクションが完全にコミットされ、fsync()でディスクに反映されるまで保持。
- 通知の順序保証のため、内部キューにコミット順で通知を格納する必要。
- トランザクションのコミット順はコミット完了時に決定されるため、排他ロックで直列化が必要。
- このため、 各書き込みでロック取得→直列化→スループット低下 という構造。
パッチの現状と限界
- Postgres 19向けのパッチでは、通知チャネルが多い場合の効率化のみが対象。
- グローバルロック自体や本質的なボトルネックは未解決。
パフォーマンス最適化手法
- 多くのユースケースでは、通知自体が「真実のソース」ではなく、単なる「新データ到着の合図」。
- 通知の順序や完全な永続性が不要 な場合、NOTIFYを メモリバッファでバッチ化 し、一定間隔でまとめて発行。
- バッファフラッシュ時のみグローバルロック取得。
- 各書き込みは高速化、Postgresのグループコミット等の最適化も活用可能。
- バッファ中にプロセスがクラッシュした場合、通知が失われるリスク。
- 対策として、リーダーは通知待機+一定間隔でバックアップ的なポーリングを実施。
- ポーリング頻度は低く抑えられるため、DB負荷増加は限定的。
最適化の効果とベンチマーク
- 最適化後 は、同時リーダー存在下で 6万件/秒 のストリーム書き込みを達成。
- レイテンシーは 15~100ms に抑制。
- 最大スループット時には CPUが完全に利用されている ことを確認、ボトルネック解消。
参考リンク・コミュニティ
- ベンチマークコード: GitHub (dbos-inc/dbos-postgres-benchmark)
- DBOSのドキュメント: Quickstart
- GitHubリポジトリ: dbos-inc
- Discordコミュニティ: 招待リンク