PROD欧州主権の BaaS プラットフォームダッシュボードを開く →

パフォーマンス · 10 分読み取り

PgBouncer トランザクション プーリングの説明

Affane Daylami · Fondateur · 2026年6月12日

ブログに戻る

PgBouncer のトランザクション モードは、クライアントの切断時ではなく、各トランザクションの終了時に PostgreSQL 接続を解放します。これにより、数十の実際のサーバー接続で数千の HTTP クライアントにサービスを提供できるようになり、短いクエリの REST API に推奨されるモードです。

この英語のテキストはフランス語のオリジナルから自動的に生成されたもので、まだレビューされていません。
このページは自動翻訳されました。英語版が正式です。

この利点には特定のコストが伴います。トランザクション モードは、あるリクエストから次のリクエストまで安定した Postgres 接続を前提とするすべてのものをサイレントに中断します。セッション SET、LISTEN/NOTIFY、勧告ロック、トランザクション後に存続するカーソル、名前付きプリペアド ステートメント。この記事では、メカニズムの詳細を説明し、これらの制限とその正確な症状をリストし、本番環境のバックエンド (リポジトリで直接検証されている) が制限に囚われずにどのように構成されているかを示します。ここで引用したパフォーマンス数値の背後にある測定方法については、ベンチマーク方法論を参照してください。

必需品

  • トランザクション モード: サーバー接続は、クライアントが切断されたときではなく、各トランザクションの終了時に解放されます。これは、短い REST API タイプの接続を共有する場合に最も効率的なモードです。
  • 構造上、セッション SET/RESET、LISTEN/NOTIFY、セッション アドバイザリー ロック、WITH HOLD カーソル、あるリクエストから別のリクエストに再利用される一時テーブルと互換性がありません。
  • 実際の最も一般的な落とし穴: いくつかのドライバー (sqlx、asyncpg、JDBC pgjdbc ドライバー) がデフォルトでアクティブ化する名前付きプリペアド ステートメントは、別のサーバー接続で再生され、負荷がかかると prepared statement does not exist のようなエラーを引き起こす可能性があります。
  • バージョン 1.21 以降、PgBouncer はトランザクション モード (サーバー接続を介した LRU キャッシュ) でプロトコルの準備されたステートメントに従うことができます。これは、アプリケーションがリクエストごとに search_path を変更する場合に、クライアント側のキャッシュを無効にすることを妨げるものではありません。
  • Aurabase コードで検証: テナント プールは pool_mode=transactionの statement_cache_capacity(0) および PgBouncer で実行されますが、PostgREST は LISTEN/NOTIFY を介したスキーマのリロードのために自発的に直接接続を維持します。
#
コンセプト

PgBouncer の 3 つのプーリング モード

PgBouncer には 3 つのモードがあり、Postgres サーバー接続がいつ共通プールに戻るかだけが異なります。公式ドキュメントでは、それらに session、 transaction 、 statement という名前が付けられています (pgbouncer.org/features.html、「プーリング モード」セクション、2026 年 8 月 24 日にアクセス)。

ファッションサーバー接続が緩んでいるセッションの互換性
セッション (デフォルト)クライアントの切断時合計: SET、LISTEN、カーソル、すべてがライブのように動作します
トランザクション各トランザクションの終了時 (COMMIT/ROLLBACK)部分的: トランザクションに対してローカルに残るもののみ
声明それぞれの個別のリクエストの後最小限: 明示的なマルチクエリトランザクションは禁止されています

セッション モードは最も寛容ですが、スケーラビリティの点で最も効果的ではありません。Postgres 接続は、2 つのリクエストの間に何も行わない場合でも、接続されたままである限りクライアント用に予約されたままになります。ステートメント モードは、非常に特殊なケース (読み取り専用プロキシ、ヘルスチェック) のために予約されており、従来の明示的なトランザクションも中断します。トランザクション モードは、REST API で実際に主流となる妥協案です。通常、各 HTTP リクエストは単一の短い Postgres トランザクションに対応します。

#
仕組み

トランザクション モードの仕組み、接続ごと

