SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-21

PostgreSQL接続プーリングで4GB VPSのOOMを防ぐ方法

PostgreSQLは接続ごとにプロセスとメモリを使います。4 GB VPSではmax_connections=100に達する前にOOMになる理由と、プーラーの効果や注意点を解説します。

max_connections に達する前に小規模 VPS の RAM が枯渇する理由

VPS 上の PostgreSQL の接続プーリングは、速度を上げるための手法ではありません。4 GB のサーバーを稼働させ続けるために必要な仕組みです。PostgreSQL の接続はそれぞれ独立したオペレーティングシステムのプロセスであり、専用のプライベートメモリを使用するためです。プーラーは、多数のクライアント接続の背後に、少数で固定数の実バックエンドプロセスを配置します。

デフォルトの max_connections は 100 です。これは上限であり、メモリ予算ではありません。PostgreSQL は、実際のクエリを実行する 100 個のバックエンドをマシンが保持できるかどうか確認しません。そのため、先にマシンのメモリが枯渇します。カーネルの out of memory (OOM) killer がプロセスを選択し、その対象がバックエンドの場合、PostgreSQL は共有メモリを安全な状態に戻すため、クラスタ全体を再起動します。ログには server process (PID 1234) was terminated by signal 9: Killed と表示され、その後 terminating any other active server processes と表示されます。正常な接続も含め、開いているすべての接続が切断されます。

各接続がプロセスであることに加え、work_mem は接続ごとではなく、ソートまたはハッシュ処理ごとに割り当てられるため、メモリが枯渇します。これらはどちらも使用量を増幅させます。

接続はそれぞれプロセスであり、各プロセスがメモリを消費します

PostgreSQL は接続ごとに1つのプロセスを使用します。クライアントが接続すると、postmaster はバックエンドを fork し、そのバックエンドはクライアントが切断するまで存続します。これはスレッドではありません。独自のページテーブル、カタログキャッシュ、キャッシュ済みクエリプランを持ちます。接続先のテーブルが増え、異なるクエリを実行するほど、これらのキャッシュは増加します。そのため、負荷の高い ORM アプリケーションで長時間存続する接続は、新しい接続より多くのメモリを消費します。

共有メモリは実際に共有されます。shared_buffers はクラスタ全体で1つ確保され、すべてのバックエンドにマッピングされます。プライベートメモリは共有されません。そのため、ここで top を使うと誤解が生じます。バックエンドの resident set size (RSS) には、そのバックエンドが参照した共有ページが含まれるため、50個のバックエンドの RSS を合計すると、shared_buffers を50回重複して数えることになります。

代わりに、プライベート部分を測定します。PSS (proportional set size) は、各共有ページをそれをマッピングしているプロセス数で按分します。USS (unique set size) は、そのプロセスだけが所有するページのみを数えます。

sudo apt update
sudo apt install -y smem
sudo smem -k -P '^postgres'

USS 列は、そのバックエンドが終了した場合に解放されるメモリを示します。これが実際の接続ごとのコストです。公開されている数値では、アイドル状態のバックエンドは通常1桁台のメガバイトで、広範な ORM クエリを実行したバックエンドはその数倍になることが一般的です。ただし、これらは公開されている典型値であり、環境固有の数値ではありません。計画の基準にすべきなのは、実際のワークロードを実行する環境で測定した値だけです。

セッションごとに確保されるメモリの1つは見落としやすいものです。temp_buffers のデフォルト値は 8MB で、各セッションが初めて一時テーブルにアクセスしたときに、そのセッション専用に確保されます。セッションが終了するまで解放されません。

work_mem は接続単位ではなく、操作単位で割り当てられる

ここで計算を誤りやすくなります。work_mem のデフォルト値は 4MB です。PostgreSQL のドキュメントには、その意味が明確に説明されています。「複雑なクエリでは、複数のソート操作やハッシュ操作が同時に実行されることがあり、各操作は通常、この値で指定された量のメモリを使用できる。指定量を超えると、一時ファイルへのデータ書き込みを開始する」。3 つのソートノードを持つ実行計画では、1 つのバックエンド内で同時に work_mem の 3 倍を使用できます。

