「リトライストーム」はなぜ起きるのか——Uberが本番障害から学んだマイクロサービスの連鎖停止とその防ぎ方

はじめに: 「再試行」がシステムを殺すとき

「一時的なエラーなら、リトライ(再試行)すればいい」。分散システムを設計したことがある人なら、誰しも一度はこう考えたことがあるはずです。実際、AWSのアーキテクチャガイドでも、一時的な障害に対するバックオフ付きリトライはベストプラクティスとされています。しかし、この「リトライは善」という常識は、システムが劣化した瞬間に崩れ落ちます。

2026年9月17日、Uberがエンジニアリングブログ「How Uber Protects Against Retry Storms」を公開し、Hacker Newsでも話題になりました。テーマは「リトライストーム」——再試行の連鎖が嵐のようにシステム全体を飲み込み、単一サービスの軽微な劣化がプラットフォーム全体の停止にまで拡大する現象です。

Uberはこの記事で、2025年11月18日に実際に起きた大規模障害を振り返っています。コールチェーンの5階層以降にあったCore Entityサービスがインフラ障害で高エラー率になると、エラーは上位サービスへ急速に伝播。もし従来のリトライバジェット運用のままなら、劣化したサービスへのトラフィックが46%〜135%も増加し、障害が長期化していたといいます。最終的にUberの新機構は、直近の呼び出し元で最大20万リクエスト、全体では950万件もの無駄なリトライを自動的に停止させました。

では、なぜ「1回のリトライ」がこれほどの破壊力を持つのでしょうか。鍵は、マイクロサービスの依存チェーンが深いほど、リトライの増幅が指数関数的に効いてしまう点にあります。1つのサービスが1回だけリトライしても大した問題にはなりません。しかし、呼び出しチェーンのすべての層が「自分は1回だけ」とリトライすると、下流のサービスは突如として数倍のリクエストを処理させられるのです。

本記事では、Uberの一次情報をもとに、①リトライストームが発生するメカニズムを数式と図で理解し、②サーキットブレーカーやバックオフといった従来対策がなぜ不十分なのかを検証し、③Uberがたどり着いた「エラーオーナーシップ」という発想までを順に解説します。ホームサーバーでnginxやcron、メッセージキューを運用している方にも応用できる内容ですので、最後までお付き合いください。

リトライストームはなぜ起きる? 増幅の仕組みを数式と図で理解する

毎ホップ1回のリトライが「8倍」を生む

Uberのブログでは、1:1のファンアウトを持つシンプルなコールチェーン A→B→C→D→E→F→G を例に、リトライ増幅の数学が示されています。各ホップで1回のリトライ(R=2回の試行)を行う設定で、Dでエラーが発生した状況を考えます。

深さdのノードが処理するリクエスト数は R^d × η(ηはAへの総リクエスト数)で表されます。つまり、Dでエラーが発生すると、D以降のノードは毎ホップでリクエストが倍増し、深さ3のDでは 8×η のリクエストを処理させられるのです。「各サービス1回ずつしかリトライしていない」のに、下流では8倍の負荷が発生する——これがリトライストームの基本的な力学です。

flowchart TD
    A["Node A<br/>リクエスト η"] --> B["Node B<br/>2η"]
    B --> C["Node C<br/>4η"]
    C --> D["Node D<br/>8η ⚠ エラー発生"]
    D --> E["Node E<br/>8η"]
    E --> F["Node F<br/>8η"]
    F --> G["Node G<br/>8η"]
    C -. "リトライ#1" .-> D
    B -. "リトライ#1" .-> C
    A -. "リトライ#1" .-> B

上の図のように、エラー発生箇所(D)より上のすべての層が「一時エラーかも」とリトライするため、下流ほど増幅が積み重なります。ファンアウトが1:2や1:3になれば、この数字はさらに爆発的に膨らみます。

対策ごとの増幅倍率を比較する

Uberのブログでは、同じチェーンに対する3つの戦略の違いが数値で示されています。