トランザクション モードでは、PgBouncer は、クライアントがトランザクションを開いたときにのみサーバー接続をクライアントに接続し、COMMIT または ROLLBACK 時にそれをプールに返します。 2 つのトランザクション間で、同じクライアントがまったく異なるサーバー接続に再割り当てされる場合があります。

具体的には、default_pool_size が 20 の場合、PgBouncer は、実際に進行中のトランザクションが数個しかない数百の同時クライアントを吸収できます。この比率により、トラフィックは多いがトランザクションが短い REST API のトランザクション モードが正当化されます。まれなリソース (サーバー側のメモリに高価な Postgres 接続) は、厳密に必要な時間だけ占有されます。

pgbouncer.iniini
[pgbouncer]
listen_port = 5432

; La connexion serveur est libérée dès la fin de chaque transaction
pool_mode = transaction

default_pool_size = 80
max_client_conn = 1000

; Ne pas utiliser avec des sessions stateful (SET, advisory locks, LISTEN/NOTIFY)

この最後のコメントは要点を要約しています。トランザクション モードは、「アプリケーション セッション」と「Postgres 接続」の間のリンクを意図的に切断するため機能します。このリンクに基づくものはすべて壊れます。次のセクションでは、正確に何をリストします。

#
限界

トランザクション プーリング モードでの障害

PgBouncer の公式ドキュメントには、同じクライアントからの 2 つのリクエスト間でサーバー接続が再利用されるとすぐに意味を失う PostgreSQL の機能が明示的にリストされています。

影響を受ける機能なぜ壊れるのか典型的な症状
設定 / セッションの設定この設定は、直後にリサイクルできる接続に適用されます。パラメータが 2 つのリクエスト間でランダムに忘れられるようです
聞く/通知する通知を受信するには永続的な接続を想定しますクライアントにはまったく通知されないか、断続的にのみ通知される
セッション勧告ロックロックは論理クライアントではなくサーバー接続によって保持されます。予想される完了前にロックが解放される、または解放されない
ホールドスライダー付き開いたトランザクションを超えて存続する必要がある次の反復で「カーソルが存在しません」エラーが発生する
一時テーブルトランザクションではなく、Postgres セッションに関連する次のクエリでテーブルが「消える」
という名前の準備済みステートメント特定のサーバー接続で準備され、別のサーバー接続で再生されますロード時に「準備されたステートメント ... が存在しません」
罠は必ずしもすぐに起こるわけではない

これらの制限のほとんどは、通常単一の接続がすべてのトラフィックを処理するローカル開発では現れません。これらは、複数のクライアントが実際にプールを共有し、同じ論理クライアントからの 2 つのリクエスト間でサーバー接続が実際に切り替わる場合に、実際の負荷がかかっているように見えます。煙の検査ではほとんど検出されません。

#
よくある罠

プリペアドステートメント: 最も誤解されている制限

最新の Postgres ドライバーは、アプリケーション コードが明示的に要求することなく、デフォルトでプロトコル側で名前付き要求を準備します。まさにこれが、この罠の予測を困難にしている理由です。

プロトコル準備済みステートメントは、 Parseの時点で名前が付けられ、特定のサーバー接続にキャッシュされます。トランザクション モードでは、同じ論理クライアントからの 2 つのリクエストの間に、この接続を別のクライアントに再割り当てできます。その後、ドライバーが準備されていない接続上で同じステートメント名を再実行すると、Postgres は明示的なエラー (通常、sqlx クライアントの場合は prepared statement "sqlx_s_N" does not exist) で応答します。この動作は断続的です。呼び出しごとに再現できる決定的なバグではなく、負荷の下で接続がどのように実行されるかによって決まります。

クライアント側の修正は言語に関係なく同じです。トランザクション モードでプーラーを通過するプールに対して、名前付きプリペアド ステートメントのキャッシュを無効にするか、名前のないクエリを強制します。 Rust と sqlx では、接続オプションで statement_cache_capacity(0) を通過します。

pool.rsrust
let connect_options = url
    .parse::<PgConnectOptions>()?
    .statement_cache_capacity(0);

// 同等のもの: asyncpg -> statement_cache_size=0、pgjdbc -> prepareThreshold=0

