データベース接続のコストを理解する
Webアプリケーションでは、HTTPリクエストごとにデータベースへの接続が必要です。しかしPostgreSQLの接続確立には、TCP接続、SSL/TLSハンドシェイク、認証、セッション初期化などの処理が必要で、1接続あたり約1〜5MBのメモリを消費します。高トラフィック時に接続数が膨れ上がると、メモリ不足やCPU過負荷でデータベースがクラッシュする危険があります。接続プールはこの問題を根本的に解決します。
接続プールの3つのモード
PgBouncerは3つの動作モードを提供しています。それぞれトレードオフがあり、ユースケースに合わせて選択します。
-- 接続数の現状確認
SELECT
count(*) AS total_connections,
count(*) FILTER (WHERE state = 'active') AS active,
count(*) FILTER (WHERE state = 'idle') AS idle,
count(*) FILTER (WHERE state = 'idle in transaction') AS idle_in_tx,
max(now() - state_change) AS longest_connection_age
FROM pg_stat_activity
WHERE datname = current_database()
AND pid <> pg_backend_pid();
-- 接続数の上限確認
SHOW max_connections;
SELECT count(*) FROM pg_stat_activity;
Session Mode(セッションモード): クライアント接続の存続中、同じDB接続を占有します。最も互換性が高く、プリペアドステートメントや一時テーブルが使えますが、プール効果は低めです。
Transaction Mode(トランザクションモード): トランザクション終了後に接続を解放します。最も効率的なモードで、少ない接続数で多くのクライアントをサポートできますが、プリペアドステートメントの扱いに注意が必要です。
Statement Mode(ステートメントモード): 各SQLステートメント後に接続を解放します。マルチステートメントトランザクションが使えないため、ほとんどのユースケースには不向きです。
PgBouncerの基本設定
-- pgbouncer.ini の主要設定(コメント付き)
-- [databases]
-- myapp = host=localhost port=5432 dbname=myapp
-- [pgbouncer]
-- listen_port = 6432
-- listen_addr = 0.0.0.0
-- 動作モード
-- pool_mode = transaction
-- 接続プールのサイズ
-- default_pool_size = 25 -- DBへの接続数(per database-user pair)
-- max_client_conn = 1000 -- クライアントからの最大接続数
-- タイムアウト設定
-- server_idle_timeout = 600 -- アイドル接続の切断(秒)
-- client_idle_timeout = 0 -- クライアントのアイドルタイムアウト(0=無制限)
-- query_timeout = 0 -- クエリタイムアウト(0=無制限)
-- パフォーマンス
-- server_reset_query = DISCARD ALL -- 接続解放時のリセット
-- server_check_query = select 1 -- ヘルスチェッククエリ
-- PgBouncerの状態確認(管理DB経由)
-- SHOW POOLS; → プールの状態
-- SHOW CLIENTS; → クライアント接続一覧
-- SHOW SERVERS; → サーバー接続一覧
-- SHOW STATS; → 統計情報
最適な接続数の計算
「接続を増やせばパフォーマンスが上がる」は誤解です。接続数はCPUコア数に基づいて計算します。
-- 推奨式: 接続数 = (CPUコア数 × 2) + 実効スピンドル数
-- 例: 8コアCPU + SSD → (8 × 2) + 1 = 17 接続
-- 現在の待機状態を確認
SELECT
wait_event_type,
wait_event,
count(*) AS waiting_queries
FROM pg_stat_activity
WHERE wait_event IS NOT NULL
AND pid <> pg_backend_pid()
GROUP BY wait_event_type, wait_event
ORDER BY waiting_queries DESC;
-- 接続使用率のモニタリング
SELECT
datname,
count(*) AS connections,
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections') AS max_conn,
ROUND(count(*) * 100.0 /
(SELECT setting::int FROM pg_settings WHERE name = 'max_connections'), 1) AS usage_pct
FROM pg_stat_activity
GROUP BY datname;
アプリケーション側の接続プール設定
PgBouncerだけでなく、アプリケーション側でも接続プールを設定します。
-- PostgreSQLの接続統計(セッション継続時間の分布)
SELECT
CASE
WHEN extract(epoch FROM (now() - backend_start)) < 60 THEN '< 1分'
WHEN extract(epoch FROM (now() - backend_start)) < 3600 THEN '1分〜1時間'
ELSE '1時間以上'
END AS connection_age,
count(*) AS connections
FROM pg_stat_activity
WHERE pid <> pg_backend_pid()
GROUP BY 1;
-- 長時間アイドルの接続を発見(切断候補)
SELECT pid, usename, application_name, state,
now() - state_change AS idle_duration
FROM pg_stat_activity
WHERE state = 'idle'
AND now() - state_change > interval '30 minutes'
ORDER BY idle_duration DESC;
-- 強制切断(必要な場合)
-- SELECT pg_terminate_backend(pid) FROM pg_stat_activity
-- WHERE state = 'idle' AND now() - state_change > interval '1 hour';
ErdmatesでインフラをERDに記録
接続プールの設定はアプリケーションアーキテクチャの重要な一部です。ErdmatesでデータベースのERDを管理しながら、PgBouncerの設定やDB接続数の設計もドキュメントとして整備しましょう。ERDとインフラ設定を一元管理することで、新しいメンバーのオンボーディングが容易になります。
まとめ
接続プールは適切に設定すれば、アプリケーションの変更なしにデータベースのスループットを大幅に改善できます。Transaction Modeを基本に、default_pool_sizeをCPUコア数から算出し、モニタリングで継続的に調整することをお勧めします。接続プールはインフラの一部であり、ERD設計と合わせて全体のアーキテクチャとして設計しましょう。