問題の概要PgBouncerを追加してスケーリングの問題を修正し、最初の5分間は完璧に動作していました。しかし突然、ログに以下のエラーが表示され始めます:
ERROR: prepared statement "S_1" already exists
これは通常、コネクションプーラーをtransactionモードに切り替えたときに発生します。ローカル開発環境ではほとんど再現しないため、「自分の環境では動く」という典型的なバグです。このエラーが表示されるのは、特定の並行処理の閾値—多くの場合、毎秒10〜20リクエスト程度—に達し、複数のクライアントが同じバックエンドのデータベース接続を共有し始めたときだけです。
原因根本的な原因は、アプリケーションのORMとコネクションプーラーの間で、データベースセッションの「所有権」に関する根本的な認識の不一致があることです。
競合の仕組みプリペアドステートメントは、特定の接続のメモリに保存されるサーバーサイドのオブジェクトです。ORMがクエリを送信する際、将来の実行時間を節約するためにクエリを「プリペア」することがよくあります。PostgreSQLはこのプランにS_1のような名前を割り当てます。
transactionモードでは、PgBouncerは忙しいウェイターのように動作します。1コース(トランザクション)の間だけテーブル(接続)を提供し、すぐに同じテーブルを別のお客様に渡します。しかし、ウェイターはテーブルを片付けません。 クラッシュが発生する仕組みは以下の通りです:
- アプリインスタンスAが接続を取得し、ステートメント
S_1を作成して処理を終え、接続を解放します。- PgBouncerはその接続をプールに戻しますが、S_1はその接続のメモリに残ったままです。- アプリインスタンスBが同じ接続を取得し、自身のS_1ステートメントを定義しようとします。- PostgreSQLはS_1という名前がすでに使用中であることを検知し、エラーをスローしてクエリが失敗します。## デバッグの手順コードを変更する前に、プーラーの設定を確認しましょう。設定ファイルを確認するか、サーバーで以下のコマンドを実行します:
grep "pool_mode" /etc/pgbouncer/pgbouncer.ini
pool_mode = transactionと表示された場合、衝突の原因が見つかりました。このモードでは50〜100個の実際のデータベース接続スロットで数千の接続を処理できますが、セッションが1つのクライアントにのみ属するという前提が崩れてしまいます。
解決策### 1. Prismaの「pgbouncer」フラグを使用するPrismaユーザーにとっては最も簡単な方法です。機能を無効化する必要はなく、PrismaにPgBouncerと共存していることを伝えるだけです。.envファイルの接続文字列に?pgbouncer=trueを追加します:
DATABASE_URL="postgresql://user:pass@db-host:6432/mydb?pgbouncer=true"
このフラグにより、Prismaは匿名のプリペアドステートメントを使用するようになります。S_1のような名前がないため、衝突が発生しません。
2. ORMでプリペアドステートメントを無効化するほとんどのアプリケーションでは、プリペアドステートメントによるパフォーマンス向上は3〜5%未満であることが多く、不安定さを招くリスクに見合わないことがほとんどです。ORMレベルで無効化することが最も堅牢な修正方法です。
- **TypeORM:**データソースオプションで
prepareThreshold: 0を設定します。- **Sequelize:**設定でminifyAliases: trueを設定します。- Go (pgx):接続設定でPreferSimpleProtocol: trueを設定します。以下はTypeORMの標準的な修正例です:
{
type: "postgres",
host: "localhost",
port: 6432,
extra: {
max_prepared_statements: 0,
prepareThreshold: 0
}
}
3. PgBouncerの新機能を使用する(v1.22.0以降)インフラをアップグレードできる場合、PgBouncer 1.22.0でこの問題がネイティブに解決されました。プリペアドステートメントをトラッキングできるようになっています。pgbouncer.iniに以下を追加します:
max_prepared_statements = 100
これにより、PgBouncerが透過的にクリーンアップを処理できるようになります。Ubuntu 22.04などの古いLTSを使用している場合、バージョンは1.16である可能性が高いため、ORM側の修正を使用する必要があります。
4. 最終手段:sessionモードに切り替えるアプリケーションがセッションレベルの状態に大きく依存しており、コードを変更できない場合は、sessionモードに切り替えます:
[pgbouncer]
pool_mode = session
**注意:**これにより「多重化」のメリットがなくなります。500個のアプリインスタンスがあり、データベース接続が100個しかない場合、400個のインスタンスは即座に接続に失敗します。
動作確認修正を適用したら、うまくいくことを祈るだけで終わりにしてはいけません。簡単な負荷テスト中にログを監視しましょう。wrkなどのツールを使用して、データベースへの負荷が最も高いエンドポイントにアクセスします:
wrk -t12 -c40 -d30s http://localhost:3000/api/users
その間、Postgresのログを監視します:
tail -f /var/log/postgresql/postgresql-main.log | grep "already exists"
テストが完了してもログに何も表示されなければ、プーリングの問題は解決されています。