戦略 各ノードのリトライ権 D〜Gの処理リクエスト数(η比) 増幅率
毎ホップ1リトライ(R=2) 全ノードが1回のリトライ権を持つ 8×η 800%
リトライバジェット B=10% 全ノードが10%まで (1+B)^d × η ≈ 1.33×η 133%
エラーオーナーシップ エラー発生源の辺(C→D)のみ 1.1×η 110%

リトライバジェット(全リクエストの10%までリトライを許す方式)は、毎ホップの無制限リトライを1.33倍まで抑えられるため有効です。しかしUberが指摘するのは、それでも「すべてのノードがリトライ権を持っている」という構造が変わらないことです。エラーと無関係な上位層までがバジェットの範囲内でリトライを試みるため、無駄なリクエストは残ります。対してエラーオーナーシップは、リトライ権をエラー発生源の辺(この例ではC→D間)だけに局所化し、D以降を1.1倍——ベースライン+10%で済ませます。

「リトライは可用性を上げる」という直感の反例

もう一つ重要な数字があります。可用性99.9%(エラー率0.1%)のサービスなら、10%のリトライバジェットでリトライ後の可用性は99.9999%まで向上します。ところが、可用性80%(エラー率20%)のサービスは、リトライしても88%にしかなりません。

これは偶然の落とし穴ではありません。システムが高エラー率に陥っているとき、エラーは独立ではなく相関しています——過負荷、DBの不良ホスト、シャーディングの問題といった「リトライでは直らない原因」があるためです。壊れたものにリクエストを送り直しても、壊れたまま返ってくる確率が高い。つまり「リトライは常に可用性を上げる」という直感は、まさにシステムが劣化したときに破綻するのです。この事実が、後述するUberのエラーオーナーシップという発想の基盤になっています。

バックオフ+ジッタは「量」の問題を解決しない

「ならば待ち時間を入れればいいのでは?」と考えるのが、AWS「Exponential Backoff and Jitter」で定番となっているエクスポネンシャルバックオフ+フルジッタ(待ち時間を random(0, min(cap, base * 2^attempt)) で完全ランダム化)です。同期されたクライアント群が同時に再試行するサンダリングハードを防ぎ、タイミングのスパイクを平準化する効果はAWSのシミュレーションでも実証されています。

しかし、これらはあくまで「いつリトライするか」の問題への答えです。Uberの数学が示すとおり、深いチェーンでの「リトライ総量」の指数増幅(R^d)そのものは、待ち時間をどれだけランダム化しても解決しません。次章で見るサーキットブレーカーも含め、従来対策には共通の限界があるのです。

サーキットブレーカーだけではなぜ防げないのか?

「失敗が続いたら一定時間リクエストを遮断する」サーキットブレーカーは、マイクロサービスの定番防衛機構です。事実、障害の悪化を検知して下流を守るという意味で有効なコンポーネント保護ですが、UberのブログとGoogle SRE Bookの知見を組み合わせて分析すると、リトライストームに対しては次の3つの構造的な弱点が起こり得ると考えられます(以下は本記事の分析です)。

盲点1: 閾値に達するまでの「暴走できる時間」

サーキットブレーカーは「一定回数の失敗」を検知してから初めて遮断します。つまり、閾値に達するまでの時間帯は、各ノードが自由にリトライを続けられます。問題は、深いコールチェーンでは複数の層が「まだ自分の閾値には達していない」状態で同時にリトライを重ねることです。前章のR^dの増幅は、どの層のサーキットも開いていない間に進行します。

盲点2: 半開状態での再同期

一度遮断したサーキットは、回復判定のために「半開」状態で少人数の試験リクエストを流します。ここで、複数のノードからの試験リクエストが同時に押し寄せると、まだ回復しきれていない下流に対して再び小規模なストームが発生し得ます。AWSのブログが指摘するサンダリングハードと同型の構造で、この場合はタイミングの同期が問題を再発させる可能性があります。

