深夜の接続エラーで学んだ、PgBouncerと本番PostgreSQL運用の地雷

Aurora PostgreSQL本番3年で経験した接続プーリング設定ミスと、VACUUM FULLでロック地獄に陥った失敗談。実装パターンと対策を実体験から解説します。

深夜の接続エラーで学んだ、PgBouncerの存在

先日も朝6時に電話がかかってきた。「本番DBが繋がりません」。

実は自分たちのチームがPostgreSQL 17を使い始めて3年目。最初は小規模だったけど、トラフィックが増えるにつれて謎のタイムアウトエラーが頻発するようになった。「接続数多すぎ」っていう警告ログが次々出てくるんだけど、当時の自分たちはそれが何を意味するのか理解していなかった。

Aurora PostgreSQLのマネージド接続数制限(インスタンスタイプごと)に引っかかっていたんだ。db.r6g.largeなら最大約200接続。それを超えると、アプリケーションからの新しい接続は即座に拒否される。キューも何もない。本当に接続が切られるだけだ。

そこで導入したのがPgBouncer。接続プーリングを行うミドルウェアで、アプリケーション側の接続要求を一度受け取って、DBへの接続は限定的に保つ。つまり、アプリからは最大1000接続来ても、DB側には50接続程度に絞ることができる。

# pgbouncer.ini の典型的な設定(うちの環境)
[databases]
appdb = host=aurora-instance.xxxxx.rds.amazonaws.com port=5432 dbname=appdb

[pgbouncer]
pool_mode = transaction  # トランザクション単位でコネクション再利用
max_client_conn = 1000   # アプリ側からの最大接続数
default_pool_size = 25   # DBへの接続数
min_pool_size = 10
reserve_pool_size = 5
reserve_pool_timeout = 3

server_lifetime = 3600
server_idle_timeout = 600

この設定を入れた日、深夜の接続エラーがぱったり消えた。ただ、ここからが本当の地獄だったんだ。

バキューム戦略の失敗——VACUUM FULLでロック地獄

PostgreSQLって、更新や削除をしても物理的にはデータを削除しない。「削除済み」というマークを付けるだけで、ディスクには古いバージョンが残る。これが積み重なると、テーブルが膨張して、クエリが遅くなる。それを片付けるのがVACUUM。

うちのチームは最初、手動でVACUUM FULLを実行してた。夜中に本番DBに対して。

結果?テーブル全体にエクスクルーシブロックがかかって、その間のすべての読み書きがブロックされる。深夜でも十分なトラフィックがある本番環境では、このロック時間が30秒を超えるだけで、APIのタイムアウトが大量発生した。

今は自動VACUUMに任せて、定期的に軽いVACUUM(ロック不要)を実行する戦略に変えた。

-- autovacuum パラメータ(RDS Parameter Group で設定)
autovacuum = on
autovacuum_naptime = 10s           -- 10秒ごとにチェック
autovacuum_vacuum_scale_factor = 0.01  -- 1%の更新で動作
autovacuum_analyze_scale_factor = 0.005

-- 本番運用での定期メンテナンス(夜中のメンテナンスウィンドウ)
VACUUM ANALYZE appdb;  -- ロック無しの軽いVACUUM

ただ、正直ここまでやってもまだ完璧じゃない。大規模テーブル(数GB超)が入ってくると、autovacuumの動作タイミングをもっと細かく調整する必要がある。数値を変えるたびに、本番で1時間様子を見て、問題なければ次の設定に進む——その繰り返しが続く。地味だけど、この積み重ねが本番安定性を支えてるんだ。

インデックスの断片化とREINDEX

PostgreSQL 17で新しく便利になったのが、CONCURRENTLYオプション付きREINDEX。これまでREINDEXはテーブルロックを必須としていたけど、今は本番稼働中でも実行可能になった。

REINDEX INDEX CONCURRENTLY idx_users_email;  -- 並行実行可能

インデックスが断片化すると、同じデータをスキャンするのに必要なディスク読み込みが増える。結果、クエリが遅くなる。うちのチームは月1回のメンテナンスウィンドウで、アクセスが多いインデックス(pg_stat_user_indexで確認)をREINDEXしてる。

実装的には、CloudWatch Eventsから定期的にLambdaを実行して、RDS APIでパラメータ変更を加えるだけ。かなり簡単に自動化できる。

max_connectionsとwork_mem のバランス

