はじめに
この記事では、Webアプリケーションのリクエスト処理中に重いDB操作を置いたとき、どのようにDB接続枯渇へつながるかを整理します。
題材にするのは、次のような設計です。
- 画面表示のたびに、一覧用データを作り直す
- 作り直しの前処理として、既存データを広い範囲で削除する
- 削除、外部API取得、保存を1つのリクエスト内で行う
- 同時アクセス時に、同じ更新処理が並列に走る
- ロック待ちや外部通信待ちの間、DB接続が返却されない
- 結果として、後続リクエストまでDB接続を取得しにくくなる
先に結論
リクエスト処理の中に、全件削除や外部API待ちのような重い処理を置くと、単体では問題なく見える処理でも本番では障害要因になります。
特に危険なのは、次の条件が重なる場合です。
- ユーザーアクセスごとに実行される
-
DELETEやTRUNCATEに近い広範囲の更新がある - 外部API呼び出し中にDB接続を掴んだままにしている
- 同時アクセス時の試験をしていない
- 条件分岐の一部だけを確認してリリースしている
この手の問題は、ローカルや単発の動作確認では見つかりにくいです。
負荷、ロック、接続数、外部通信の待ち時間が重なったときに表面化します。
何が起きるのか
例えば、ある画面で一覧データを表示するとします。
その一覧は外部APIから取得し、アプリケーション側のDBに一時保存してから表示する設計だとします。
単純に考えると、次の流れになります。
- ユーザーが一覧画面へアクセスする
- 既存の一覧データを削除する
- 外部APIから最新データを取得する
- 取得したデータをDBへ保存する
- 画面へ一覧を返す
一見すると分かりやすい処理です。
古いデータを消して、新しいデータを入れるだけです。
しかし、本番ではこの「古いデータを消す」が重くなります。
この処理が1アクセスだけなら問題にならないかもしれません。
問題は、これが複数ユーザーのアクセスで同時に実行されることです。
広範囲の削除はロックと待ちを生みやすい
一覧データを広い範囲で削除する処理は、対象件数やテーブル設計によっては重くなります。
例えば次のようなSQLです。
DELETE FROM cached_list_items;
または条件があっても、実質的に広い範囲を消すSQLです。
DELETE FROM cached_list_items
WHERE list_type = 'external';
このような削除は、行ロック、インデックス更新、外部キー制約、トランザクションログなどの影響を受けます。
実行中は他のSQLが待たされることがあります。
さらに、同じ処理がユーザーアクセスごとに走ると、次の状態になります。
- 最初のリクエストが削除処理を実行する
- 後続リクエストも同じ削除処理を実行する
- ロック待ちが発生する
- 待機中のDB処理が増える
- アプリケーションが借りたDB接続を返却できない
- アプリケーションのコネクションプール、またはDBの最大接続数に近づく
この状態になると、障害は対象画面だけで止まりません。
同じDBを使う他の機能も、接続取得待ちやタイムアウトの影響を受けます。
結果として、問題のある画面以外にも影響が広がります。
外部API待ちとDB接続保持が重なると危険
もう1つ危険なのは、外部API呼び出し中にDB接続を掴んだままにすることです。
例えば次のような処理です。
- DBトランザクションを開始する
- 既存データを削除する
- 外部APIへリクエストする
- APIレスポンスを待つ
- 取得データをDBへ保存する
- コミットする
この場合、外部APIの応答が遅いと、その間もDB接続やロックを保持し続けます。
外部APIの遅延はアプリケーション内で制御しにくいです。
ネットワーク、相手先の負荷、タイムアウト設定によって待ち時間が伸びます。
その待ち時間の間にアクセスが増えると、DB接続が戻らないまま積み上がります。
外部通信とDBトランザクションを同じ処理に詰め込むと、外部の遅さがDB障害として現れます。
単体の動作確認では見つかりにくい
この問題は、単発の試験では見つかりにくいです。
例えば手元で1回アクセスして、画面が表示されることを確認しても、次の観点は見えません。
- 同時アクセス時に同じ削除処理が重ならないか
- 削除処理がロック待ちを作らないか
- 外部APIが遅いときにDB接続を保持し続けないか
- 条件分岐によって広範囲の削除が走るケースが残っていないか
- 失敗時にトランザクションが長時間残らないか
コード上は小さな変更に見えても、実際には実行タイミングが変わることがあります。
例えば、以前は管理バッチで1回だけ実行されていた処理が、画面アクセスごとに実行されるようになると、負荷の性質は大きく変わります。
差分の行数が少なくても、実行回数と実行タイミングが変わる修正は慎重に見る必要があります。
設計として避けたいこと
リクエスト処理の中では、次の設計を避けた方が安全です。
- ユーザーアクセスごとに全件削除する
- 外部API待ち中にDBトランザクションを開いたままにする
- 更新対象が広いSQLを画面表示のたびに実行する
- キャッシュ更新と画面表示を同じ責務にする
- 失敗時のロールバックやリトライ方針が曖昧なままにする
画面表示のリクエストは、できるだけ短く終わるべきです。
DB更新や外部API同期のような処理は、画面表示から切り離す方が運用しやすくなります。
代わりにどう設計するか
一覧データを更新したい場合は、リクエストごとに全削除して作り直すのではなく、更新の責務を分けます。
代表的な選択肢は次のとおりです。
- 定期バッチで外部APIから取得する
- 非同期ジョブで更新する
- 更新中でも古いデータを返せるようにする
- 差分更新にする
- ステージングテーブルに投入してから切り替える
- テナントやカテゴリ単位で更新範囲を限定する
重要なのは、ユーザーのアクセス数と重い更新処理の実行回数を直結させないことです。
例えば、次のような流れにできます。
この形なら、ユーザーアクセスのたびに外部API取得や全削除を実行しなくて済みます。
画面表示は参照中心になり、更新処理はジョブ側で制御できます。
削除より切り替えを考える
大量データを入れ替える場合は、いきなり本番テーブルを削除するより、切り替え方式を検討します。
例えば次のような方法です。
- ステージングテーブルに新しいデータを入れる
- データ件数や必須項目を検証する
- 問題なければ参照対象を切り替える
- 古いデータを後で削除する
この方式にすると、更新中でも既存データを読み続けられます。
もちろん実装は少し複雑になります。
しかし、画面表示のリクエストで全削除するより、障害時の影響範囲を小さくできます。
テストで見るべき観点
この種の修正では、正常系だけでは不十分です。
テスト仕様として、少なくとも次の観点を入れた方がよいです。
- どの条件で削除処理が実行されるか
- 削除対象が想定範囲に収まっているか
- 同時アクセス時に同じ更新処理が多重実行されないか
- 外部APIが遅い場合にDB接続を保持し続けないか
- 外部APIが失敗した場合に古いデータを残せるか
- タイムアウト時に処理が中断されるか
- リトライで同じ削除が繰り返されないか
特に、条件分岐の中にある削除処理は見落としやすいです。
「小さな整理」に見える修正でも、分岐の通り方が変わると本番で実行されるSQLが変わります。
コードレビューでは差分だけでなく、実行回数と実行条件を見る必要があります。
観測するべき指標
本番で早く気づくには、アプリケーションログだけでは足りません。
次の指標を見えるようにしておくと、接続枯渇の兆候を掴みやすくなります。
- DB接続数
- コネクションプールの使用率
- SQLの実行時間
- ロック待ち時間
- 待機中のSQL
- 外部APIの応答時間
- タイムアウト件数
- リクエスト数と更新ジョブ実行数
DB接続数が増えているだけでは、原因は分かりません。
どのSQLが待っているのか、外部API待ちと同時に増えていないか、同じ削除SQLが大量に並んでいないかを見る必要があります。
再発防止はテスト仕様書だけでは足りない
再発防止として、テスト仕様書を作ることは重要です。
ただし、テスト仕様書だけに寄せると、同じ構造の問題を防ぎきれないことがあります。
必要なのは、設計、実装、試験、監視のそれぞれで止めることです。
- 設計: リクエストごとの全削除を避ける
- 実装: 外部API待ち中にDB接続を保持しない
- 試験: 同時実行、失敗、タイムアウトを確認する
- レビュー: 実行回数と実行条件を見る
- 監視: DB接続数、ロック待ち、遅いSQLを検知する
テスト仕様書は最後の確認として有効です。
しかし、危険な処理が設計として残っていると、確認漏れがそのまま障害になります。
まとめ
リクエスト処理中の全削除は、単体では単純に見えます。
しかし、同時アクセス、ロック待ち、外部API待ち、DB接続保持が重なると、対象画面を越えて広い範囲に影響する原因になります。
ポイントは次のとおりです。
- ユーザーアクセスごとに重い削除処理を実行しない
- 外部API待ち中にDB接続やトランザクションを保持しない
- 一覧更新はリクエスト処理から切り離す
- 全削除より差分更新や切り替え方式を検討する
- テストでは同時実行、失敗、タイムアウトを見る
- 監視ではDB接続数とロック待ちを追う
障害の原因は、1つのSQLだけではないことが多いです。
SQLの実行場所、実行回数、待ち時間、接続保持が重なったときに問題になります。
そのため、DBを触る処理では「このSQLは正しいか」だけでなく、「このSQLはいつ、何回、どの接続を持った状態で実行されるか」まで見る必要があります。
外部連携が絡む障害調査の難しさについては連携システムの障害調査が難しくなる理由と内製化の判断軸でも整理しています。
DBとアプリケーションの責務分担については状態遷移の制御はDBとアプリケーションで役割を分けるも関連します。