ハッシュ操作では、さらに多くのメモリを使用します。hash_mem_multiplier のデフォルト値は 2.0 です。そのため、ハッシュ結合やハッシュ集約では work_mem の 2 倍、標準設定では 8MB を使用する場合があります。並列クエリでは、各並列ワーカーが固有の割り当てを持つ別プロセスであるため、使用量はさらに増えます。

4 GB の VPS で計算してみます。shared_buffers を 1 GB に設定し、work_mem は 4MB のままにします。100 個の接続がそれぞれ 2 つのハッシュノードを含むクエリを実行するとします。この場合、100 × 16MB となり、1 GB の共有バッファに加えて 1.6 GB のプライベートメモリを使用します。ページキャッシュや、サーバー上のその他の処理を考慮する前の数字です。次に、サーバーの空き RAM を理由に work_mem を 64MB へ引き上げると、同じ 100 接続で 100 × 256MB になります。警告は表示されません。OOM killer が動作して初めて問題に気付きます。

推測するのではなく、work_mem が小さすぎるか確認できます。log_temp_files = 0postgresql.conf に設定して reload します。これにより、ディスクへの spill が発生するたびに、ファイル名とサイズを示す行が temporary file: path "base/pgsql_tmp/pgsql_tmp1234.0", size 20971520 のように記録されます。spill が頻繁に発生するなら、work_mem を増やすと改善できます。spill が発生しないなら、値を増やしても効果はなく、使用可能なメモリだけが減ります。

実際に問題になるプール計算

240 個の接続を開こうとして設定する人はいません。20 個のプールを設定した後、アプリケーションを複数の場所で実行します。

ChartBackends requested per app topology, pool size 20 per worker (arithmetic)
The data behind this chart
[
  {
    "config": "1 worker",
    "backends": 20
  },
  {
    "config": "4 web workers",
    "backends": 80
  },
  {
    "config": "4 web + 2 background",
    "backends": 120
  },
  {
    "config": "2 hosts x 4 workers",
    "backends": 160
  },
  {
    "config": "3 hosts x 4 workers",
    "backends": 240
  }
]

それぞれが20 個のプールを保持する4 個の Gunicorn worker は、80 個のバックエンドを要求します。バックグラウンドジョブの worker を2 個追加すると、120 です。3 hosts x 4 workers まで増やすと、アプリケーションは max_connections の100 個に対して 240 個のバックエンドを要求します。これらの 5 個の構成は、どの1 箇所を見ても誤設定ではありません。プールはプロセスごとに作成され、アプリケーションのどの部分からも合計数を確認できないためです。

ライブラリのデフォルト値も同じ方向に働きます。SQLAlchemy の QueuePoolmax_overflow=10 を指定すると、デフォルトで pool_size=5 になります。つまり、プロセスごとに15 個の接続です。HikariCP のデフォルトは10 個です。Django には5.1 より前のバージョンでは組み込みプールがなく、worker プロセスごとに1 接続でした。そのため、Django アプリケーションではこの問題が後から発生し、誰かが CONN_MAX_AGE を設定するか、新しい "pool": True オプションを有効にしたときに一度に表面化します。Gunicorn と nginx の背後で Django アプリを実行する場合、掛け合わせる数はサーバー数ではなく、Gunicorn worker 数です。

VPS での Postgres 接続プーリングが実際に変えること

プーラーは、一方でアプリケーションと PostgreSQL wire protocol で通信し、もう一方で少数の実サーバー接続を保持するプロセスです。クエリを高速化するものではありません。接続の負担を誰が担うかと、実際のバックエンド数を変えます。

改善する点は2つあります。プーラーがクライアントの接続を自ら処理するため、接続のたびに fork と、空のバックエンドキャッシュを埋めるためのカタログ検索が発生しなくなります。さらに重要なのは、実際のバックエンド数がアプリケーション接続数に連動しなくなることです。500 個のクライアントで20 個のバックエンドを共有できます。

