深夜の接続エラーで学んだ、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 17 | Aurora 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運用の失敗を経験してますか?もしよかったら教えてください。