盲点3: 「伝播エラー」と「自分のエラー」を区別できない

これがUberがブログで指摘する中心的な問題です。サーキットブレーカーが見るのは自ノードから見た失敗回数だけで、「下流のせいで失敗した」のか「自分自身が失敗した」のかを知りません。その結果、上位層は下流から伝播してきただけのエラーに対しても、無差別にリトライを実行してしまいます。リトライは一律(uniformly)に適用され、中度・重度の劣化時には逆効果になります。

実際の失敗の連鎖をシーケンス図で見る

3つの盲点が組み合わさると、次のような連鎖が起きます。

sequenceDiagram
    participant A as Node A
    participant B as Node B
    participant C as Node C
    participant D as Node D(劣化中)

    A->>B: リクエスト
    B->>C: リクエスト
    C->>D: リクエスト
    D-->>C: エラー(過負荷)
    Note over C: サーキットはまだ閾値前<br/>→ リトライ実施
    C->>D: リトライ(増幅)
    D-->>C: エラー
    C-->>B: 伝播エラー(unclaimなし)
    Note over B: 「伝播か自分か」区別できない<br/>→ リトライ実施
    B->>C: リトライ(さらに増幅)
    C->>D: リトライ(8×ηへ増幅)
    D-->>C: エラー連鎖
    B-->>A: 伝播エラー
    Note over A: 上位もリトライ…<br/>ストームが全体へ拡大

Dの劣化という「たった1つの原因」に対して、C・B・Aすべてがリトライを重ねる構造が見えるはずです。Uberの表現を借りれば、C・B・Aが返しているエラーは「症状(symptom)」にすぎないのに、それぞれが「原因(cause)」であるかのように振る舞ってしまうのです。

それでもサーキットブレーカーが無意味なわけではない

誤解しないでください。サーキットブレーカーは「悪化を検知してコンポーネントを遮断する」防御として引き続き有効です。Uber自身、サーキットブレーカーとエラーオーナーシップ・リトライバジェット・バックオフを補完関係として位置づけています。

一方のGoogle SRE Book「Handling Overload」も同様の前提に立ちます。データセンター内の大部分のタスクが過負荷なら、リトライせずエラーを上位に伝えるべき、と明言しており、「リトライが常に正しいわけではない」という認識はUberと共通しています。またGoogleは、直近2分のリクエスト数が受理数のK倍(通常2倍)を超えたらクライアント自身が確率的にリクエストを手元で拒否する「クライアント側適応スロットリング(adaptive throttling)」というアプローチで近い問題に対処してきました。

まとめると、サーキットブレーカー・バックオフ+ジッタ・スロットリングは、それぞれ「遮断」「タイミング」「総量の抑制」に効く防御です。しかし「リトライ権を誰が持つべきか」という問いに答えられる仕組みは、この中にありません。次章で紹介するUberの「エラーオーナーシップ」は、まさにこの空白を埋める発想です。

Uberの事例と数字: 2025年11月の大規模障害で何が起きたか

理屈は分かっても、「実際にどれほどの被害になり得るか」は本番の数字を見るのが一番わかりやすいでしょう。ここでは、Uberが公式ブログで公開している2025年11月18日の大規模障害を具体例に、エラーオーナーシップが実際にどう働いたかを追います。

障害の発端: 深層のサービスが高エラー率に

障害は、コールチェーンの5階層以降にある「Core Entityサービス」と呼ばれる重要サービスが、インフラ障害によって高エラー率に陥ったことから始まりました。問題は、このエラーが上位サービスへ急速に伝播したことです。前述のとおり、各層が「自分もリトライすべきか分からない」まま一律にリトライをかけると、リクエストはホップごとに指数関数的に増幅します。

Uberの試算では、もし単純なリトライバジェット(各サービスでリトライ量を上限比率で抑える方式)だけだった場合、劣化したサービスへのトラフィックが46%〜135%増加し、障害が長期化・悪化していた可能性があります。46%でも十分に深刻ですが、135%というのは「リトライだけで通常の2倍以上の負荷を、すでに壊れかけたサービスに追加する」ことを意味します。