待機させることが、この仕組みの要点です。ここに抵抗を感じる人が多くいます。プーラーがない場合、同時実行された500個のクエリがすべてバックエンドを取得し、2コアの CPU 上で同時に実行されます。そのため、すべてのクエリが遅くなり、メモリも同じタイミングで消費されます。プーラーがあれば20個だけが実行され、残りは数ミリ秒待機します。その結果、実行中の各クエリが CPU を十分に使え、早く完了します。大きなプールの前にキューがない構成より、小さなプールの前にキューを置く構成の方が優れています。

プーラーは、マシン上の他のリソース使用量を制限するものではありません。Postgres がアプリケーションサーバーや 同じ VPS 上のベクトルデータベースと VPS を共有している場合、プーラーが保護するのはアプリケーションから Postgres への影響だけです。それ以外のプロセスには効果がありません。周辺のサービスにもハード上限を設定してください。systemd でサービスが使用できるメモリと CPU を制限すれば、暴走したプロセスがデータベースまで停止させる事態を防げます。データベース自体をどこで実行するかによって、これらの上限の設定方法は変わります。これは、Postgres を Docker で実行するか、ホスト上で直接実行するかの実務上の違いです。

セッションプーリングとトランザクションプーリング

ほかの設定をすべて左右する設定は、pool_mode です。

セッションプーリングでは、1 本のサーバー接続がクライアント接続の存続中ずっと同じクライアントに割り当てられ、クライアントが切断すると解放されます。プールャーは単純なプロキシとして動作するため、すべてがそのまま機能します。接続確立のコストを削減できるだけで、それ以外の効果はありません。アプリケーションが 200 本の接続を開く場合、バックエンドも 200 個必要です。

トランザクションプーリングでは、サーバー接続は 1 つのトランザクションの実行中だけクライアントに割り当てられます。COMMIT または ROLLBACK の時点で接続はプールに戻り、次に待機しているクライアントが取得します。これにより、500 個のクライアントを 20 個のバックエンドで処理できます。一方で、これが問題の原因にもなります。これは設計上の動作です。次の文は、直前の文を実行したバックエンドとは別のバックエンドで実行される可能性があります。

PgBouncer のデフォルトは pool_mode = session です。インストール後に何も変更しないと、安価になるという利点だけを得て、接続数を削減する効果は得られません。3 つ目のモードである statement では、各文の実行後に接続が返却され、複数文のトランザクションは拒否されます。明確な理由がある場合を除き、このモードは変更しないでください。

どのトランザクションモードで何が機能しないのか、その理由

以下のすべてが失敗する理由は 1 つです。これらは単一のバックエンド内に保持される状態であり、トランザクションプーリングでは同じバックエンドが 2 回割り当てられる保証がないためです。

  • セッションレベルの SETRESETSET search_pathSET statement_timeoutSET TIME ZONESET ROLE は、そのステートメントを処理したバックエンドに設定され、次のトランザクションまでには失われます。明示的なトランザクション内で SET LOCAL を使用してください。これはそのトランザクションに限定されるため、安全です。
  • LISTEN。通知の配信先は LISTEN を実行したバックエンドです。そのバックエンドはトランザクション終了直後に別のクライアントへ割り当てられます。NOTIFY はトランザクションモードでも機能するため、この失敗は分かりにくくなります。送信は成功しますが、受信は決して発生しません。LISTEN が必要な場合は、プールを経由せず、port 5432 に直接接続する追加の接続を 1 つ開いてください。
  • セッションレベルのアドバイザリロック。pg_advisory_lock() はセッションが保持し、セッション終了時に解放されます。トランザクションプーリングでは、ロック解除の呼び出しが別のバックエンドで実行されるため、PgBouncer がそのサーバー接続を破棄するまでロックが保持されます。デフォルトでは server_lifetime、つまり 1 hour 後です。同じバックエンドが取得したロックをトランザクション終了時に解放する pg_advisory_xact_lock() を使用してください。
  • SQL ステートメントの PREPAREDEALLOCATE。トランザクションモードでは利用できません。
  • WITH HOLD カーソル、およびトランザクション終了後も存続すると想定するサーバーサイドカーソル。
  • commit 後も存続させる一時テーブル。CREATE TEMP TABLE ... ON COMMIT PRESERVE ROWS はテーブルを 1 つのバックエンドの一時スキーマに作成します。次のトランザクションが同じバックエンドで実行されるとは限りません。
  • LOAD

