Uberがリトライストームから保護する方法
How Uber Protects Against Retry Storms (uber.com)
要約
Uberは、サービス間の依存関係が深まり、単一の障害が連鎖的な障害を引き起こす「リトライストーム」問題に対処するため、コンテキストを認識するエラー処理メカニズムを開発しました。従来のリトライ設定は手動で可視性が低く、問題が悪化する原因となっていました。新しいシステムは、エラーの原因を特定し、リトライをより賢く制御することで、インフラストラクチャの安定性とユーザーエクスペリエンスを向上させます。
全文翻訳
Uberはリトライストームからどのように保護しているか
はじめに
リトライストームは、歴史的にビジネスオペレーションとブランドの信頼に影響を与えてきました。リトライ設定のチューニングやリトライバジェットは、サービスレベルでの意味のある緩和策を提供しますが、これらは手動で設定され、深い依存関係チェーンやファンアウトパターンによって引き起こされるサービス間の増幅に対する可視性が欠けています。その結果、スタックの奥深くにある単一のサービス障害によって引き起こされるドミノ効果からインフラストラクチャを保護することが困難になることがあります。
主な理由は、現在のリトライ動作がコンテキストを認識していないことです。リトライが何回発生するかは制御できても、いつ発生するかを正確に制御することはできません。これは、サービスによって生成されたエラーと、単にそれを通過しただけのエラーを確実に区別するという課題に起因します。
その結果、リトライは条件付きではなく、一様に適用されます。
このアプローチは、一時的な障害や低レートの障害には有効です。しかし、中程度または深刻な劣化の際には、逆効果になります。すでに苦境にあるサービスに対して積極的にリトライを行うと、負荷が増加し、障害が加速し、アップストリームの依存関係全体でリトライトラフィックが増幅されます。局所的な障害として始まったものが、急速にスタック全体に広がるインシデントにエスカレートする可能性があります。最終的には、エンドユーザーエクスペリエンスが悪化するか、最悪の場合は完全に壊れてしまいます。
ダウンストリームサービスのエラーコードをアップストリームに変換してリトライのコンテキストを提供できると主張する人もいるかもしれません。理論的には可能ですが、Uberでは大規模なファンインとファンアウト、進化するコールフロー、頻繁な適応的変更の必要性から、このアプローチはスケーリングしません。そのため、エラーをより効率的に処理するために、共有インフラストラクチャにコンテキストを認識するメカニズムを開発しました。この記事ではそのメカニズムについて説明します。
背景
図1に示す単純なコールチェーンを考えてみましょう。ノードAに到着するリクエストの総数はηです。推論により、ノードB、C、D、E、F、Gのすべてが定常状態(ノードがエラーを起こさない場合)でηのリクエストを処理します。
[画像: AからGまでの7つの青い円が右向きの矢印で水平線上に接続されている。]
図1: 1:1のファンアウトを持つコールチェーン。ノードは、着信リクエストごとに1回だけダウンストリームを呼び出します。
サービスDがエラーを起こし始め、各サービスが1回のリトライ(通常の試行1回と、ダウンストリームが失敗した場合のもう1回の試行)で設定されている場合、各ノードが処理するリクエストの総数を見てみましょう。
[画像: AからGまでの7つの円が直線状に並び、矢印が付いている。Dが赤くハイライトされている。]
図2: サービスがエラーを起こすコールチェーン。
| ノード | A | B | C | D | E | F | G |
| 深さ | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| 処理リクエスト数 | η | 2 × η | 4 × η | 8 × η | 8 × η | 8 × η | 8 × η |
これは、すべてのホップでリトライ数Rが同じであると仮定すると、単純な公式に集約できます。ɗはコールチェーン内のノードの深さであり、ノードがエラーを作成または通過した場合にノードが処理するリクエスト数です。
Rɗ × η
リトライバジェット
リトライバジェットを導入することで、これを最適化できます。各ホップで同じリトライバジェットBを表すと仮定しましょう。新しい公式は次のようになります。
(1+B)ɗ × η
次に、リトライバジェットが10%の場合の処理リクエスト数を見てみましょう。
| ノード | A | B | C | D | E | F | G |
| 深さ | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| 処理リクエスト数 | η | 1.1 × η | 1.21 × η | 1.33 × η | 1.33 × η | 1.33 × η | 1.33 × η |
上記の例では、エラーはノードDで発生し、ノードDとノードC間のリトライのみを制限し、ノードAとノードBがこのエラーに対するリトライを完全に制限した場合、ノードD、E、F、Gに過負荷をかけずに、コールパスの同様の可用性を保証できます。
エラーオーナーシップ
エラーが発生したノードCからノードDへのエッジ間のリトライを制限する場合と同じリトライバジェットの例を考えてみましょう。
| ノード | A | B | C | D | E | F | G |
| 深さ | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
| 処理リクエスト数 | η | η | η | 1.1 × η | 1.1 × η | 1.1 × η | 1.1 × η |
ここでは、DからリーフノードGまでのすべてのノードが処理するリクエストの総数をベースラインの10%増に制限し、最初に返されたエラーの最大10%に対して少なくとも1回のリトライを許可します。しかし、ノードCから見たノードDの可用性はどうでしょうか?さまざまな可用性シナリオで数値を実行し、リトライ後の可用性を計算してみましょう。
| 基本可用性 % | 基本エラー率 % | リトライバジェット | リトライ後のエラー率 % | リトライ後の可用性 % |
| 99.9 | 0.1 | 10% | 0.0001 | 99.9999 |
| 99 | 1 | 10% | 0.01 | 99.99 |
| 95 | 5 | 10% | 0.25 | 99.75 |
| 90 | 10 | 10% | 1 | 99 |
| 80 | 20 | 10% | 12 | 88 |
| 70 | 30 | 10% | 23 | 77 |
上記の表に示すように、呼び出し先ノードの可用性が10%まで低下した場合、1回のリトライでも、呼び出し元ノードが認識する可用性を99%まで向上させるのに役立ちます。これを超えると、リトライバジェットが配置されているため、リトライされないリクエストの大部分が発生するため、認識される可用性は大幅に低下します。
この計算は、呼び出し先からのエラーが独立しており、リトライが回復につながると仮定しています。しかし、サービス過負荷、データベースホストの障害、データベース過負荷、シャーディングの問題など、多くの現実世界のシナリオでは、リトライがあってもリトライの確率は高くなります。
これは、呼び出し先へのリトライが常に呼び出し元の認識される可用性を高めるという考えと矛盾します。また、この直感がエラーオーナーシップの基礎を形成しています。サービスからのエラー率が高い期間中、エラーはランダムである可能性が低く、より多くのリトライから利益を得られない可能性があり、代わりにさらなる劣化の原因となる可能性があります。
アーキテクチャ
このソリューションは、症状と原因のアナロジーを使用して説明できるエラーオーナーシップを確立することです。
サービスがリクエストを処理するためにN個のアウトバウンドを呼び出し、アウトバウンドがエラーアウトしてエラーを返す場合、そのサービスによって返されるエラーは単なる症状です。同時に、サービスのコンテキストでは、原因はそのダウンストリームからの着信エラーです。
しかし、サービスがリクエストを処理する際、アウトバウンドのエラーがなく、それでもエラーを返す場合、そのサービスが返されたエラーの原因であり、オーナーです。次のセクションでは、これを活用できる可能性のあるソリューションについて説明します。
単純な相関
エラーオーナーシップの主張
サービス依存関係分析ソリューションを使用して、インバウンド障害とアウトバウンド障害を相関させ、図3のルールセットを使用してエラーの主張を作成または否定します。
[画像: サービスSでのエラー処理のためのフローチャート。ダウンストリーム呼び出しの結果に基づいて、主張と主張解除のロジックを詳述している。]
図3: エラーオーナーシップを主張するための決定ロジック。
エラーオーナーシップでのリトライ
呼び出し元は、図4に示すロジックを使用して、リクエストをリトライすべきかどうかを判断します。
[画像: x-uber-error-claimヘッダーの存在と値に基づいて、ダウンストリームエラー応答を処理するためのフローチャート。]
図4: リトライを許可するための決定ロジック。
エラー主張ヘッダーの欠落は非協力的な環境ですが、ダウンストリームサービスがサービス依存関係分析ソリューションを持っておらず、アウトバウンドとインバウンドのエラーを相関させることができないために発生する可能性があります。または、ダウンストリームサービスがコンテキスト伝播を欠いており、アウトバウンドとインバウンドのエラー間の不完全な相関につながります。
ここで、ダウンストリームから欠落しているエラー主張を最初に見たノードはエラーを主張解除し、リトライ障害の影響範囲を制限します(もはやストームではなくなります)。それでも、エラーを返したサービスへの十分なリトライを許可します。
決定マトリックス
| 呼び出し先エラー | 呼び出し元エラー | 呼び出し先エラー主張 | 呼び出し元はリトライすべきか(リトライミドルウェア) | 呼び出し元伝播エラー主張 |
| いいえ | はい | NA | NA | 主張 |
| はい | はい | 欠落 | はい | 主張解除 |
| はい | はい | 主張済み | はい | 主張解除 |
| はい | はい | 主張解除なし | いいえ | 主張解除 |
[画像: 呼び出し元エラーを実証するために使用される3つのピンク色の円A、B、Cが、AからBへ、BからCへの矢印で接続されている。]
図5: 呼び出し元エラーを実証するために使用される3つのノード。
エッジA→BおよびB→Cがフェイルクローズであり、ノードCが主張された内部エラーを返した場合、ノードBはCからの主張されたエラーを見てCにリトライすべきです。しかし、リトライが失敗した場合、それはエラーをノードAに伝播しますが、エラーを返す際にそれを主張解除しなければなりません。ノードAはBからのエラーと主張解除されたエラーヘッダーを見て、ノードBへのリクエストをリトライすべきではありません。
[画像: ノードA、ノードB、ノードC間のエラー伝播とリトライロジックを示すフローチャート。フェイルクローズエッジを使用。]
図6: エラー主張伝播のための決定ロジック。
coincidental Errors とサービス依存関係分析が必要な理由
上記の決定マトリックスは、ダウンストリーム呼び出しが失敗した場合や、内部サーバーエラーが発生した場合をカバーしています。図7に示すように、両方が同時に発生するシナリオもあります。
[画像: ノードAが、単純な有向グラフでノードBとノードCに矢印で接続されている。]
図7: ノードAが2つのフェイルオープン依存関係、ノードBとノードCを持つ例。
この例では、ノードAは、サービス依存関係分析ソリューションで追跡されていないオーバーロードされたキャッシュ/データベースのために、10%のエラー率も経験しています。これはノードAの内部サーバーエラーとなり、ノードBとCも無関係な理由で10%のエラー率を持っています。
サービス依存関係分析ソリューションは、エラーをまずダウンストリームに、最後に自身に帰属させようとするため、ノードBとCがフェイルオープンであっても、障害が共存している場合はそれらに責任を割り当てます。これによりエラーが主張解除され、ノードAのアップストリームがそれにリトライして回復する可能性を防ぎます。
このようなシナリオでの正当なリトライ機会の喪失の影響は大きいです。ノードAが100リクエストを処理し、そのうち10がノードAで発生した内部サーバーエラーを経験した場合、その10件のうち1.9件はノードAによって誤って主張解除され、正当なリトライ機会を逃すことになります。
6ヶ月間のサービス依存関係分析ソリューションデータの分析に基づくと、正当なサーバーエラーやフェイルクローズ依存関係のエラーがフェイルオープン依存関係のエラー中に発生するような共存エラーは非常にまれです。最悪の場合、1分あたり100件以上の呼び出し先障害を持つエッジの80%で、約2%の時間で呼び出し元も失敗しました(共存障害)。上記の例を使用すると、ノードAによって誤って主張解除されるリクエストは10件あたり0.396件になります。
しかし、最悪の場合に固執したとしても、サービス依存関係分析ソリューションは障害パターンのメモリを作成するため、このメモリを活用して、インバウンド障害がフェイルクローズ依存関係のアウトバウンド障害と相関する場合にのみエラーを主張解除できます。これにより、共存エラー中のリトライ抑制のリスクが排除されます。
[画像: 依存関係の障害相関に基づいて、主張解除するかリトライを保持するかを決定する、ノードエラーを処理するためのフローチャート。]
図8: リトライするかどうかを決定するための決定ロジック。
エッジケースシナリオ
少なくとも1回の保証されたリトライ
多くのサービスでは、フェイルクローズアウトバウンドのリトライが設定されていません。これらのサービスの呼び出し元からのリトライを排除すると、着信呼び出しチェーンの可用性が低下します。
これは、ダウンストリームから返されたエラーに対してリトライ基準が満たされているかどうかを示すフラグを導入することで解決されます。リトライミドルウェアがダウンストリームからのエラーを見た場合、リトライ基準が満たされているかどうかを計算し、それをサービス依存関係分析ソリューションに渡すことができます。
[画像: 4つのサービスコンポーネントにわたるリトライロジックとエラー処理を示すアウトバウンドコールチェーンのシーケンス図。]
図9: 少なくとも1回の保証されたリトライのためのリトライロジック。
リトライミドルウェアは、図10に示すいずれかの条件が満たされた場合に、リトライ基準が満たされているとマークします。
[画像: ダウンストリームエラーとリトライ基準の満足度を処理するためのリトライミドルウェアロジックを詳述するフローチャート。]
図10: リトライ基準が満たされているかどうかを判断するための決定ロジック。
サービス依存関係分析ソリューションは、リトライミドルウェアからのこのリトライ基準が満たされているというシグナルを活用して、インバウンドで、フェイルクローズダウンストリームから返されたエラーを所有する必要があるかどうかを決定します。
[画像: フェイルクローズ障害とリトライ基準の決定ポイントを持つサービスエラーを処理するためのフローチャート。]
図11: サービスエラーオーナーシップのための決定ロジック。
エンドツーエンドのフローは次のとおりです。
[画像: アップストリームA、サービスB、SDA@B、リトライMW@B、およびダウンストリームC間のエラー処理とリトライロジックを示すシーケンス図。]
図12: エンドツーエンドのフロー。
呼び出しチェーンに新しいリトライを導入しません。少なくとも1回の保証されたリトライ動作は、呼び出しチェーンに沿った少なくとも1つのサービスにリトライが設定されている場合にのみ機能します。
少なくとも1回の保証されたリトライを導入することにより、コールチェーンの可用性の低下を排除しました。同時に、エラーオーナーシップを活用してリトライストームから保護しました。
コンテキストドロップ処理
[画像: A、B、C、Dという4つのピンク色の円が、線形シーケンスで右向きの矢印で接続されている。]
図13: コンテキストドロップ処理を実証するために使用される4つのノード。
サービス依存関係分析ソリューションとエラーオーナーシップの場合、サービス内のコンテキストドロップはリトライ可能なエラーを左にシフトします。図14の例を考えてみましょう。ノードDがノードCに内部エラーを返します。ノードC内のサービス内コンテキストが壊れているため、サービス依存関係分析ソリューションは、ノードCからDへのアウトバウンドリクエストと、ノードBからCへのインバウンドリクエストを相関させることができません。
その結果、ノードCはエラーを主張しますが、リトライミドルウェアでの分離されたリトライはノードCとDの間で継続して発生します。ノードCが返したエラーを主張する必要があるため、ノードBとCの間でも一連のリトライが発生します。しかし、リトライが失敗し、ノードBがノードAにエラーを返した場合、主張解除されたエラーヘッダーも返します。これは、ノードAがノードBへのリクエストをリトライするのを防ぐはずです。この場合、すべてのノードで単一のリトライ(試行1回とリトライ1回)設定を仮定すると、ノードBからDへのリクエストの総数を半分に削減しました。ノードAの左側に5つのノードがあった場合、最悪のケースでは、エラーオーナーシップ伝播なしで32倍のリクエストが発生した可能性があります。
[画像: コンテキストドロップと主張処理を備えた4つのノードにわたるエラー伝播とリトライロジックを示すシーケンス図。]
図14: 説明。
本番環境でのエラーオーナーシップ
これまでに説明したことはすべて理論的なものではありません。エラーオーナーシップは、Uberのサービスメッシュ全体で完全に実装され、運用されています。このスキームは、ユーザー向けAPIのリクエストパスにあるリトライミドルウェアとサービス依存関係分析ソリューションで実行され、トラフィックが深い依存関係チェーンを流れるにつれて、継続的にエラーを主張および主張解除します。共有インフラストラクチャに組み込まれているため、サービスは、個別のサービスエラー処理ロジックなしでリトライストーム保護を継承します。以下の実際のインシデントは、この本番デプロイメントが実際の広範な劣化下でどのように動作したかを示しています。
Uberでのユースケース
2025年11月18日、Uberは主要なアウトエージを経験しました。これは、Core Entityサービスの問題が原因でした。このサービスは、当社のコールチェーンで5レベル以上の深さに位置し、ビジネスオペレーションに不可欠です。基盤となるインフラストラクチャの問題により、非常に高いエラー率を返し始めました。このエラーは、コールチェーンを急速に上に伝播しました。この間、多くのアップストリームの呼び出し元サービスは、これらのサービスから返された失敗したリクエストをリトライする十分な機会がありました。単純なリトライバジェットでは、これは低下したサービスで46%〜135%のトラフィック増加につながり、回復の可能性を低下させることでアウトエージを長引かせたでしょう。エラーオーナーシップはすでに本番環境で有効になっていたため、上記のように、システムは自動的に影響範囲を封じ込めました。
低下したサービスへの直接の呼び出し元に対する即時のリトライ試行は停止され、一部の呼び出し元は最大200,000件の追加リクエストの送信を停止されました。また、これらの直接の呼び出し元の祖先のノードで停止されたリトライを計算し、ルートノードに集約しました。サービスメッシュで最大950万件の無効なリクエストを停止できたことがわかりました。これらは、低下したサービスを絶えず叩くことで、アウトエージを簡単に長引かせた可能性があります。
結論
このアプローチにより、劣化イベント中のコールグラフを流れる総リクエスト量が劇的に減少しました。
これを定量化するために、エラーオーナーシップ後の最大リトライストーム半径を、エラーオーナーシップが有効になった後にリトライストームが発生する可能性のあるコールパスの最大深度として定義します。これは、すべてのルートノードのコールグラフに対して計算されます。すべてのユーザー向けAPI全体で、この値を最大3にまで削減しました。以前の最大リトライストーム半径は最大25でした。平均も20から2に削減しました。
[画像: エッジゲートウェイサービス、エンドポイント、およびページネーション付きの最大リトライストーム半径値を示す表。]
図15: エラーオーナーシップ後の最大リトライストーム半径。
エラーオーナーシップを持つサービス、つまり問題を実際に解決できるサービスにリトライを限定することで、リトライストームの特徴であるリクエストの指数関数的なファンアウトを防ぐことができます。これにより、単一点の劣化によって引き起こされるカスケード障害からUberのインフラストラクチャを保護するのに役立ちました。
謝辞
カバー写真の帰属: OpenAIのChatGPTによって生成されました。外部の画像、ロゴ、サードパーティの資産は使用されていません。
Uber Engineeringの最新情報については、LinkedInでフォローして、最新のブログ投稿や洞察を入手してください。