kickflow Tech Blog

株式会社kickflowのプロダクト開発本部によるブログ

Disk I/O Budget 枯渇と監視を作り直した話

新規事業本部でバックエンドエンジニアをしている渡辺です。
本記事では、検証用の Preview 環境で起きたDB障害の調査と、監視を作り直すまでの記録をまとめます。 ひとことで言えば、DB が急に詰まってログインなどが不安定になった障害です(本番の実ユーザーへの影響はありません)。 学びが多かったのは復旧よりも、主要メトリクスが正常に見えて原因特定に遠回りした過程でした。

1. 前提となるアーキテクチャ

障害の話に入る前に、構成を整理しておきます。

flowchart TD
    subgraph clients["アクセス元(Preview 環境)"]
        APP["アプリ (サーバーレス)"]
        CRON["Inngest Cron (ジョブ実行基盤)"]
        PV1["Preview デプロイ #1"]
        PV2["Preview デプロイ #2"]
        PVn["Preview デプロイ #n"]
    end

    APP --> S
    CRON --> S
    PV1 --> S
    PV2 --> S
    PVn --> S

    S["Supavisor 接続プーラー<br/>(Transaction mode / PgBouncer 互換層)"]
    S --> DB[("共有 Preview DB<br/>(マネージド Postgres / Supabase)")]
    S --> AUTH["Supabase Auth"]
    AUTH --> DB

    style S fill:#fde68a,stroke:#d97706
    style DB fill:#bbf7d0,stroke:#16a34a

今回の舞台は本番ではなく Preview(検証用)環境です。 アプリや Cron は Postgres へ直接つながず、接続はすべてプーラー(Supavisor)を経由します。 そして PR ごとに作られる Preview デプロイが一つの DB を共有しており、複数の Preview のアクセスが同じ DB に集まります。

Disk I/O Budget という別軸のリソース上限

Disk I/O Budget は、CPU 使用率、メモリ使用率、接続数とは別軸のリソース上限です。 マネージド DB(Supabase を含む)は、ベースラインを超える I/O を蓄積したクレジットで処理し、使い切ると I/O 性能が絞られます。

実効的なベースラインは、DB インスタンスの規模で決まります。 今回の Preview の DB は小さめの構成で、ベースラインは スループット 22 MB/s、IOPS 1,000 と低めでした。 規模の大きい構成ではバーストの余地がなくなり、Budget という考え方自体も消えます。

この別軸のリソースを意識から外していたことが、のちの診断を遠回りさせました。

2. 何が起きたのか(症状)

Preview 環境で、二日間にわたり以下が二つの波に分かれて同時多発しました。

  • 接続プーラー(Supavisor)で、プールから接続を取得できないタイムアウトが多発しました(15 秒で発生。ログ上は ECHECKOUTTIMEOUT)。接続断ログも出ました。
  • Auth API で 429 が急増し、522 / 524 / 525 の 5xx も出ました。
  • Postgres で、プーラーの認証用クエリが 12 秒以上かかり、canceling statement due to statement timeout が出ました。
  • マネージド DB から Disk I/O Budget 枯渇の通知を受信しました。
  • Preview 環境ではログインなど普段の操作までタイムアウトしました(本番の実ユーザー影響なし)。
timeline
    title Preview 環境の障害と再発防止
    第1波 : Cron 失敗が多発 : 接続取得タイムアウト多発 : Auth 429 / 5xx 増加
    調査(遠回り) : 主要メトリクスは正常 : DB は問題なしと誤判断 : 待機数 100 に違和感 異常か不明
    第2波 : 接続取得タイムアウト再発
    原因の絞り込み : ログを時系列で突き合わせ : Cron 高頻度と 失敗リトライ集中を特定
    対応 : Cron 頻度 2分→1時間 : プーラー設定調整 : Budget 枯渇通知を確認 : Compute アップグレード
    収束 : 接続数が 1〜10 に低下 : タイムアウトが沈静化
    再発防止 : Datadog に多層モニターを新設

第1波と第2波のあいだに恒久対応は打てず、原因を絞り込めないまま暫定緩和も入れられずに再発しました。 この「一度目で診断しきれず二度目を招いた」ことが、監視を作り直す動機になりました。

3. なぜ「DB は正常」と読み違えたか

