背景:なぜPgBouncerにも監視が必要なのか?
昨年のフラッシュセール時、サーバーのCPU使用率はわずか35%程度だったにもかかわらず、本番のPostgreSQLクラスターがあわやダウン寸前に追い込まれました。その原因は、誰も予期していなかったボトルネック、PgBouncerにありました。
PostgreSQLへの各コネクションはLinux上で独立したバックエンドプロセスとして動作し、1接続あたり5〜10MBのRAMを消費します。マイクロサービスが300ポッドまでスケールアウトすると、接続数は容易にmax_connectionsの上限(通常200〜300程度)に達してしまいます。その結果、データベースのメモリが逼迫しクラッシュを引き起こします。PgBouncerはこの問題を根本から解決してくれるツールです。トランザクションプーリング(Transaction Pooling)の仕組みにより、数千ものクライアントが実際にはわずか40〜50個のバックエンド接続プールを共有して効率的に処理を行えます。
しかし、この命綱とも言えるPgBouncer自体がボトルネックになることがあります。バックエンドデータベースの処理が遅延すると、プールの空きコネクションは一瞬で枯渇します。後続のクエリは空きスロット待ちのキューに並ばざるを得なくなり、このキューがほんの数十秒で膨れ上がると、API Gatewayは即座に504 Gateway Timeoutを返します。データベース自体はダウンしていなくても、アプリケーション全体が完全に機能停止に陥るのです。
ユーザーから障害報告が届く前にこうしたトラブルを検知するためには、以下の3つのメトリクスを常時監視することが不可欠です:
- Client Waiting Connections: プーラーのキューで待機状態になっているクエリ数。
- Query Max Wait Time: クエリがバックエンドに割り当てられるまでに待たされた最大待機時間。
- Pool Utilization: 設定上限に対する稼働中バックエンドコネクションの割合(プール使用率)。
pgbouncer_exporterのインストール
PgBouncerは、pgbouncerという仮想データベースを通じて運用データを管理しています。SHOW POOLS;やSHOW STATS;コマンドを実行することで、これらの統計情報を手動で確認できます。Prometheusでこれらのデータを自動収集するために、Prometheus Communityが提供するGo言語製のバイナリpgbouncer_exporterを使用します。
まず、PgBouncerが稼働しているサーバーに対応するバイナリをダウンロードします:
# pgbouncer_exporterのバイナリをダウンロード
wget https://github.com/prometheus-community/pgbouncer_exporter/releases/download/v0.8.0/pgbouncer_exporter-0.8.0.linux-amd64.tar.gz
tar -xvf pgbouncer_exporter-0.8.0.linux-amd64.tar.gz
sudo mv pgbouncer_exporter-0.8.0.linux-amd64/pgbouncer_exporter /usr/local/bin/
# サービス実行用のシステムユーザーを作成
sudo useradd --no-create-home --shell /bin/false pgbouncer_exporter
次に、バックグラウンドプロセスを管理するためのsystemdサービスファイルを作成します:
sudo tee /etc/systemd/system/pgbouncer_exporter.service << 'EOF'
[Unit]
Description=PgBouncer Prometheus Exporter
After=network.target pgbouncer.service
[Service]
Type=simple
User=pgbouncer_exporter
EnvironmentFile=/etc/default/pgbouncer_exporter
ExecStart=/usr/local/bin/pgbouncer_exporter $PGBOUNCER_EXPORTER_OPTS
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
接続設定とPrometheus Scrapeの設定
PgBouncerの仮想コンソールから統計データを読み取れるよう、exporterに権限を付与する必要があります。設定ファイル/etc/pgbouncer/pgbouncer.iniを開きます:
[pgbouncer]
logfile = /var/log/postgresql/pgbouncer.log
pidfile = /var/run/postgresql/pgbouncer.pid
listen_addr = 127.0.0.1
listen_port = 6432
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
# admin/statsユーザーの定義
admin_users = postgres
stats_users = pgbouncer_exporter
/etc/pgbouncer/userlist.txtファイルで、pgbouncer_exporterアカウントのパスワードを設定します:
"pgbouncer_exporter" "ExporterSecurePassword123"
既存の接続を切断することなく、PgBouncerの設定をリロードします:
sudo systemctl reload pgbouncer
環境変数ファイル/etc/default/pgbouncer_exporterを通じて、exporterに接続文字列を渡します:
sudo tee /etc/default/pgbouncer_exporter << 'EOF'
PGBOUNCER_EXPORTER_OPTS="--pgBouncer.connectionString=postgres://pgbouncer_exporter:[email protected]:6432/pgbouncer?sslmode=disable --web.listen-address=:9127"
EOF
exporterを起動し、ポート9127からメトリクスが出力されているか確認します:
sudo systemctl daemon-reload
sudo systemctl enable --now pgbouncer_exporter
curl -s http://localhost:9127/metrics | grep pgbouncer_pools_server_active_connections
curlコマンドでステータスコード200と共にメトリクス一覧が返ってきたら、prometheus.ymlにスクレイプジョブの設定を追加します:
scrape_configs:
- job_name: 'pgbouncer'
scrape_interval: 10s
scrape_timeout: 5s
static_configs:
- targets: ['10.10.20.15:9127']
labels:
environment: 'production'
cluster: 'pg-core'
重要なコアメトリクスと実践的な運用の知見
Prometheusにデータを収集できただけでは、道半ばにすぎません。システムの健全性を正確に表すメトリクスがどれであるかを正しく見極める必要があります。
1. キューの監視:Client Waiting & Maxwait
これらはプールの詰まりを検知する上で最も感度が高い指標です。正常な状態であれば、この2つのメトリクスは常に0であるべきです。
# バックエンドスロットの空きを待っているクライアント数
sum by (database) (pgbouncer_pools_client_waiting_connections)
# プール内でクエリが待たされた最大時間(秒)
max by (database) (pgbouncer_pools_maxwait_seconds)
もしclient_waitingが5を超え、maxwaitが2秒以上に達した場合、システムは危機的な状態にあります。プールサイズが小さすぎるか、あるいはバックエンドのデータベースでスロークエリが発生して多数のテーブルロックを引き起こしている可能性があります。
2. プール飽和度(Pool Saturation)の測定
PgBouncerはPostgreSQLへの接続を遅延割り当て(lazy allocation)方式で管理します。初期状態ではごくわずかな接続しかオープンせず、負荷の上昇に応じてdefault_pool_sizeの上限まで徐々に接続数を増やしていきます。
そのため、active / (active + idle)のような計算式で使用率を算出すると、低負荷時に誤検知を起こしやすくなります。より安全なアプローチは、設定上の上限値に対して実際に稼働しているサーバー接続数を直接モニタリングすることです:
# 実際にクエリを処理中のバックエンド接続数
sum by (database) (pgbouncer_pools_server_active_connections)
ピーク時にこの数値がプールサイズの上限近く(例:50接続中45接続など)に達した場合は、プールサイズを少し引き上げるか、アプリケーション側のトランザクション保持時間を短縮する最適化を検討すべきタイミングです。
3. アラート設計の勘所:アラート疲れ(Alert Fatigue)を防ぐ
運用初期の頃、client_waiting > 0になった瞬間にSlackへ即座に通知を飛ばすアラートを設定していました。しかし、これは明らかな失敗でした。5秒ごとのcronジョブ実行やKubernetesのポッドスケーリングのたびに、メトリクスが一時的に1へ跳ねてすぐ0に戻るため、Slackには毎日数百件ものノイズ通知が届くことになりました。オンコール担当者は疲弊し、結果として本物の障害アラートまで見過ごす危険が生じたのです。
鉄則は「異常な状態が一定時間継続した場合にのみアラートを発報すること」です。現在標準化して運用しているルール定義は以下の通りです:
groups:
- name: pgbouncer_alerts
rules:
- alert: PgBouncerHighClientWaiting
expr: sum by (database) (pgbouncer_pools_client_waiting_connections) > 5
for: 1m
labels:
severity: critical
annotations:
summary: "PgBouncer データベース {{ $labels.database }} でキュー詰まりが発生"
description: "コネクション待ちのクライアント数が {{ $value }} 件に達し、1分以上継続しています。プールサイズまたはスロークエリを確認してください。"
- alert: PgBouncerHighQueryWaitTime
expr: max by (database) (pgbouncer_pools_maxwait_seconds) > 2
for: 30s
labels:
severity: critical
annotations:
summary: "PgBouncer プール {{ $labels.database }} でクエリ遅延が発生"
description: "クエリの待機時間が2秒を超えた状態が30秒間継続しています。"
- alert: PgBouncerPoolSaturation
expr: sum by (database) (pgbouncer_pools_server_active_connections) >= 45
for: 3m
labels:
severity: warning
annotations:
summary: "PgBouncer プール({{ $labels.database }})が上限寸前"
description: "アクティブなバックエンド接続数が上限(50)付近に達した状態が3分以上継続しています。"
Grafanaでは、公式ダッシュボードテンプレートの11905または14118をインポートすればすぐに使い始められます。PgBouncerのダッシュボードは、サーバーインフラ(CPUやDisk I/O)のダッシュボードと並べて配置するのがおすすめです。障害発生時に、コネクション不足が原因なのか、それともデータベースのディスクIOPS詰まりによるものなのかを即座に見極めることができます。