バージョン 1.21 以降、PgBouncer はサーバー側の問題の一部を軽減します。トランザクション モードでプロトコルの準備済みステートメントに従い、割り当てられた接続上でオンザフライでステートメントを準備できます。接続ごとの LRU キャッシュのサイズは max_prepared_statements経由で調整されます。これによりミスの数は減りますが、 search_path があるリクエストから別のリクエストに変更されるマルチテナント プールでクライアント キャッシュを無効にすることが免除されるわけではありません。キャッシュされたプランは Parseの時点で解決されたテーブルの内部識別子 (OID) をフリーズし、別のスキーマで再実行すると、単純なエラーではなく間違ったテナントからデータが返される可能性があります。

#
コードをチェックインしました

Aurabase がトランザクションモードで PgBouncer を設定する方法

Aurabase リポジトリは、共有データプレーン (deploy/helm/aurabase/templates/infra/pgbouncer.yaml) の前に PgBouncer を pool_mode=transaction としてデプロイし、テナントの各専用 Postgres インスタンス (deploy/cnpg/tenant-pooler.yaml) の前に同一に設定された Pooler CNPG をデプロイします。どちらのパスにも、上で説明したのと同じ規律が適用されます。

ソース コードには、安定性だけの理由ではなく、この選択の具体的なセキュリティ上の理由が文書化されています。テナント間で共有される Postgres プールは、再利用された接続上でプロジェクトごとに異なる search_path を配置します。キャッシュされたプリペアド ステートメントは、 Parseの時点で解決されたテーブルの OID をフリーズします。同じ接続上の別のテナントに対してクエリを再実行すると、単なるアプリケーション エラーではなく、最初のテナントのスキーマに対してクエリが実行され、分離バイパスが行われます。 したがって、statement_cache_capacity(0) は、トランザクション モードで CNPG プーラーも通過する専用インスタンスを含め、例外なく適用されます。

2 番目のセッション衛生対策: 各接続がプールに戻ると、フックによって DISCARD ALL が実行されます (設定のリセット、サーバー側で準備されたステートメントの割り当て解除、アドバイザリー ロックの解放、カーソルと一時テーブルのパージ)。このフックがないと、前のリクエストによって発生したセッションの残留物が、同じリサイクルされた接続を再利用する別のテナントからの次のリクエストでリークする可能性があります。

Exception accepted: dedicated PostgREST instances remain in direct connection to the primary, without going through the pooler. PostgREST schema reloading relies on LISTEN/NOTIFY, which assumes a persistent connection, exactly the functionality that transaction mode breaks (detail already documented in our article on PostgREST compatibility at Aurabase). RLS settings per request are passed to SET LOCAL within an explicit transaction, the only way to remain compatible with a pool that can change server connections at any COMMIT (see our article onmulti-tenant RLS isolation).

#
実践ガイド

アプリケーションを中断せずにトランザクション モードを有効にする

短いチェックリスト。Postgres の直接接続からトランザクション モードの PgBouncer に移行するバックエンドに適用できます。

  1. アプリケーション コードを監査します。 非トランザクション SET、 LISTEN/NOTIFY、セッション勧告ロック、 WITH HOLD カーソル、およびクエリ間で再利用される一時テーブルを探します。
  2. 明示的なトランザクション内のセッション SET を LOCAL SET に置き換えます。これは、次の接続でリークするのではなく、COMMIT/ROLLBACK でクリーンアップされるため、接続のリサイクル後に適切に存続する唯一の設定です。
  3. プールがプーラーを通過し、スキーマまたはロールがリクエスト間で変更される場合は、ドライバー側のプリペアド ステートメント キャッシュ を無効にします。パフォーマンスのコストは実際のものですが測定可能であり、テナント間の漏洩のリスクよりもはるかに低くなります。
  4. セッション モード (移行、管理スクリプト、LISTEN/NOTIFY に依存するもの) を本当に必要とする接続を、他のすべてのトラフィックに対してトランザクション モードを放棄するのではなく、直接の非プーラー接続に分離します。
  5. default_pool_size および max_client_conn のサイズは、別のプロジェクトからコピーした任意の数値ではなく、Postgres の実際の max_connectionsを基準にします。
  6. 単なるスモークテストではなく、実際の負荷の下でテストします。 準備されたステートメントのエラーとセッション設定のリークは、単一のローカル接続ではほとんど発生しません。
  7. 運用環境に入ったら、PgBouncer 管理コンソールから SHOW POOLS および SHOW STATS を監視し、クライアント側でプールの飽和が表示される前にそれを特定します。