エラーオーナーシップの自動作動

ここでエラーオーナーシップが自動的に働きました。依存関係分析がインバウンド/アウトバウンドの失敗を相関し、エラーの発生源である辺(この場合はCore Entityサービスを呼び出している直近の呼び出し元)だけにリトライ権を局所化したのです。

結果は数値で明確です。

  • 直近の呼び出し元では最大20万リクエストの追加リクエストを停止
  • ルート集計で950万件のスプリアス(無駄な)リクエストを削減

950万件という数字は、単一障害から生まれ得るリトライの増幅規模を物語っています。この950万件が実際に発火していたら、Core Entityサービスは負荷でさらに沈み、復旧は大幅に遅れていたはずです。

導入後の効果: 「リトライストーム半径」の劇的な縮小

Uberは導入効果を「最大リトライストーム半径(max retry storm radius)」という指標で測定しています。これは、単一の障害がリトライ連鎖によって波及しうる範囲の大きさです。導入後、ユーザー向けAPI全体でこの半径は最大25→3、平均20→2に低下しました。つまり、かつては25層規模まで連鎖し得たリトライストームが、3層程度に封じ込められるようになったということです。

頑健性への懸念と実測データ

「エラー発生源だけにリトライを許す」方式への当然の懸念は、「自分のエラーと下流のエラーが同時に起きたら(偶発エラー)、リトライを誤って抑制しないか?」です。Uberの6ヶ月の実データでは、1分間に100回以上の呼び出し先失敗があるエッジの80%において、呼び出し元も同時に失敗するケースは最大でも約2%と極めて稀でした。さらに依存分析が失敗パターンの「メモリ」を持つため、誤った抑制を回避できるといいます。

この事例が示す教訓はシンプルです。リトライの総量を制御する仕組みがない限り、障害はリトライによって増幅される。そして、その制御を個別サービスの手動設定に頼るのではなく、エラーの発生源という「文脈」に基づいて自動化できる——これがUberが本番障害から学んだ結論です。

対策比較表: リトライ設計の選択肢はどう違う?

リトライストーム対策は単一の銀の弾丸ではなく、複数の手段をレイヤーごとに組み合わせるものです。ここでは主要な6つのアプローチを比較します。

手段 防ぐもの 効果範囲 導入コスト 主な欠点
素朴なリトライ(固定回数・固定間隔) 一時的な瞬断 単一呼び出し 極小 深いチェーンでR^d倍に増幅。ストームの「元凶」
エクスポネンシャルバックオフ+フルジッタ 同時再試行の同期(サンダリングハード) クライアント単位 小 「タイミング」は緩和するがリトライ総量の増幅は解決しない
リトライバジェット リトライ総量の超過 サービス単位 中 手動設定。全ノードが無関係にリトライ権を持つため増幅は残る
サーキットブレーカー 障害サービスへの継続アクセス 単一エッジ 中 閾値到達前の暴走・半開状態の再同期・伝播エラーの区別ができない
クライアント側スロットリング(Google SRE adaptive throttling) バックエンド到達前の過剰リクエスト クライアント群全体 中 大まかな確率的制御。エラーの発生源までは見ない
エラーオーナーシップ(Uber) リトライ権の無差別な伝播 コールチェーン全体 大(依存分析基盤が必要) 偶発エラーの判断が必要。フル実装にはメッシュ基盤が要る

各手段の位置づけ

バックオフ+ジッタはAWS Architecture Blog「Exponential Backoff and Jitter」が古典的な定番で、待ち時間を random(0, min(cap, base * 2^attempt)) で完全ランダム化するフルジッタがAWSのシミュレーションで最良とされています。ただしUberが示したとおり、深いチェーンでのリトライ総量の指数増幅(R^d)自体は解決しません。

