TL;DR
- 本番DBの障害調査を「調査できますか」の一言で任せたところ、原因特定から修正まで完遂した
- 見る指標や遡る期間は指定していないため、どう判断したのかを聞き返して型として取り出した
- 出てきたのは「日次の最小値の形で慢性と急性を切り分ける」など、再利用できる4つの判定軸
- 4つ目は検証段階の話。本番で
EXPLAIN (ANALYZE)を打つと、確認行為が障害を再発させる - 賢さはボトルネックではなかった。差がつくのは何を渡したかと何を持ち帰ったか
この記事で扱うこと
本番データベースの障害について、Slackのアラートへのリンクを貼って調査を依頼したところ、原因の特定からアプリケーション側の修正、本番反映後の確認まで進みました。見る指標も、遡る期間も、こちらからは一切指定していません。
そこで「どういう思考回路で進めたのか」を聞き返し、判定の型として取り出しました。この記事の中身はその成果物です。障害調査そのものよりも、どんな判断を積み重ねて結論に至ったのかのほうが主題になります。
実際に投げた入力
再現できる形にするため、最初に渡したものをそのまま書いておきます。
Slackに流れてきたアラートの中身は、要点だけ取り出すとこの1行でした。
FATAL: the database system is starting up
本番のAPIがデータベースへ接続できずに失敗した、という通知です。同じ症状が同日の午前中に2回起きていました。エンドポイント名やインスタンス名も通知には含まれていましたが、ここでは伏せています。
依頼はこれだけです。
(Slackアラートへのリンク)これ何起きてるかAWSのログ見たりして調査できますか
役割の指定も、手順の指示も、出力形式の指定もしていません。プロンプトの工夫はゼロです。
ただし、厳密にはワンショットではありませんでした。このあと2回だけやりとりが挟まっています。どちらもAI側が次の一手を提案し、こちらが承認しただけです。調査の進め方を決めていたのは終始AI側でした。
AI: 原因クエリの特定まで進めましょうか
私: お願いします
AI: アプリ側リポジトリで原因箇所を特定する作業まで進めましょうか
私: 進めたい
前提: AIに渡していた環境と権限
判断の質は、渡した権限の範囲でほぼ決まります。同じ依頼でも見られる範囲が違えば結論も変わるので、先に条件を書いておきます。
- モデル: Claude Opus 4.8(Claude Codeから実行)
-
AWS: 読み取り権限のあるプロファイルでCLIを利用。CloudWatchのログとメトリクス、RDSのイベント、Performance Insights(
aws pi get-resource-metrics)を参照 - コードベース: 複数のリポジトリを並べた親ディレクトリを作業ディレクトリに指定。バックエンドとフロントエンドの両方を読める状態
- Slack: アラートのスレッドを読み取り専用で参照
- 書き込み: コードの修正とローカルの検証スクリプトのみ。本番への変更は人が実施
この範囲の切り方には理由があります。AWSだけを渡すと「どのクエリが重かったか」までは分かりますが、「何を直せばいいか」には届きません。 実際、負荷の正体を特定した後の作業は、リポジトリを読んでクエリの組み立て箇所を探し、件数取得が本体クエリを丸ごと使い回している構造を見つけるという流れでした。ここはコードベースがなければ成立しません。
逆にコードだけ渡しても、どのクエリが問題なのかは分かりません。両方あって初めて「原因の特定」から「修正の提案」までがつながります。 依頼を一言で済ませられたのは、この2つが揃っていたからだと理解しています。
何が起きていたか
事象としては、本番のAurora PostgreSQLでリーダーインスタンスがメモリ不足によりクラッシュし、自動再起動していました。復旧処理の最中に接続したアプリケーションが FATAL: the database system is starting up を受け取り、それがアラートとして上がってきたという流れです。
負荷の出どころは一覧取得APIの件数取得クエリでした。表示用の重い結合を含んだクエリ全体を、そのまま件数取得にも使い回していたためです。ここは修正して、読み込むデータ量を大きく削れました。
補足すると、アラートに出ていたエンドポイントは真犯人ではありませんでした。復旧中のデータベースに接続して巻き込まれただけです。
ここまでは、ほぼ丸投げで到達しています。以降が、聞き返して取り出した判断の中身です。
判定軸1: メトリクスは平均ではなく日次の最小値で見る
最初に出てきたのが、この見方でした。
見ていたのはリーダーインスタンスの空きメモリですが、平均値ではなく 日次の最小値 を並べています。平均だと、1日のうち数分だけ枯渇しても他の時間帯にならされてしまう。知りたいのは「最も苦しいときにどれだけ余裕があったか」なので、最小値のほうが症状を素直に表す、という理屈でした。
並べた結果は次のとおりです。
- 障害の前日までの約45日間: 毎日6.3〜7.2 GBで安定していて、下降トレンドなし
- 障害当日: 0.00 GB
さらに分単位で見ると、直前まで7.6 GBの空きがあった状態から約5分で0に落ちる動きが、午前中に2回起きていました。
ここから導かれる結論は明確です。データ量の増加によってメモリが慢性的に圧迫されているなら、日次の最小値は数週間かけて少しずつ先細るはずです。ところが実際の形は、45日間まったく動かず当日だけ崖のように落ちるというものでした。慢性劣化の形をしていません。
以下は形の違いを示す模式図です。縦軸は日次の最小空きメモリ、横軸は経過日数で、値そのものは実測ではなく傾向を表しています。
型として取り出すと、こうなります。
- なだらかに先細る: 慢性(データ増加、リソースリーク、断片化など)
- 横ばいから崖: 急性(バースト、特定の操作、外部からのスパイク)
45日という期間も、日次の最小値という見方も、こちらは指定していません。それでもこの形で出てきたのは、判定に必要な粒度から逆算されているからだと理解しました。
判定軸2: 該当コードがいつ入ったかを見る
次に確認されていたのが、負荷の原因となっていたクエリを組み立てている箇所の git log でした。
分かったのは、この実装が障害の約1か月前のリファクタリングで追加されたもので、それ以降このファイルには一度も変更が入っていないことです。
つまり「まったく同じ重いクエリで、1か月のあいだ問題なく動いていた」ことになります。直近のリリースが原因だという線はここで消えました。
これも型になります。コードの導入時期と障害の発生日に大きな開きがあるときは、問題の立て方そのものを切り替える必要があります。「どの変更が壊したのか」ではなく「何が閾値を超えさせたのか」を探す問題に変わるわけです。
判定軸3: 環境側の変更を確認する
最後にデータベースのパラメータ変更履歴です。作業メモリの設定などに当日の変更はなく、環境側の要因も除外できました。
空振りに終わることが多い確認ですが、やっておく価値はあります。後から「あの日に設定を触っていた」という話が出てきたときも、調査をやり直さずに済みます。
結論は「潜在要因 × 引き金」の二層になっていた
ここまでの3つから出てきた説明は、次の形でした。
- 潜在要因: 件数取得が表示用の重い結合をそのまま評価していたという構造的な非効率。1か月前から常に存在していた
- 引き金: 当日に発生した、重いエンドポイントの同時実行
単発で実行されているうちは、遅いだけで閾値には届かずアラートも鳴りません。同時に走ったときだけメモリ使用量が跳ね上がります。
この二層で書くと、報告の結論が「既知の問題を放置していた」ではなく「そもそも顕在化していなかった」に変わります。言葉遊びではなく、取るべき対策が変わります。
- 見逃していた場合: 監視の閾値やアラート設計に問題がある
- 顕在化していなかった場合: 潜在要因の除去と、引き金の検知が別々に必要になる
今回は後者だったので、恒久対応としてクエリの修正を入れ、同時実行が重なったときに気付けるかどうかは別途見ていく形にしました。
判定軸4: 検証行為そのものの副作用を見る
修正を本番へ反映したあと、効果をどう確かめるかという段になって、もう1つの判断が出てきました。
本番のリーダーで、修正前のクエリに EXPLAIN (ANALYZE) を打ってはいけない。
理由は、ANALYZEが推定プランを返すだけの機能ではないことです。実際にクエリを走らせて実測値を取るため、検証行為そのものがメモリ枯渇の引き金になりえます。原因を確かめようとして障害を再発させる形になります。
そのため、確認の手順は次のように分かれていました。
- デプロイ前:
ANALYZEなしのEXPLAIN(推定プランのみ)で新旧を比較し、不要な処理が消えることを確認する - デプロイ後: 能動的にクエリを叩かず、画面から通常どおり操作して、メトリクス側で負荷が消えたことを確認する
つまり本番では、重いものを自分から走らせずに済む方法だけを選んでいます。
これは判定軸1から3とは性質が違い、原因を突き止めるための軸ではありません。結論を確かめる段階にも判断が要るという話です。修正内容そのものより、こちらのほうが自分にとっては発見でした。
まとめ
成果物ができた時点で手を止めて、過程を聞き返しました。出てきたのは次の4点です。
- 症状のメトリクスは、平均ではなく日次の最小値で長期間並べる。先細りなら慢性、崖なら急性
- 該当コードの導入時期を
git logで確認し、直近の変更が原因かどうかを切る - 設定や環境の変更履歴を確認して、外部要因を除外する
- 本番での検証は、確認行為自体が障害を再発させないか先に確かめる
そのうえで「潜在要因 × 引き金」の二層で報告を書く。ここまで来ると、AIの出力を貼っただけの報告ではなくなります。
モデルが賢くなるほど、どこで差がつくのか
今回の調査で、モデルの能力が足りないと感じた場面はありませんでした。一言の依頼から、原因の特定、修正、本番での検証手順の提示まで到達しています。ボトルネックは賢さではなかったわけです。
代わりに結果を決めていたのは2つでした。何を渡したかと、何を持ち帰ったかです。
前者は権限の設計です。AWSだけを渡していれば原因の特定で止まり、コードベースがあって初めて修正まで届きました。ここには重要な性質があります。モデルがどれだけ賢くなっても、権限の外にあるものは推論できません。 賢さが上がるほど、渡す範囲のほうが相対的にボトルネックへ近づきます。何を見せるかを決める仕事は、当面こちらに残ります。
後者は、出てきた結論を自分の語彙へ翻訳することです。「日次の最小値の形を見る」「検証行為の副作用を確かめる」という単位まで落とすと、次に別の障害が来たときに使えます。結論をそのまま覚えても次には効きませんが、判定の形にしておけば効きます。
これから伸びていくのは主にモデル側です。こちらが伸ばせるのは、渡す範囲と、持ち帰る単位の2つだと考えています。今後はこの4つを障害対応の手順に組み込んで、毎回思い出さずに済む形へ寄せていく予定です。