プロトコルレベルの prepared statement は、対応状況が変わった唯一の項目です。PgBouncer 1.21.0 でトランザクションモードのサポートが追加され、1.24.0 では max_prepared_statements を 200 に設定することでデフォルトで有効になりました。古いビルドではこの値が 0 のままで、機能は無効です。Ubuntu 24.04 には PgBouncer 1.22.0 が含まれているため、この機能自体はありますが、max_prepared_statements は自分で設定する必要があります。ビルドの動作が不明な場合は、クライアント側で安全に設定できます。psycopg 3 では、prepare_thresholdNone に設定すると、サーバーサイド prepared statement の使用が停止します。

Django には、この問題に関する独自の表現があります。ドキュメントには、「トランザクションプーリングモードの接続プーラー(例: PgBouncer)を使用する場合、その接続ではサーバーサイドカーソルを無効にする必要がある」と記載されています。その理由は、「サーバーサイドカーソルは、それを作成した接続でのみアクセスできる」ためです。そのデータベースのエントリで DISABLE_SERVER_SIDE_CURSORSTrue に設定してください。設定しないと、.iterator() の呼び出しが負荷時だけ断続的に失敗します。

トランザクションモードを採用する価値はありますが、守るべき契約があります。この一覧を読み、ORM とバックグラウンドジョブライブラリがこの条件に対応していることを確認してから切り替えてください。

PgBouncer をインストールし、アプリケーションの接続先を変更する

以下の設定は、PostgreSQL がすでに 127.0.0.1 の port 5432 で待ち受けている Ubuntu 24.04 サーバーで実行します。

sudo apt update
sudo apt install -y pgbouncer
pgbouncer --version

Ubuntu 24.04 では PgBouncer 1.22.0 がパッケージとして提供されています。2026 年 8 月時点の Upstream のバージョンは 1.25.2 です。使用中のバージョンを確認してください。上記の prepared statement の動作は、バージョンによって異なります。

PgBouncer の管理コンソールへのログイン専用の role を作成し、続けて password file を作成します。PgBouncer は pg_authid にある SCRAM(salted challenge response authentication mechanism)の secret を必要とします。この table は superuser だけが読み取れます。

sudo -u postgres psql -c "CREATE ROLE pgb_admin LOGIN PASSWORD 'change-this'"
sudo -u postgres psql -At -c \
  'SELECT format($$"%s" "%s"$$, rolname, rolpassword) FROM pg_authid WHERE rolpassword IS NOT NULL' \
  > /tmp/userlist.txt
sudo install -o postgres -g postgres -m 640 /tmp/userlist.txt /etc/pgbouncer/userlist.txt
rm /tmp/userlist.txt

パスワードを再入力せずに secret をコピーすることが、この構成を機能させます。PgBouncer が SCRAM secret を再利用して PostgreSQL にログインできるのは、client も SCRAM で認証され、file 内の secret が pg_authid の値と byte for byte で一致し(同じ salt と iteration count が必要で、パスワードが同じだけでは不十分です)、[databases] 行で user= を固定していない場合だけです。その行に user=appuser を追加すると、PgBouncer は代わりに plaintext password を必要とします。systemctl show pgbouncer -p User で、file の owner が service の実行 account と一致することを確認します。PostgreSQL で password を変更した場合は、この file を再生成してください。そうしないと、次の connect で password authentication failed が返されます。

次に /etc/pgbouncer/pgbouncer.ini を記述します。

[databases]
appdb = host=127.0.0.1 port=5432 dbname=appdb

