0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リトライが殺意を持つとき -- 制御ロジックの暴走が本番を殺す【デバッグスキル付】

0
Last updated at Posted at 2026-04-28

「あなたのリトライが私たちを殺しています。」

あるサービスチームが、依存先チームからこのメッセージを受け取りました。障害の真っ最中に、依存先のAPIがタイムアウトしている。当然、リトライする。3回、5回、10回。クライアント側は正しい動作をしているつもりです。

しかし依存先から見れば、障害で処理能力が半分に落ちているところに、通常の数倍のリクエストが殺到している状態でした。リトライが障害を悪化させ、回復を妨げていた。

これはどこかの寓話ではありません。2012年8月、Knight Capitalの話です。デプロイ時に旧コード(Power Peg)が誤って活性化され、45分間に数百万件の注文を自動生成。4.4億ドルの損失を出して実質破綻しました。注文が「完了」とマークされず、システムが注文を再生成し続けた。構造としては「フィードバックループが閉じていない無限再実行」で、リトライ暴走と同じ力学です。

リトライは障害を乗り越えるための仕組みです。しかし設計を誤ると、リトライそのものが障害の原因に変わります。

自分自身と戦うシステム -- 3つの暴走パターン

Michael Nygardは著書 Release It! で、本番システムが自分自身を攻撃するパターンを複数挙げています。リトライ関連の暴走パターンは3つです。

ドッグパイル

キャッシュの有効期限が切れた瞬間、全クライアントが同時にオリジンサーバーへリクエストを送る。通常時は1秒あたり100リクエストのサービスが、ドッグパイル時には数千リクエストを同時に受ける。正常に戻ったはずのサービスが、一斉リトライの波で再び倒れる。

障害から復旧した瞬間が最も危険です。キューに溜まったリクエストが一斉に流れ込むので、復旧直後に再度ダウンする。これを繰り返すと、オンコール担当の精神が先にダウンします。

カスケーディング障害

サービスAがBに依存し、BがCに依存する。Cが遅延すると、Bのスレッドがブロックされる。Bのスレッドが枯渇すると、Aのリクエストもブロックされる。1つのサービスの遅延が、依存チェーンを通じて全体に波及します。

ここでの厄介なポイントは、遅延がエラーより悪質だということです。エラーは速く返るのでリソースを長時間占有しません。しかし遅延はスレッドやコネクションを占有し続ける。Nygardの言葉を借りれば、「遅いレスポンスは、エラーよりも悪い」。

遅いレスポンスの罠

タイムアウト30秒のHTTPクライアントが、遅延するサービスを呼び出す。30秒間スレッドが占有される。その間にリクエストが積み上がり、スレッドプールが枯渇する。

タイムアウトが長すぎればリソースを長時間食い潰し、短すぎれば正常な処理まで中断する。「適切なタイムアウト値」は見た目以上に難しい問題です。私はこれまで「デフォルト値のままでした」という告白を何度も聞いてきました。自分もやっていた時期があるので、人のことは言えませんが。

リトライ設計の3原則

リトライそのものが悪なのではありません。問題は「考えなしのリトライ」です。

ただし大前提として、リトライ先のAPIが冪等(同じリクエストを複数回送っても結果が変わらない)でなければ、どんなリトライ戦略も安全にはなりません。POST /orders を3回リトライしたら注文が3件できた、という話は笑えません。

原則 やること やらないと何が起きるか
指数バックオフ リトライ間隔を1→2→4→8秒と倍増 全クライアントが同時にリトライし続ける
ジッター バックオフにランダムな揺らぎを加える バックオフの波が同期してスパイクが周期的に発生
リトライバジェット システム全体のリトライ割合に上限を設ける 個々は合理的でも、集団として破壊的になる

指数バックオフだけでは足りない

即座にリトライするのではなく、リトライ間隔を徐々に広げる。これは基本中の基本です。しかし指数バックオフだけでは不十分です。全クライアントが同じタイミングでリトライを開始すると、1秒後、2秒後、4秒後にそれぞれスパイクが発生する。バックオフの波が同期してしまうのです。

ジッターで波を崩す

バックオフにランダムな揺らぎを加えます。

import random

def retry_with_jitter(attempt, base=1, max_delay=60):
    delay = min(base * (2 ** attempt), max_delay)
    jitter = random.uniform(0, delay)
    return jitter

AWSのブログ「Exponential Backoff And Jitter」(2015)で、Full Jitter戦略が推奨されています。リトライタイミングが分散され、ドッグパイル効果が軽減されます。

リトライバジェット -- 集団行動の制御

「直近1分間のリクエストのうち、リトライが占める割合が20%を超えたら、新規のリトライを停止する。」

Google SREハンドブックで紹介されているアプローチです。個々のクライアントは自分のリトライが「合理的」だと判断しています。しかし全員が同時にリトライすれば、集団として破壊的になる。リトライバジェットは、この「合成の誤謬」に対処します。

交通渋滞と同じです。1台が車線変更するのは合理的。全員が同時に車線変更すると、渋滞が悪化する。