サーキットブレーカーは「悪化を検知して遮断」するコンポーネント保護であり、「リトライそのものを賢くする」対策(バジェット、エラーオーナーシップ)とは補完関係にあります。単独では①閾値到達前のリトライ暴走、②半開状態での再同期、③伝播エラーと自分のエラーの区別、という3つの盲点があります。

Google SREのHandling Overloadでは、直近2分のrequests/acceptsを追跡し、requestsがacceptsの2倍を超えたらクライアント自身が確率的にローカル拒否するadaptive throttlingを紹介しています。また「大部分のタスクが過負荷ならリトライせずエラーを上位に伝えるべき」とも明言しており、Uberと同じ前提を共有しています。

両者の違いを一言でいえば、Googleは「クライアント側で量を絞る」アプローチ、Uberは「リトライ権をエラー発生源に局所化する」アプローチです。規模と基盤に応じて、まずは手軽なバックオフ+バジェット+サーキットブレーカーから始め、大規模ならスロットリングやオーナーシップに進む、という積み上げが現実的でしょう。

実践チェックリスト: 自分のシステムをリトライストームから守るには(How-to)

Uberほどの基盤がなくても、今日からできる対策はあります。以下を順に確認してください。

  1. すべての外部呼び出しにタイムアウトを設定する。 タイムアウトなしのリトライは、応答しない相手にリクエストを溜め続けるだけです。HTTPクライアントでもDB接続でも、接続・読み取り両方のタイムアウトを明示しましょう。
  2. リトライ回数は最小限(初期値の一例は1〜2回)とし、フルジッタ付きエクスポネンシャルバックオフを使う。 待ち時間は random(0, min(cap, base * 2^attempt)) で完全ランダム化します(AWS「Exponential Backoff and Jitter」)。適切な回数は操作の冪等性・タイムアウト設計・相手の容量に依存するため、Retry-Afterヘッダやサービス固有の制約を確認した上で調整してください。回数を増やすより、間隔を散らす方が効きます。
  3. リトライバジェット(例: 全リクエストの10%まで)を入れる。 「直近のリクエスト総数に対するリトライ数」に上限を設けるだけで、毎ホップ10%のバジェットでも深いチェーンの増幅を約1.33倍に抑えられます(Uberの試算)。
  4. サーキットブレーカーを併用する。 連続失敗が続いたら一定時間遮断し、回復を少量の試験リクエストで確認します。ただし前述の3つの限界(閾値前の暴走・半開の再同期・伝播エラーの区別不可)を理解した上で、あくまで補助と位置づけます。
  5. リトライ可能なエラーと不可能なエラーを区別する。 まず操作が冪等(再実行しても副作用が重複しない)であることを確認します。ネットワーク瞬断・タイムアウトは冪等な操作ならリトライ候補ですが、429ではRetry-Afterの指示に従い、400系やビジネスエラー、冪等でない操作(決済など)は原則リトライせず、重複実行防止(冪等キー等)を併用します。無差別な5xxリトライはストームの温床です。
  6. ヘルスチェック失敗時の自動再起動にもバックオフとジッタを入れる。 再起動ループは「再起動ストーム」として同型の問題です。失敗が続くなら再起動間隔を指数的に広げ、ジッタで複数ノードの再起動が同期しないようにします。ホームサーバーのDocker restart: always も、restart: on-failure + 最大試行回数の検討を。
  7. キューのコンシューマーは最大試行回数+デッドレターで打ち切る。 RedisやMQの再送を無限にせず、N回失敗したらデッドレターキューへ退避して人間が確認します。ポイズンメッセージの無限再送は、隠れたリトライストームです。

ホームサーバー運用の具体例として、複数のcronジョブやエージェントが同じAPIを叩く構成なら、③のバジェットをアプリ側に実装するだけでストームリスクは激減します。まず1〜3だけでOKです。大切なのは「リトライの総量を制限する」という発想の転換です。

FAQ: リトライストームに関するよくある疑問