[pgbouncer]
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = scram-sha-256
auth_file = /etc/pgbouncer/userlist.txt
admin_users = pgb_admin
pool_mode = transaction
max_client_conn = 500
default_pool_size = 20
min_pool_size = 5
max_db_connections = 80
max_prepared_statements = 200
ignore_startup_parameters = extra_float_digits

listen_addr = 127.0.0.1 により pooler は public internet からアクセスできなくなります。外部から到達可能な pooler は、公開するつもりのない authentication endpoint になるため重要です。max_client_conn は PgBouncer が受け入れる application connection の数です。1 接続あたりの負荷が小さいため、大きな値にできます。default_pool_size は、1 組の database と user が保持できる実際の backend の数です。こちらのほうが負荷の大きい値です。max_db_connections により database 全体を 80 に制限し、max_connections の下に psql、backup、monitoring 用の余裕を残します。ignore_startup_parameters = extra_float_digits により、connect 時にその parameter を送信する JDBC driver などの driver を PgBouncer が拒否しないようにします。

sudo systemctl restart pgbouncer
sudo systemctl status pgbouncer --no-pager
sudo journalctl -u pgbouncer -n 20 --no-pager

正常に起動すると、PgBouncer が 127.0.0.1:6432 で待ち受けていることを示す行が log に出力されます。password file が原因で起動に失敗した場合は、読み取れなかった path が log に出力されます。通常は syntax の問題ではなく、mode または owner の問題です。次に、application の connection string の port を 5432 から 6432 に変更して再起動します。application のその他の設定は変更しません。

プールが正常に機能しているか確認する方法

PgBouncer には、pgbouncer という仮想データベース経由で接続する管理コンソールがあります。

psql -h 127.0.0.1 -p 6432 -U pgb_admin pgbouncer
SHOW POOLS;
SHOW STATS;
SHOW CLIENTS;

確認すべき項目は SHOW POOLS です。cl_active はサーバー接続に現在割り当てられているクライアント数、cl_waiting は接続を待機しているクライアント数、sv_activesv_idle は実際に使用中および空いているバックエンド数、maxwait は待機キューの先頭にいるクライアントの待機時間を秒単位で示します。通常の負荷で正常な状態では、cl_waiting は 0、maxwait も 0 です。maxwait が 1 秒または 2 秒を超えて増加する場合、プールが小さすぎるか、クエリの実行が遅すぎます。これらには別々の対策が必要です。

default_pool_size を増やす前に、どちらなのか確認してください。

SELECT state, count(*) FROM pg_stat_activity
  WHERE backend_type = 'client backend' GROUP BY state;

ほとんどのバックエンドが idle in transaction の状態にある場合、問題はプールサイズではありません。アプリケーションがトランザクションを開始した後、HTTP 呼び出しなどの遅い処理をその中で実行しているため、クエリを実行していない間も各バックエンドが保持されています。idle_in_transaction_session_timeout でこの状態を打ち切れますが、根本的な修正はアプリケーションコード側で行います。一方、バックエンドがすべて active の状態にある場合、プールは実際に飽和しています。接続数を増やす前に、クエリの EXPLAIN (ANALYZE, BUFFERS) を確認してください。

サイズ設計では、HikariCP のヒューリスティックである「コア数の約 2 倍 + 1」が、最もよく引用される開始点です。2 コアの VPS では 5 になります。これを公表されている開始値として扱い、default_pool_size をその付近に設定したうえで、maxwait に基づいて調整します。小さなプールは不適切に感じられることがありますが、通常は測定結果が良くなります。キューで待機しているバックエンドにはコストがかかりませんが、実行中のバックエンドは CPU、メモリ、ロック競合を消費するためです。

PgBouncer、PgDog、Pgpool-II の選び方

通常のケースでは PgBouncer が適しています。PostgreSQL サーバーが 1 台、VPS が 1 台あり、アプリケーションがサーバーの処理可能数を超える接続を開く場合です。PgBouncer は 1 つの役割に特化しており、設定は 1 つの ini ファイルにまとまっています。Debian と Ubuntu のパッケージも利用できます。接続処理は単一スレッドで実行します。VPS 規模のワークロードには十分で、より大規模なマシンで初めて上限になります。