最初に見た主要メトリクス(CPU 使用率、メモリ使用率、接続数)は、どれも正常範囲でした。 そのため「リソースは足りている、DB 本体は悪くなさそうだ」と判断し、エラーがプーラーや Auth 側で出ていたことから意識は「アプリや SDK のリトライの問題では」に向かいました。

これが最初の見誤りです。 Disk I/O Budget は主要メトリクスとは別軸で、これらが正常でも使い切れば DB の応答性は落ちます。

引っかかる指標が一つだけありました。プーラーの待機数が 100 近くまで上がる瞬間があったことです。 「何かが起きている」とは感じたものの、異常だと確信できませんでした。 平常時の待機数を監視しておらず、100 が異常なのかありうる値なのか判断できなかったからです。 平常値を知らない指標は、異常が出ても異常と判断できません。 これが二つ目の、より本質的なつまずきでした。

4. ログとメトリクスを時系列で突き合わせる

単一のメトリクスで白黒がつかない以上、複数のログとメトリクスを時系列で突き合わせました。

  • ジョブ実行基盤(Inngest)で、ある Cron が約 2 分間隔で走り、同時期に Failed が大量発生していました。実行と失敗リトライが重なり、DB アクセスが短時間に集中した疑いがあります。
  • プーラーの接続取得タイムアウトの多発時間帯が、この Cron の失敗多発と一致していました。
  • Postgres のログには、認証クエリの遅延と statement timeout が残っていました。
  • プーラーの接続数は障害時に 100 付近、対応後は 1〜10 で、この差が障害時の異常を事後に裏づけました。
  • pg_stat_statements に高 I/O クエリは見当たりませんでした。ただし DB 再起動と収束のあとの状態なので、事象の最中の証拠にはなりません。過去事象の追跡には、収束後のスナップショットより時系列のログとメトリクスが役立ちました。

単一メトリクスの絶対値ではなく、複数ソースの時系列を突き合わせて初めて因果が見えました。

5. 仮説としての障害の連鎖

集めた証拠から、次の連鎖が起きていたと考えています。 確度の高い仮説であり、単独の根本原因とは断定していません。

flowchart TD
    S1["① Cron が高頻度で実行<br/>(約2分間隔)"] --> S2["② 一部 Run が失敗しリトライ発生"]
    S2 --> S3["③ 複数 Preview が同一 DB を共有し<br/>実行がさらに重複"]
    S3 --> S4["④ DB 接続数・クエリ・Disk I/O が急増"]
    S4 --> S5["⑤ Disk I/O Budget を消費し<br/>DB 応答性が悪化"]
    S5 --> S6["⑥ プーラーが接続を checkout できず<br/>接続取得タイムアウト多発"]
    S6 --> S7["⑦ Auth / API の失敗とリトライで<br/>負荷がさらに増幅"]
    S7 -.->|リトライが負荷を押し上げる<br/>フィードバックループ| S4

    style S5 fill:#fecaca,stroke:#dc2626
    style S6 fill:#fecaca,stroke:#dc2626

高頻度の Cron と失敗リトライが短時間に集中し、共有 DB で重なって Disk I/O が急増しました。 Budget を消費して応答性が落ち、プーラーの接続取得タイムアウトと、Auth や API のリトライが負荷を増幅しました。

確度の整理

確度が高いと考えていること:

  • Cron の Failed 増加と、プーラーの timeout 発生時間帯が一致しています。
  • DB 側の応答遅延を示す Postgres ログと、Disk I/O Budget 枯渇の通知が実際にあります。
  • Cron 頻度の調整、プーラー設定の調整、Compute アップグレードののちにエラーが沈静化しました。

まだ断定できないこと:

  • Cron が単独の根本原因だったか。
  • 複数の Preview デプロイがどの程度、実行数を増幅していたか。
  • どの SQL が Disk I/O Budget の消費に最も寄与したか。

6. 実施した対応と、残した課題

まず、目の前の負荷を下げる手当てを打ちました。

  • Cron の実行頻度を約 2 分間隔から 1 時間に 1 回程度へ下げました。
  • 接続プーラー(Supavisor)の設定を調整しました。
  • 失敗時リトライの扱いを見直しました。
  • DB の Compute をアップグレードしました。ベースライン性能と Budget は Compute サイズに紐づくため、これがベースラインを引き上げます。

これで接続取得タイムアウト系のエラーは沈静化し、接続数も平常に戻りました。