Q1: リトライストーム(retry storm)とは何ですか?

A: 分散システムで、あるサービスの障害や遅延が発生した際に、複数の上位サービスが同時にリトライをかけ、リトライがホップごとに指数関数的に増幅して障害を拡大させる現象です。Uberの試算では、毎ホップ1回のリトライ(R=2)でも、深さ3のノードは通常の8倍(2^3 = 8倍)のリクエストを処理することになり、さらに深いチェーンでは64倍以上にも達します。壊れかけたサービスにさらに負荷を与えるため、単独なら短時間で収まる障害が長期化・悪化します。

Q2: サーキットブレーカーを入れていれば安全ですか?

A: 残念ながら3つの限界があります。①失敗が閾値に達する前に大量のリトライが発火する時間差、②回復判定のための試験リクエストが複数ノードから同時に押し寄せる半開状態の再同期、③「下流のせいで失敗した」のか「自分が失敗した」のかを区別できず、上位層が伝播エラーにも無差別にリトライすること、です。サーキットブレーカーは「悪化を検知して遮断」する守りであり、リトライそのものを賢くするバックオフ・バジェット・エラーオーナーシップとの併用が正しい位置づけです。

Q3: リトライ回数は何回が適切ですか?

A: 一般的な出発点は「1〜2回+フルジッタ付きエクスポネンシャルバックオフ+リトライバジェット(例: 10%)」ですが、最適値は操作の冪等性、タイムアウト設計、Retry-After、相手サービスの容量によって変わるため、あくまで初期値として扱うのが正解です。待ち時間を random(0, min(cap, base * 2^attempt)) でランダム化すること(AWS)と、リトライ総量の上限を設けることが重要です。可用性80%(エラー率20%)のサービスはリトライしても88%にしかならないというUberの数字が示すとおり、高エラー率時にリトライを重ねても回復は望めません。

Q4: Uberのエラーオーナーシップは自分たちでも実装できますか?

A: 簡易版なら可能です。「このエラーは自分が原因か、下流から伝播しただけか」を表すclaim/unclaimヘッダをサービス間で伝播させ、リトライ権をエラー発生源の辺だけに限る仕組みは、ヘッダ管理と失敗の相関分析さえできれば中規模システムでも導入できます。ただしUberのフル実装(Service Dependency Analysisとの連携、at-least-once保証、偶発エラー対策)にはサービスメッシュ級の基盤が必要です。まずはリトライバジェットとバックオフで総量制御から始めるのが現実的でしょう。

まとめ: 「丁寧な失敗」が信頼をつくる

Uberの事例が教えてくれる核心は、意外にもささやかなものです。失敗するときの振る舞いを丁寧に設計すること——それがシステム全体の信頼性を決める。

「一時的なエラーならリトライすればいい」という直感は、単独の呼び出しでは正しいことが多い。しかし深い依存チェーンでは、その直感がすべての層で同時に発動したとき、R^dの増幅として障害を育てます。サーキットブレーカーも、バックオフも、それぞれに盲点がありました。Uberのエラーオーナーシップは「エラーの発生源だけがリトライする権利を持つ」という一文に集約される発想の転換で、本番障害では950万件の無駄リクエストを抑え、リトライストーム半径を最大25から3まで縮めました。

私たちが明日から真似すべきは、この思想の小さな版です。タイムアウトを設定し、リトライは1〜2回に絞り、ジッタで散らし、総量に上限を設ける。再起動もキューも無限に頑張らせない。ホームサーバーのcron一つにも同じ原則は効きます。Uberが言うように、リトライの賢さを個々のサービス任せにせず、共有基盤で担保するのが理想ですが、小規模環境でも「リトライ総量を制限する」発想の転換だけで、障害リスクは大きく下がります。

賢いリトライは、失敗をなかったことにするのではなく、失敗の影響を最小の範囲に留める技術です。それは結局、システムが利用者に対して約束を守る方法なのだと思います。

参考文献

関連記事