#
決定

常にセッションではなくトランザクション モードを選択する必要がありますか?

いいえ、しかし、これは大部分の REST API にとって正しいデフォルトの選択です。セッション モードは、すぐにリファクタリングできないセッション機能に大きく依存するレガシー アプリケーションや、プールのゲインが移行作業を補えない低トラフィックの場合には、引き続き推奨されます。

このプーリング モデルの実装は PgBouncer だけではありません。Supavisor (Supabase) と PgCat は最近の 2 つの代替案であり、負荷分散とクラスタリングに異なるトレードオフがあります。トポロジに応じて 3 つのうちのいずれかを選択するには、詳細な比較「 PgBouncer vs Supervisor vs PgCat」を参照してください。

#
よくある質問

よくある質問

実稼働環境でトランザクション モードがアクティブ化されると、最も頻繁に生じる質問です。

PgBouncer トランザクション プーリング モードとは何ですか?+
これは PgBouncer の 3 つのモード (セッションとステートメントを含む) の 1 つで、クライアントの切断時ではなく、各トランザクションの終了時に Postgres 接続が別のクライアントに再割り当てされます。これにより、実際に開いている Postgres 接続よりも多くの競合クライアントにサービスを提供できるようになります。
プリペアド ステートメントがトランザクション モードでクラッシュするのはなぜですか?+
名前付き準備済みステートメントは、特定のサーバー接続上で準備されます。トランザクション モードでは、この接続は 2 つのリクエストの間に別のクライアントに再割り当てできます。ステートメントがまだ準備されていない接続上でドライバーがステートメントの名前を再生すると、Postgres は「準備されたステートメントが存在しない」というようなエラーを返します。修正には、ドライバー側で準備されたステートメントのキャッシュを無効にすることが含まれます (sqlx の場合は statement_cache_capacity(0)、asyncpg の場合はstatement_cache_size=0)。
トランザクション モードで PgBouncer の背後で LISTEN/NOTIFY を使用できますか?+
いいえ、確実ではありません。 LISTEN/NOTIFY は、通知を受信するために永続的な接続を前提としていますが、トランザクション モードではこれが保証されていません。標準的な方法では、LISTEN/NOTIFY に依存するコンポーネント (PostgREST など) を、プーラーの外部の Postgres への直接接続を通じて渡します。
トランザクション モードでは SET ではなく SET LOCAL を使用する必要がありますか?+
はい、特定のリクエストに適用する必要がある設定に対して体系的に適用されます。 SET LOCAL は COMMIT または ROLLBACK で自動的にクリーンアップされるため、2 つのトランザクション間で変更される可能性があるサーバー接続でも安全になります。従来の SET は、同じリサイクルされたサーバー接続を回復する次のクライアントに漏洩する可能性があります。
トランザクション モードは行レベル セキュリティ (RLS) で機能しますか?+
はい。RLS ポリシーで使用される JWT クレームまたはセッション変数が、セッション SET ではなくトランザクション内の LOCAL SET に設定されている場合に限ります。これは、マルチテナント RLS 分離に関する記事で説明されているパターンです。
PgBouncer、Supervisor、PgCat: どれを選択しますか?+
これら 3 つは同様のプーリング モデルを実装していますが、クラスタリング、負荷分散、エコシステムに違いがあります (Supavisor は Supabase によって開発され、PgCat は Rust で書かれています)。選択は、何よりも導入トポロジと既存の運用上の制約によって決まります。詳細については、専用の比較を参照してください。

導入の準備はできていますか?

5 分でバックエンドが完成します。

クレジット カードは不要 · 500 MB 無料 · 50,000 MAU