高パフォーマンスなPostgreSQL本番環境のためのPgBouncer設定ガイド

Database tutorial - IT technology blog
Database tutorial - IT technology blog

スケーラビリティの限界点

すべてはシンプルに始まります。アプリケーションを構築し、PostgreSQLインスタンスに接続すれば、最初はスムーズに動作します。しかし、トラフィックが増えるにつれて、奇妙な現象に気づき始めます。ピーク時のレイテンシの急上昇。クエリ量はそれほど変わっていないのに、データベースサーバーのCPU使用率が上昇する。そして最終的には、恐ろしい FATAL: remaining connection slots are reserved for non-replication superuser connections(致命的エラー:残りの接続スロットは非レプリケーション用のスーパーユーザー接続のために予約されています)というエラーに直面することになります。

私も経験があります。多くのエンジニアはまずその場しのぎの解決策を試みます。postgresql.confmax_connections を100から500、それとも1000へと引き上げます。数日間は機能しますが、やがてサーバーは這いつくばるように遅くなります。RAM使用量は爆発し、コンテキストスイッチが大きなボトルネックとなります。この時、生のPostgreSQL接続はタダではないこと、そしてハードウェアを増強するだけでは解決にならないことに気づくのです。

PostgreSQL接続コストの理解

PostgreSQLは「プロセス単位の接続モデル」を採用しています。クライアントが接続するたびに、データベースは新しいバックエンドプロセスをフォーク(fork)します。この設計は優れた分離性と安定性を提供します。一つのプロセスがクラッシュしてもクラスタ全体がダウンすることはありませんが、リソースの観点からは重い代償が伴います。

各バックエンドプロセスは数メガバイトのメモリを消費します。1,000の有効な接続がある場合、単一の SELECT を実行する前段階で、接続を維持するだけのオーバーヘッドに数ギガバイトのRAMが必要になります。

さらに重要なのは、OSが1,000もの競合するプロセスを管理するのに苦労することです。CPUは実際の計算よりも、プロセスの切り替え(コンテキストスイッチ)に多くの時間を費やすようになります。私の経験上、数百の接続というしきい値を超えると、接続あたりのパフォーマンスは著しく低下し始めます。

アプリケーションレベルのポーリング vs 外部プロキシ

多くのフレームワークが、JavaのHikariCPや、SQLAlchemy、Djangoの組み込みプーラーなど、組み込みの接続ポーリング機能を提供しています。これらは単一のアプリケーションインスタンスに対して安定した接続を維持するのには適しています。しかし、現代のマイクロサービスアーキテクチャでは不十分です。

例えば、サービスを実行しているKubernetesのポッド(Pod)が20個あるとします。バーストに備えて各ポッドが50の接続プールを維持すると、それだけでデータベースへの接続数は1,000に達してしまいます。

これらの接続のほとんどは、大半の時間はアイドル状態ですが、それでもPostgreSQLサーバー上のメモリとリソースを占有し続けます。ここで、PgBouncerのようなミドルウェアプーラーが不可欠になります。これはゲートウェイとして機能し、数千ものアプリケーション接続が、より小さく高効率な実際のデータベース接続プールを共有できるようにします。

PgBouncerのセットアップ:戦略的なアプローチ

PgBouncerは非常に軽量です。libeventをベースにしたシングルスレッドのイベントループで、最小限のCPUとRAM使用量で数万のクライアント接続を管理できます。本番環境で私がよく採用するデプロイ方法は以下の通りです。

1. インストールと基本設定

DebianやUbuntuシステムでは、インストールは非常に簡単です:

sudo apt-get update
sudo apt-get install pgbouncer

設定は /etc/pgbouncer/pgbouncer.ini にあります。セットアップの核心は、データベースと認証方法の定義です。メイン設定をきれいに保つため、ハッシュ化されたパスワードを保存する別の userlist.txt ファイルを使用するのが好みです。

2. ポーリングモードの定義:最も重要な選択