プーリングと同じネットワークホップでルーティングを決定したい場合は、PgDog を検討する価値があります。PgDog は PostgreSQL をスケールするためのプロキシを自称しており、Rust で実装されています。クエリを解析してトランザクションプーリング、セッションプーリング、読み取りと書き込みの分離を行い、マルチシャードルーティングと 2 フェーズコミットによるシャーディングにも対応します。プライマリと 1 台以上のレプリカがあり、レプリカの存在をアプリケーションに認識させずに読み取りをレプリカへ送信したい場合に適しています。注意点が 2 つあります。ライセンスは AGPLv3 です。そのため、ネットワーク利用条項については、本番環境に導入する前に、社内でその判断を担当する人と確認しておく必要があります。プロジェクトの見解では、社内利用や非公開の改変だけではソースコード公開義務は発生しません。もう 1 つは、まだ新しいプロジェクトであることです。週次でリリースされ、バージョン番号も 0.x のため、main を追従するのではなく、リリースタグを固定してください。

git clone https://github.com/pgdogdev/pgdog
cd pgdog
cargo build --release
./target/release/pgdog --config pgdog.toml --users users.toml

ソースからビルドするには、現行の安定版 Rust ツールチェーン、CMake、C/C++ コンパイラが必要です。リリースページにはビルド済みの Linux バイナリと Debian パッケージもあり、ghcr.io/pgdogdev/pgdog にはコンテナイメージがあります。設定は 2 つのファイルに分かれます。1 つ目には全般設定とデータベースごとのエントリを記述します。ここでは、2 つの形式を容易に区別できるよう、インラインテーブルの TOML 配列として記述します。

databases = [
  { name = "appdb", host = "127.0.0.1" },
]

[general]
port = 6432
default_pool_size = 10

2 つ目には、ユーザーごとのエントリを同じ配列形式で記述します。

users = [
  { name = "appuser", database = "appdb", password = "change-this" },
]

PgDog のデフォルト待ち受けポートは 6432 です。PgBouncer と同じポートのため、同じホスト上で両方がデフォルト設定のままポートを使用することはできません。

2026 年 6 月時点で 4.7.2 の Pgpool-II は、ロードバランシングを備えたプーリングと、自動フェイルオーバー用の watchdog を提供します。追加機能がある分、障害要因も増えます。選択する前に、まずプーリングモデルを理解してください。Pgpool-II は num_init_children 個の子プロセスを事前に fork します。各子プロセスは最大 max_pool 個のサーバー接続をキャッシュするため、バックエンド数の上限は num_init_childrenmax_pool の積です。各子プロセスは一度に 1 クライアントだけを処理します。そのため、受け入れられるクライアント数は num_init_children と同じで、起動時に固定されます。アイドル状態のクライアントも子プロセスを 1 つ占有します。num_init_children を 100、max_pool を 4 に設定すると、400 個のバックエンド接続を許可することになります。これは、プールを導入して解決しようとした問題そのものです。フェイルオーバーとクエリルーティングが必要なら Pgpool-II を選び、その場合は積を慎重に計算してください。バックエンド数を減らすことだけが目的なら、必要以上に複雑です。

マネージドプロキシに関する疑問とセルフホストでの同等構成

マネージドプラットフォームでは、これを独立した製品として提供しています。AWS は RDS の前段に RDS Proxy を配置し、Supabase は Supabase Postgres の前段に独自のプーラーである Supavisor を配置します。どちらもここで説明した役割を果たします。クライアント接続を低コストで保持し、実際のバックエンド接続数を少なくして割り当てます。Supavisor はオープンソースで、セルフホストも可能です。そのため、選択肢はプロプライエタリ製品と無償ソフトウェアの二択ではありません。