深夜対応で学んだもう一つが、メモリ周りの設定だ。

Aurora r6g.largeなら16GBのメモリがあるけど、max_connectionsを無限に増やせばいいわけじゃない。クエリ実行時に必要なwork_memが、接続数 × work_mem を超えるとOOM(メモリ不足)で落ちる。

サーバー物理メモリ: 16GB
shared_buffers(推奨25%): 4GB
effective_cache_size(推奨75%): 12GB

work_mem = (12GB - 4GB) / (max_connections × 平均スレッド数)
        = 8GB / (200 × 3) ≈ 13MB程度

うちのチームは最初、work_memを32MBに設定していた。それでも大型クエリ(複数JOINとGROUP BY)を複数同時実行すると、メモリバースト(reserved capacity)を使い果たしていた。

AWS RDS Performance Insightsを見ると、wait eventで「memory」が頻出していたんだ。これを見て「あ、メモリが詰まってるんだ」と気づいたんだよ。

NULLの処理、DISTINCT、ソート——こうした操作はwork_memを大量消費する。大きなテーブルに対してこれらを実行すると、spill to disk(ディスクへの一時書き込み)が発生して、クエリが劇的に遅くなる。これはかなり痛い。

本番データベース選定時に気づいたこと

2026年時点で、PostgreSQL vs MySQL の選択肢をもう一度見直す価値がある。特にAWS環境では。

項目Aurora PostgreSQL 17Aurora MySQL 8.4
接続プーリングPgBouncer外部必須RDS Proxy対応
インデックス並行再構築17から可能8.0から可能
JSON機能jsonb で高速検索JSON高速化は限定的
複雑なクエリ最適化優秀(CTE、Window関数)改善中だが劣る
メモリ効率比較的良好バージョン依存
コスト(on-demand)ほぼ同額ほぼ同額

個人的には、複雑なデータ分析やレポート機能が多いなら確実にPostgreSQLだ。Window関数やCTE(Common Table Expression)の使いやすさが段違い。一方、シンプルなCRUD中心で高速性重視なら、MySQLも選択肢に入る。

ただし、うちのチームは採用してからの保守負荷(バージョンアップ、インデックス戦略、バキューム調整)をすべて含めて考えると、PostgreSQL一択になってしまう。バージョン17での改善が地味だけど、実運用ではかなり効いてるんだ。

Performance Insightsとメトリクスの活用

最後に、本番運用で自分たちが導入して初めて意味を理解したのが、RDS Performance Insights。毎日眺めるだけで、トラブルの兆候がだいぶ見えてくる。

Performance Insightsで見るべきポイント:

1. DB Load(ユーザーセッション × CPU コア数)
   - 平常時: 2~4
   - スパイク時: 8~10 → バキューム or インデックス再構築の合図

2. Wait Events(何で待ってるか)
   - "CPU": クエリが単に遅い
   - "memory": work_mem不足か max_connections超過の前兆
   - "lock": コンカレンシー問題
   - "IO": ディスク読み込み遅延(インデックス断片化?)

CloudWatchアラームだけでは拾えない、「来週ヤバくなりそう」という予兆が見える。これを毎日5分眺めるだけで、問題の兆候をキャッチできるから、本当に重宝してる。

まとめ

PostgreSQL・Aurora本番運用で学んだ、3つの地味だけど重い知見をまとめると:

1. 接続プーリングは必須 PgBouncerなしで本番スケールは無理だ。max_connectionsとの兼ね合いを正確に計算する手間は惜しまない価値がある。接続不足による深夜対応ほど辛いものはない。

2. VACUUM戦略が性能を左右する FULLロックはNG。autovacuumパラメータ調整を月単位でやる覚悟を持つ必要がある。RDS Parameter Groupで一元管理するのが推奨。

3. メモリ・インデックス・バキューム は三位一体 どれか一つ怠ると、他が全部台無しになる。Performance Insightsで毎日監視する仕組みが、結局コスト削減にもなるんだ。

正直、PostgreSQL運用の「完璧な答え」は存在しない。チームの規模、トラフィックパターン、データサイズで最適解は毎月変わる。だからこそ、チェックリストを作らずに、毎週Performance Insightsを眺めて、「今月は何を調整しようか」と考え続ける柔軟性が大事なんだ。

皆さんのチームでは、どんなPostgreSQL運用の失敗を経験してますか?もしよかったら教えてください。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事