ただしこれらは、負荷に対する余力を増やす手当てであって、負荷を生む側の構造を消したわけではありません。 高頻度で走るジョブの負荷設計や、環境をまたいでリソースを共有する構成には、まだ見直しの余地が残っています。 こうした構造の作り直しは影響範囲が大きく、一度に片づけられるものではありません。

そこで今回いちばん力を入れた恒久対応は、構造を一気に潰すことではなく、次に同じ枯渇へ近づいたときに前もって気づける監視を持つことにしました。 残った構造課題は、その監視で早期に捉えながら、優先度をつけて段階的に閉じていきます。 その監視の作り直しを、次章で述べます。

7. 監視を異常が判断できる形に作り直す

今回の収穫は、この作り直しにあります。 遠回りの原因は二つとも監視の欠陥でした。 主要メトリクスしか見ておらず別軸のリソースやプーラー層を捉えられなかったことと、平常値からの変化を監視していなかったことです。

制約が一つありました。 Budget の残量や消費率に当たるメトリクスは Metrics API にも Datadog 連携にも無く、直接監視することができません。 そこで方針を「生の I/O から枯渇に向かう状態を間接的に捉える」に切り替えました。 Budget が減るのはベースラインを超えている時間だけなので、見るべきは超過がどれだけ続くかです。

一本のモニターに頼るのをやめ、検知タイミングの違うシグナルを層として重ねました。

Layer 監視するもの 検知タイミング 実現
0 Supabase 純正の Budget 枯渇通知 遅行 通知メールを Slack へ転送
1a スループット比(実測 ÷ ベースライン 22 MB/s) 先行 追加
1b IOPS 比(実測 ÷ ベースライン 1,000) 先行 統合に完了数メトリクスがなく不可
2a ディスク利用率 io_time 同時 追加
2b I/O キュー深度 aqu-sz 同時 追加
3 プーラー異常シグナル(接続取得タイムアウト等) 遅行 追加

設計の要点は三つです。

  • 先行(Layer 1)を主役にする:スループットの実測をベースラインで割った比を継続で見て、枯渇の前に気づきます。これが「別軸のリソースを枯渇の前に監視する」という教訓への回答です。
  • 組めない指標は制約として残す:IOPS 側の先行指標(Layer 1b)は、連携に完了 I/O 数がなく作れませんでした。既知の制約として残し、下位の層で代替します。
  • 閾値は平常値を見てから置く:平常を知らなければ、異常も判断できないからです。

同時(Layer 2)は本当に詰まっているかの裏づけ、遅行(Layer 0 と Layer 3)は起きたことの確認に回します。 症状だけを見ていた Before から、先行指標を先頭に置く After への組み替えです。

Before / After

Before(障害当時) After(見直し後)
DB の見方 CPU、メモリ、接続数が中心 ベースライン超過(Disk I/O 系)を先行指標として追加
ベースライン 平常値を持たず、異常を判断できず 平常のキュー深度を基準に閾値を設定
監視の層 プーラーの症状のみ 先行(Layer 1)、同時(Layer 2)、遅行(Layer 0 と 3)の多層
監視できない部分 認識していなかった IOPS 先行指標の欠落を制約として残し、下位の層で代替

8. おわりに

この障害でいちばん学んだのは、監視が「いまが正常か異常か」の一軸に寄っていて、平常の状態が時間とともにどう動くかを追う指標が足りていなかったことです。 Disk I/O Budget の枯渇は、ある瞬間の値が正常でも、平常が少しずつ限界へ寄っていく推移として進みます。 一点で正常を確認するだけの監視は、この種のゆっくりした変化を最後まで気づけません。

  • 点ではなく推移を見る:Budget が減るのはベースラインを超えている間だけなので、効くのは瞬間値ではなく「超過がどれだけ続いたか」という時間方向の量です。監視すべきなのは、ある時点の高さより状態の動き方そのものでした。
  • 取れないメトリクスを、間接的に監視する:枯渇を直接示す指標はなく、API にも連携にも流れてきませんでした。生の I/O から枯渇へ向かう推移を組み直し、それでも作れない IOPS の先行指標は既知の制約として残し、下位の層で代替しました。穴の位置がわかっている監視のほうが、隙が無いように見えて実は取りこぼす監視より当てになります。

kickflow では、こうしたマネージドサービスの運用や監視設計に一緒に向き合ってくれるエンジニアを募集しています。 ぜひ採用サイトをご覧ください。 careers.kickflow.co.jp