マネージドプロキシに相当するセルフホスト構成は、別の考え方ではありません。設定ファイルを自分で管理する、同じ考え方です。データベースと同じ VPS 上で transaction mode の PgBouncer を実行し、127.0.0.1 で待ち受けさせます。実際の違いは 2 つあります。マネージドプロキシはネットワーク 1 hop 分離れた場所にあるため、遅延が増えます。また、背後のデータベースが再起動している間もクライアント接続を保持します。データベースホスト上の PgBouncer は loopback を 1 hop 経由しますが、コストはほぼありません。一方、そのホストが停止すると PgBouncer も停止します。再起動をまたいで接続を維持したい場合は、フェイルオーバー機構も必要です。この段階で、Pgpool-II の watchdog や PgDog の health checks の複雑さに意味が出てきます。

もう 1 つ、候補に加えるべき選択肢があります。デプロイを複雑にしている主な原因が接続数である場合、組み込みデータベースにはプールすべき接続モデルがありません。組み込みデータベースはポートで待ち受けるサーバーではなく、プロセス内のライブラリだからです。書き込み量が中程度の単一アプリケーションサーバーであれば、VPS 上で SQLite を本番運用することで、管理によって解決するのではなく、この問題全体をなくせます。実際のデータベースサーバーが必要な場合は、マシンのサイズを決める前にプールサイズを決めてください。

FAQ

アプリケーションにすでに接続プールがある場合も PgBouncer は必要ですか?

通常は必要です。アプリケーションのプールはプロセスごとに存在し、他のプロセスのプールを把握できないためです。各 20 接続のプールを持つ Gunicorn worker が 4 つある場合、リクエストを処理する 80 backends になり、バックグラウンド worker を 2 つ追加すると 120 になります。合計を把握して上限を設定できるコンポーネントは PgBouncer だけです。適切な構成は両方を使うことです。各 worker 内には小さなプールを置き、リクエストごとの TCP 接続を避けます。その背後では、PgBouncer を transaction mode で動かし、実際の backends 数を制限します。

PgBouncer を transaction mode に切り替えると、具体的に何が壊れますか?

トランザクションをまたいで 1 つの backend に状態を保持する機能が影響を受けます。セッションレベルの SETRESETLISTENWITH HOLD cursor、SQL の PREPAREDEALLOCATE statement、セッションレベルの advisory lock、commit 後も存続させる必要がある temporary table、そして LOAD です。NOTIFY は引き続き動作するため、壊れた LISTEN がプールの問題ではなく配信の問題に見えることがあります。Django では DISABLE_SERVER_SIDE_CURSORSTrue に設定します。psycopg 3 では、prepare_thresholdNone に設定するか、PgBouncer 1.22 以降を使用して max_prepared_statements を 0 より大きくします。pg_advisory_lock()pg_advisory_xact_lock() に置き換えます。

2 core の VPS では default_pool_size をどの程度にすべきですか?

感覚より小さく設定します。広く知られている HikariCP の経験則では、core 数の約 2 倍に 1 を加えるため、2 core ならおよそ 5 です。ただし、これは開始点であり、最終的な答えではありません。値を設定したら、実際の負荷をかけて SHOW POOLS 内の maxwaitcl_waiting を確認します。両方が 0 なら、プールは十分な大きさです。maxwait が増加している場合、client がキューに滞留しています。値を増やす前に pg_stat_activity を確認してください。idle in transaction で停止している backends はアプリケーションのバグであり、接続数を増やしても問題を隠すだけです。

PgBouncer と PgDog のどちらを使うべきですか?

1 台の VPS 上の 1 台の PostgreSQL server であれば PgBouncer を使用します。これは多くの構成に該当します。Ubuntu の package があり、動作が十分に文書化されており、設定全体を 1 つの ini file にまとめられるためです。replica 間の read/write splitting や sharding を pooling と同じ経路で処理し、アプリケーションに topology を認識させたくない場合は PgDog を使用します。PgDog を採用する前に、勤務先で licensing を担当する人と AGPLv3 の扱いを確認してください。また、特定の release を固定してください。プロジェクトはまだ 0.x の version 番号で、毎週 release されています。

#postgres#pgbouncer#pgdog#connections#performance