本番デバッグ時の5つのチェックポイント

障害の原因がリトライや制御ロジックにある場合、以下の5点を確認してください。

# チェック項目 確認すること 危険サイン
1 リトライ間隔 指数バックオフ+ジッターが実装されているか sleep(1) のベタ書き
2 リトライ上限 最大リトライ回数が設定されているか while True + retry
3 タイムアウト値 デフォルト値のまま放置されていないか タイムアウト未設定、または30秒超
4 サーキットブレーカー 依存先障害時にリクエストを止める仕組みがあるか 障害中も全リクエストを投げ続ける
5 フィードバックループ 処理完了が正しく記録されるか 完了マークなしの処理が再実行される

Knight Capitalの事故では、項目2と項目5の両方が欠落していました。注文に上限がなく、完了マークがつかないので再生成し続けた。2つの欠落が重なった結果が4.4億ドルです。

制御ロジックの本質的な怖さ

ここまで読んで「うちのリトライ設定は大丈夫だろう」と思った方。私も以前はそう思っていました。本番障害で初めて「あれ、リトライ何回まで?」と調べて、答えが見つからなかったときの焦りは忘れられません。

正常系のコードは毎日何百万回も実行され、バグがあればすぐに気づきます。しかし制御ロジック -- リトライ、タイムアウト、バックオフ、サーキットブレーカー -- は障害時にしか実行されません。障害は頻繁には起きないので、制御ロジックのバグは長期間潜伏します。そして本当に必要な瞬間に、期待通りに動かない。

「障害を乗り越えるための仕組み」が「障害を悪化させる仕組み」に変わる。このパラドックスが、制御ロジックの本質的な難しさです。

だからこそカオスエンジニアリングが必要になります。本番環境で意図的に障害を起こし、制御ロジックが想定通りに動くか検証する。平常時に制御ロジックをテストする唯一の方法は、平常時に障害を作ることです。矛盾しているようですが、それが現実の運用です。

まとめ

リトライは味方にも敵にもなります。

  • ドッグパイル、カスケーディング障害、遅いレスポンス -- 暴走パターンを知らなければ、自分のリトライが何を引き起こしているか気づけません
  • 指数バックオフ + ジッター + リトライバジェット -- この3つをセットで実装して初めて、リトライは安全になります
  • 制御ロジックのバグは障害時まで眠っている -- 平常時のテストでは見つからない

あなたのサービスのリトライ設定、最後に確認したのはいつですか? grep -r "retry" src/ を叩いてみると、思わぬ発見があるかもしれません。

付録: リトライデバッグスキル(コピペ用)

以下をそのまま CLAUDE.md やAIエージェントのスキル定義に貼ると、障害時のリトライ関連チェックをAIに実行させられます。

# リトライ・制御ロジックデバッグ

障害が発生し、リトライや制御ロジックが原因として疑われる場合に実行する。

## 初動: 5つのチェックポイント

以下を順番に確認し、各項目の結果を報告すること。

1. **リトライ間隔**: 指数バックオフ+ジッターが実装されているか。`sleep(1)` のベタ書きは危険サイン
2. **リトライ上限**: 最大リトライ回数が設定されているか。`while True` + retry は無限ループと同義
3. **タイムアウト値**: デフォルト値のまま放置されていないか。未設定または30秒超は要確認
4. **サーキットブレーカー**: 依存先障害時にリクエストを止める仕組みがあるか
5. **フィードバックループ**: 処理完了が正しく記録されるか。完了マークなしの処理が再実行される構造は致命的

## 検出コマンド

    # リトライ関連コードの洗い出し
    grep -rn "retry\|retries\|max_attempts\|backoff\|jitter" src/
    # タイムアウト設定の確認
    grep -rn "timeout\|TIMEOUT\|time_out" src/
    # サーキットブレーカーの有無
    grep -rn "circuit\|breaker\|CircuitBreaker" src/

## 判定基準

- 5項目すべてに明示的な設定がある → 🟢 問題なし
- 1-2項目が欠落 → 🟡 改善推奨(欠落箇所と修正案を提示)
- 3項目以上が欠落、またはリトライ上限なし → 🔴 即時対応(Knight Capital型の暴走リスクあり)

## 前提条件の確認

リトライ設計の前に、以下の前提が満たされているか確認すること。

- リトライ先APIは冪等か(同じリクエストを複数回送っても結果が変わらないか)
- 冪等でない場合、冪等キー(Idempotency-Key ヘッダ等)の仕組みはあるか

参考文献

  • Michael Nygard, Release It! (2007, 2018 2nd Edition) -- 安定性アンチパターンの原典
  • Google SRE Handbook, Chapter 22: "Addressing Cascading Failures" -- リトライバジェットの解説
  • AWS Architecture Blog, "Exponential Backoff And Jitter" (2015) -- Full Jitter戦略の提案
  • SEC Filing: Knight Capital Group, Form 10-Q (2012) -- 4.4億ドル損失の詳細
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?