ここで多くの人がつまずきます。PgBouncerは3つのポーリングモードを提供しており、間違った選択をするとアプリケーションが動作しなくなります。

  • セッションポーリング (Session Pooling): 接続はセッションの全期間中、クライアントに割り当てられます。最も互換性が高いですが、バックエンド接続数に制限されるため、メリットは最小限です。
  • トランザクションポーリング (Transaction Pooling): これが「最適解」です。接続は単一のトランザクションの間だけクライアントに割り当てられます。COMMIT または ROLLBACK が呼び出されると、接続はプールに戻されます。これにより、1,000のクライアントが50のバックエンド接続を効果的に共有できます。注意: このモードでは SET ROLE やプリペアドステートメント(prepared statements)のようなセッションベースの機能を簡単には使用できません。
  • ステートメントポーリング (Statement Pooling): 最もアグレッシブなモードです。ステートメント(SQL文)ごとに接続が返されます。マルチステートメントのトランザクションが動作しなくなるため、一般的なWebアプリでは滅多に使用されません。

ほとんどの本番ワークロードでは、トランザクションポーリングが望ましいです。堅牢に設定された pgbouncer.ini のスニペットは以下の通りです:

[databases]
# ローカルホストの 'myapp_db' に接続
myapp = host=127.0.0.1 port=5432 dbname=myapp_db

[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 5000
default_pool_size = 50
reserve_pool_size = 10
ignore_startup_parameters = extra_float_digits

ユーザーリストの管理

userlist.txt はシンプルな "ユーザー名" "パスワード" の形式です。私は通常、PostgreSQLの pg_shadow テーブルからこれらを取得します。データインポートや環境間のユーザー移行のためにCSVをJSONに素早く変換する必要があるときは、toolcraft.app/ja/tools/data/csv-to-json を使います。ブラウザ内で動作するため、データがマシンから外部に出ることはありません。機密性の低い設定項目を扱う際には、セキュリティ上の利点となります。

本番環境の安定稼働に向けた実践的チップス

PgBouncerを稼働させた後、本番環境のトラブルを防ぐために私が苦労して学んだいくつかのポイントを紹介します。

1. 「ignore_startup_parameters」のテクニック

JDBCやHibernateで構築されたアプリケーションなどは、特定の起動パラメータ(extra_float_digits など)を送信することがよくあります。PgBouncerがこれらを認識できないと、接続を拒否してしまいます。設定に ignore_startup_parameters = extra_float_digits を追加することは、謎の接続エラーに対する一般的な修正方法です。

2. 管理コンソールによるモニタリング

PgBouncerには pgbouncer という独自の仮想データベースがあります。ここにログインして、プールのリアルタイム統計を確認できます:

psql -p 6432 -U pgbouncer pgbouncer

ログイン後、SHOW POOLS; を実行して、接続を待機しているクライアント数(cl_waiting)を確認します。もし cl_waiting が常に0を超えている場合は、default_pool_size を少し増やすタイミングです。

3. 接続制限とファイル記述子

5,000以上の接続を処理する予定がある場合は、OSの制限が許可されているか確認してください。ulimit -n をチェックし、pgbouncer ユーザーが十分なファイルを開けるようにします。1つのクライアント接続が1つのファイル記述子を使用し、1つのバックエンド接続がもう1つを使用します。

4. セキュリティとMD5

PostgreSQLはSCRAM-SHA-256に移行しつつありますが、古いPgBouncerのバージョンは依然として userlist.txt のMD5に依存しています。認証方法がデータベースの期待値と一致しているか確認してください。SCRAMを使用する場合は、PgBouncerのバージョンが1.12以上であることを確認してください。

結論

PgBouncerの導入は、単にメモリを節約するためだけではありません。データベースの挙動を「予測可能」にすることが目的です。PgBouncerがないと、トラフィックの急増がプロセス管理の負荷を招き、指数関数的なスローダウンを引き起こす可能性があります。導入することで、データベースは安定した管理可能なトラフィックの流れを受け取ることができ、アプリケーションはバックエンドのクラッシュを恐れることなく水平スケーリングが可能になります。

本番環境でPostgreSQLを運用しているなら、「クライアントが多すぎる(Too many clients)」エラーが発生するのを待たないでください。早い段階でPgBouncerを導入し、トランザクションポーリングを設定して、安定した接続レイヤーがもたらす安心感を享受しましょう。

Share: