Aurora PostgreSQLのメジャーバージョンアップグレード(14→15)を本番で実施したときの話です。
事前に調べた限りでは「オンラインで実行でき、数分から十数分程度のオフライン時間で完了する」という理解でした。実際、アップグレード自体はその通りに終わっています。オフラインだったのは11.6分ほどで、RDSのイベントにもDatabase cluster engine major version upgrade completeが出ました。
ここで「終わった」と思ったのですが、本当の山場はそこからでした。復帰直後からDBのCPUが90%台後半に張り付き、アプリ側のコンテナがヘルスチェックに失敗して再起動を繰り返し、実質的なサービス影響が約1.5時間続きました。オフライン時間より、復帰後の高負荷フェーズの方がずっと長かったことになります。
誰にでも起きうる内容だったので、整理しておきます。
何が起きていたか
時系列で並べると、こういう流れでした。
- アップグレード開始、DBがオフラインになる(11.6分)
-
major version upgrade completeイベントが出る - 直後から
CPUUtilizationとDatabaseConnectionsが急上昇し、下がらない - ALBのヘルスチェックが
Target.Timeoutでunhealthyになる - ECSタスクが次々と入れ替わり、同じタスク定義revisionのdeploymentが複数同時にIN_PROGRESSになる異常な状態に
- タスクの入れ替わりが収束しないまま高負荷が継続
ポイントは4番です。アプリが例外で落ちていたわけではなく、DBの応答待ちで極端に遅くなっていただけでした。それでもタイムアウトには引っかかるので、外形的には「アプリが死んでいる」ように見えます。そしてタスクが再起動すれば、また一からDBに接続しにいくので、さらにDBが重くなります。
原因は、独立した2つのことが同時に起きたことでした。
原因その1: 復帰直後の一斉再接続(サンダリングハード)
DBがオフラインだった十数分の間、アプリ側のECSタスクはずっと接続を待っている状態でした。そこにDBが復帰すると、待っていたプロセスが一斉に再接続を試みます。いわゆるサンダリングハード(Thundering Herd)です。
平常時は接続がばらけているので問題になりませんが、全員が同じ瞬間に待ちから解放されるとなると話が変わります。復帰直後のDBに、平常時とは比べものにならない密度で接続とクエリが押し寄せます。
しかもここで、もう1つの問題が効いてきます。
原因その2: 統計情報が引き継がれず、クエリプランが劣化する
PostgreSQLのプランナは、pg_statisticカタログに入っている統計情報をもとにクエリプランを決めています。この統計情報が、メジャーバージョンアップグレードでは新バージョンに引き継がれません。
これはAuroraに限った挙動ではなく、pg_upgradeの仕様によるものです。AWSのドキュメントでも、メジャーバージョンアップグレード後にCPU使用率が高くなる典型的な原因として統計情報が最新でないことが挙げられており、アップグレード後にANALYZEを実行して統計を作り直すことが推奨されています。
つまり復帰直後のDBは、統計情報がないために非効率なプランを選びがちな状態です。本来インデックスを使うはずのクエリがSeq Scanになったりして、1本あたりのコストが跳ね上がります。
そこに原因その1の一斉再接続が重なると、
- 1本1本のクエリが重い
- そのクエリが一斉に大量に飛んでくる
- 応答が遅れてヘルスチェックが落ちる
- タスクが再起動して、また接続しにくる
という悪循環が完成します。どちらか片方だけならここまでにはならなかったと思いますが、重なったことで1.5時間コースになりました。
なお、PostgreSQL 18からはpg_upgradeが統計情報の大部分を引き継ぐようになっています。ただしそれでもアップグレード後のANALYZEが不要になるわけではない、とされています。14→15のような、それ以前のバージョン間のアップグレードでは統計情報は引き継がれないと考えておくのが安全です。
見分け方
同じ状況に陥ったときに、これを疑うべきサインは次のあたりです。
- RDS/Auroraの
CPUUtilization・DatabaseConnectionsが、アップグレード完了イベントの直後から急上昇し、なかなか下がらない - ALBのヘルスチェックが
Target.Timeoutで落ちている(=アプリの異常終了ではなく、応答が遅いだけ) - ECSで、同一タスク定義revisionのdeploymentが複数同時にIN_PROGRESSになるなど、タスクの入れ替わりが収束しない
3つ目は特にわかりやすいサインでした。デプロイしていないのにdeploymentが増えるのは明らかにおかしいので、「アプリのバグではなく、下のレイヤーで何か起きている」と切り替えるきっかけになります。
対処
まず、実際に何が重いのかをpg_stat_activityで確認しました。
SELECT pid, now() - query_start AS duration, state, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE state <> 'idle'
ORDER BY duration DESC;
滞留しているクエリと、その対象テーブルを特定します。そのうえで、該当テーブルにANALYZEを実行します。
ANALYZE public.some_table;
対象が絞り込めない場合や、全体的に遅い場合は、DB全体に対して実行します。
ANALYZE VERBOSE;
統計情報が入るとプランナが正しいプランを選べるようになり、CPUは正常な水準に戻ります。ここが効きます。
サンダリングハードの側が疑わしい場合は、一時的にECSサービスのdesired countを絞ってDBへの同時接続数を減らし、落ち着いてから段階的に戻します。全員を同時に戻さないことが大事です。
手順に組み込んだこと
この件を受けて、メジャーバージョンアップグレードの手順書を次のように変えました。
1. DB復帰直後のANALYZEを、標準ステップとして組み込む
「様子を見ておかしかったら実行する」ではなく、必ず実行する手順にしました。統計情報が引き継がれないことは仕様として分かっているので、問題が出てから対応するのでは順番が逆です。理想を言えば、完了イベントを検知して自動実行できると事故がなくなります。
2. 「アップグレード完了」を「復旧完了」と見なさない
RDSのイベントが出た時点ではまだ終わっていません。CPU使用率・接続数・ヘルスチェックが平常値に落ち着くところまで確認して、はじめて完了とする運用にしました。作業時間の見積もりにも、復帰後の監視時間を含めるようにしています。
3. 復帰時の接続の戻し方を決めておく
ECSのdesired countを事前に絞っておき、復帰後に段階的に戻す進め方を、選択肢として手順に書き足しました。毎回必要というより「詰まったときにすぐ引ける札」という位置づけです。
まとめ
- Auroraのメジャーバージョンアップグレードは、オフライン時間が終わっても終わりではない
- 復帰直後は「統計情報がない状態のDB」に「待たされていた全プロセスの再接続」が同時に来る、かなり厳しい条件が揃う
-
ANALYZEはアップグレード手順の必須ステップとして最初から組み込んでおく - 完了イベントではなく、メトリクスが平常値に戻ることをもって復旧完了とする
事前の調査では「オフライン時間」の情報ばかり追いかけていて、復帰後に何が起きるかまでは見ていませんでした。今回いちばんの学びは、データベースの作業は「戻ってきた瞬間」がいちばん危ない、という感覚を持てたことだったと思います。
これからメジャーバージョンアップグレードを控えている方の参考になれば嬉しいです。