代表的な面接トピック

バックエンド面接:タイムアウト、リトライ、サーキットブレーカーはどう連携させるべきか?

バックエンド難しい
Offer.cc 編集チーム公開日 更新日

質問

チェックアウトAPIが、低速応答、429や503の返却、接続断が時折発生する外部税金計算サービスを同期的に呼び出しています。障害の増幅や書き込みの重複を発生させずに、800 msのエンドツーエンド目標内で成功率を向上させるには、タイムアウト、リトライ、指数バックオフ、ジッター、サーキットブレーカーをどのように組み合わせますか?

設問とこの質問が適用される場面

チェックアウトAPIがサードパーティの税金計算サービスを同期的に呼び出しています。この依存先は通常、p50が80 ms、p99が180 msですが、接続障害、低速応答、HTTP 429、503、およびリクエストはサーバーに到達したもののレスポンスが失われたケースが散発的に発生します。チェックアウトAPIには800 msのエンドツーエンド目標が設定されています。サービス呼び出しチェーンは5階層の深さになる場合があり、一部のSDKはすでにデフォルトでリトライを行う可能性があります。

呼び出しポリシーを設計してください。デッドラインを試行ごとのタイムアウトにどのように分割するか、どの障害がリトライ可能か、最大試行回数・指数バックオフ・ジッターをどのように設定するか、サーキットブレーカーがどのようにオープンし回復するか、そして分離、縮退、メトリクス、フォールトインジェクションによって局所的な障害がカスケード障害にならないことをどう証明するかを説明してください。

レイテンシと深さは面接上の前提条件です。中核となるスキルは、同期的なサービス間呼び出しにおける障害セマンティクス、リソース保護、回復制御であるため、カテゴリはバックエンドになります。既存の冪等な注文に関する質問は、単一の書き込み操作に対するデータベースコントラクトに焦点を当てています。本問では、タイムアウト後のリトライが安全かどうかを判断するためにのみ冪等性を使用し、呼び出しチェーン全体にわたる時間と負荷のバジェットを中心として扱います。

面接官が評価しているポイント

第1のシグナルは、候補者が時間の台帳(タイムバジェット)を作成しているかどうかです。800 msのエンドツーエンドデッドラインは、依存先のタイムアウトが800 msであることを意味しません。ローカルのバリデーション、キューイング、シリアライズ、レスポンス処理のすべてにバジェットが必要です。各試行は接続、TLS、レスポンスの各フェーズも考慮しなければならず、次のリトライは十分な残り時間がある場合にのみ開始する必要があります。

第2のシグナルは、障害セマンティクスに応じたリトライです。接続確立前の失敗、一時的な503、またはリトライ案内を伴う429はリトライ可能な場合があります。不正なパラメータ、認証、認可は時間が経っても修復されません。書き込み送信後にタイムアウトした場合、結果は「コミットされたがレスポンスは不明」である可能性があります。リトライを安全に行うには、安定した冪等性キー、ステータス参照、または照合プロセスが必要です。

第3のシグナルは、リトライの増幅です。5階層のチェーン内のすべてのサービスが最大3回試行すると、最下層の依存先は3^5 = 243回の呼び出しを受ける可能性があります。リトライはビジネスデッドラインを把握している1つのレイヤーに集約し、試行回数、総経過時間、トークンベースのリトライバジェットによる制限を設ける必要があります。

最後に、面接官は回復ループを求めています。タイムアウトは1回の待機時間を制限し、リトライは一時的な障害を処理し、サーキットブレーカーは持続的な障害時に呼び出しを遮断し、バルクヘッドは並行性とキュー占有率を制限します。優れた回答では、ハーフオープン(半開)状態のプローブを制限し、不適切なフォールバックを拒絶し、論理的な呼び出しを物理的な試行とは別個にモニタリングします。

回答前に確認すべき明確化の質問

  • 依存先呼び出しは読み取りですか、それとも副作用を伴う書き込みですか? 読み取りは通常繰り返し可能です。タイムアウトした書き込みは結果が不明な場合があり、冪等性キー、ステータス参照、または照合が必要です。
  • 800 msは厳格なデッドライン(ハードデッドライン)ですか、それとも観測目標ですか? ここでは、それを超えると呼び出し元が待機を終了するポイントです。呼び出し元が不在の中で処理が完了しないよう、キャンセルをダウンストリームに伝播させる必要があります。
  • 依存先はどのエラーをリトライ可能と宣言していますか? 分類はプロトコルとベンダーのコントラクトに従う必要があり、特に429、503、接続障害、ビジネスエラーにおけるRetry-Afterに準拠します。「200以外すべて」はポリシーとは言えません。
  • SDK、プロキシ、またはサービスメッシュはすでにリトライを行っていますか? 各レイヤーのデフォルト設定と試行制限を棚卸ししてください。そうでない場合、アプリケーション層での1回の可視的なリトライがリトライストームへと拡大する恐れがあります。
  • 依存先のレイテンシ分布とキャパシティはどうなっていますか? パーセンタイル、許容される誤タイムアウト率、ネットワーク余裕からタイムアウトを算出し、新規接続、クロスリージョントラフィック、ピーク負荷を検証します。
  • どのような縮退(デグラデーション)が許容されますか? 税金計算にコンプライアンス要件がある場合、適当に捏造した「成功」のデフォルト税額を返すのは安全ではありません。明示的な失敗、手動レビュー、またはチェックアウトの延期は、ビジネスコントラクトに従う必要があります。
  • サーキットブレーカーの分離境界は何ですか? 独立したベンダー、リージョン、エンドポイント、または操作単位を使用します。1つの障害シャードが正常なリソースまで切断してはなりません。

30秒の回答フレームワーク

「800 msのエンドツーエンドデッドラインをタイムバジェットに分割し、絶対デッドラインをダウンストリームに伝播させます。すべての呼び出しに接続タイムアウトとリクエストタイムアウトを設定し、残りのバジェットに収まる場合にのみ次の試行を開始します。障害は回復可能性と副作用が既知かどうかに基づいて分類します。429に対してはサーバーの案内に従い、短時間の接続障害と選択した一部の5xx応答のみをリトライし、書き込み結果が不明な場合は冪等性またはステータス照合を必須とします。リトライは1つのレイヤーに集約し、上限付きの指数バックオフとジッターを用いて最大2回とし、障害時の余剰トラフィックを制限するトークンバジェットを消費させます。持続的な障害が発生するとサーキットをオープンしてフェイルファストさせ、クールダウン後は少数のハーフオープンプローブのみを通過させます。独立した並行性制限と有界キューによりローカルリソースを保護します。その後、フォールトインジェクションによってデッドライン、試行回数、重複効果、回復パスを検証します。」

ステップバイステップの詳細解説

まずは時間を明示的なバジェットに変換することから始めます。この演習では、800 msのうち依存先呼び出し前のローカル処理に120 ms、結果の処理に100 msを確保します。これにより、依存先には580 msのバジェットが残ります。検証可能な初期設定例としては、1回目の試行の上限を220 ms、[0, 80) msのランダムバックオフ、2回目の試行の上限を220 msとします。最悪のケースでは520 msを消費し、依存先バジェットに60 msの余裕が残ります。これらは普遍的な定数ではなく、本番環境の値はダウンストリームのパーセンタイル、許容誤タイムアウト率、ネットワーク遅延、負荷テストからキャリブレーションする必要があります。

絶対デッドライン、または単調減少する残りバジェットを呼び出しチェーン全体に伝播させます。リトライを行う前に、remainingを再計算します。それが「次の試行上限 + 最小完了時間」より小さい場合は、完了できない呼び出しを開始する代わりに即座に戻ります。DNS、TLS、コネクションプール待機が含まれるかどうかを含め、接続タイムアウトとリクエストタイムアウトのセマンティクスを正確に定義します。新規インスタンスはトラフィックを受け入れる前に接続をウォームアップしておくことで、ハンドシェイク時間が依存先の遅延と誤認されるのを防ぎます。リモート側が処理をコミットした後にキャンセルが到着する可能性があることも考慮しつつ、キャンセルを伝播させます。

次に、障害セマンティクス・マトリクスを作成します:

結果リトライするか?前提条件とアクション
接続確立前の失敗はい十分なバジェットが残っていること。バックオフとジッターを使用
HTTP 429条件付きRetry-Afterに従う。待機時間と次回試行がデッドラインに収まること
HTTP 503 または選択された5xx条件付きベンダーが一時的と宣言しており、リトライバジェットが残っていること
HTTP 400, 401, 403通常はいいえパラメータ、認証情報、権限を修正する。待機してもリクエストは修復されない
書き込み送信後のタイムアウト盲目的には不可冪等性キーを再利用するか、操作ステータスを照会・照合する
呼び出し元のキャンセルまたはデッドライン切れいいえ処理の追加を停止し、明示的な失敗を返す

すべてのリトライは3つのゲートによって制御されます:エラーがリトライ可能であること、十分な時間が残っていること、リトライバジェットにトークンがあることです。同時に失敗したクライアント同士の相関を崩すために、上限付き指数バックオフとランダムジッターを使用します。利用可能なサーバー指示のリトライタイミングが存在する場合は、それを優先します。最大試行回数には初回の呼び出しが含まれます。この設計では合計2回の試行から開始します。SDKの文言には注意してください。maxAttempts = 2と「2回のリトライ」は、それぞれ合計2回の呼び出しと合計3回の呼び出しを意味する場合があります。

ビジネスデッドラインを最もよく理解している単一のレイヤーでリトライを実行します。もし5つのレイヤーすべてが3回ずつ試行した場合、理論上の最下層の負荷は243倍に膨れ上がります。リトライ増幅率としてdownstream_attempts / logical_requestsを追跡します。これは正常時には1に近いはずであり、障害発生時には明示的に上限を設ける必要があります。トークンバケットまたは同等のリトライクォータは、障害が増加するにつれて枯渇し、リトライを一時停止するか低レートに制限することで、依存先が最も脆弱なときにクライアントが負荷を追加するのを防ぎます。

特定の依存先操作をサーキットブレーカーでラップします。Closed(閉)状態では呼び出しを許可し、最小サンプルサイズを持つスライディングウィンドウ上で起因する障害や低速呼び出しを測定します。しきい値を超えるとサーキットがOpen(開)になります。Open状態では依存先を呼び出さず、認識可能な失敗またはビジネス承認済みの縮退結果を即座に返します。クールダウン後、Half-Open(半開)状態では少数の並行プローブのみを許可します。プローブが十分に成功するとサーキットは閉じ、致命的な障害が発生すると再び開きます。しきい値はトラフィックと回復特性に基づいて設定します。「20回の呼び出しで50%の失敗」は一例であり、普遍的な定数ではありません。

サーキットブレーカーにはコストが伴います。モーダルな状態(ステートフル)はテストと回復を複雑にし、長すぎるクールダウンは回復を遅らせ、粗すぎる境界設定は正常なシャードまでブロックする可能性があります。すべての状態遷移を記録し、ハーフオープンプローブの並行数を制限し、ブレーカーを実際の障害ドメインにスコープします。短時間の低リスクな障害に対しては、厳格なタイムアウト、単一レイヤーでのリトライ、リトライクォータだけで十分な場合もあります。単に別のパターン名を挙げたいがためにブレーカーを追加してはなりません。

ローカルリソースの分離も依然として必要です。税金計算の依存先に対して専用の並行性制限、コネクションプール、有界キューを割り当て、低速な呼び出しがチェックアウトのすべてのスレッドや接続を使い果たさないようにします。キューに入れられた呼び出しもデッドラインバジェットを消費します。キューから出た時点で完了できない処理は破棄してください。フォールバックは真実かつ説明可能でなければなりません。「税金計算が一時的に利用不可」や手動ワークフローは有効な場合がありますが、不明な税額をゼロとして扱いチェックアウトを成功とみなすことは許されません。

障害条件下で不変条件をテストします。接続拒絶、250 msの低速応答、様々なRetry-After値を持つ429、503、リトライ不可の400、「書き込みコミット済みだがレスポンス喪失」を注入します。1つの論理リクエストがダウンストリームに対して最大2回しか試行しないこと、合計レイテンシが800 msを超えないこと、恒久的なエラーがリトライされないこと、結果不明な書き込みが重複しないことをアサートします。その後、サーキットが開くまで障害を持続させます。高速な遮断、制限されたハーフオープンプローブ、回復後の正常なクローズ、およびバルクヘッドが飽和していないことを確認します。

本番環境のメトリクスでは、論理呼び出しと物理試行を分離する必要があります:エンドツーエンドの成功率とp95/p99、試行ごとの結果とレイテンシ、接続フェーズ対リクエストフェーズのタイムアウト、リトライ増幅率、リトライ回復率と追加レイテンシ、リトライバジェットの残高、ブレーカーの状態と遮断数、ハーフオープンプローブの結果、プールの並行数とキューの深さ、さらに冪等性の競合と照合結果を監視します。最終的な成功率だけを見ていると、ダウンストリームトラフィックを3倍に増やして可用性を買い叩いているシステムを見逃す可能性があります。

質の高い模範解答

「まず、これが同期的な税金計算の参照であり、呼び出し元が800 ms後に待機を終了することを確認します。呼び出し前の処理に120 ms、レスポンス処理に100 msを確保すると、依存先には580 msが割り当てられます。私の初期ポリシーでは合計2回の試行を使用します。各試行は最大220 msで、試行間に[0, 80) msのジッター付きバックオフを設けます。各試行の前に伝播された残りデッドラインを確認するため、キューの待機が長い場合に機械的に2回目の呼び出しがトリガーされることはありません。

障害には分類が必要です。接続確立前の失敗とベンダー定義の一時的な503は、バジェット内でリトライできます。429はRetry-Afterに従いますが、待機によってデッドラインを超える場合は即座に失敗とします。400、401、403はリトライしません。操作が状態を書き込む場合、送信後のタイムアウトは結果不明を意味します。ブラインドに新しいリクエストを発行するのではなく、冪等性キーを再利用するか、ステータスを照会・照合する必要があります。

SDK、ゲートウェイ、サービスメッシュのリトライを棚卸しし、ビジネスデッドラインを把握してトークンリトライバジェットを強制できる単一のレイヤーにリトライを配置します。5つのレイヤーそれぞれで3回試行すると最下層の呼び出しが243倍に増幅する可能性があるため、論理リクエストで割った物理試行回数を監視します。

持続的な障害に対しては、税金計算ベンダーおよび操作単位でサーキットブレーカーをスコープします。Closedでは最小サンプル数で失敗率を測定し、Openではフェイルファストし、Half-Openでは回復前に少数のプローブのみを許可します。依存先には個別の並行性プールと有界キューも割り当てます。縮退時はビジネス承認済みの明示的な状態のみを返し、税額ゼロを捏造することは決してしません。

最後に、フォールトインジェクションを行って、最大2回の試行、800 msの境界、恒久的エラーのリトライ抑止、書き込みの重複防止、制限付きプローブによるサーキット回復を検証します。本番環境では、論理成功率、試行結果、リトライ増幅率、バジェット残高、ブレーカー状態、バルクヘッドの飽和度を総合的に監視します。」

よくある間違い

  • 依存先のタイムアウトを800 ms全体に設定する → ローカル処理を完了する時間が残らず、呼び出し元が諦めた後も依存先がリソースを保持し続けます → デッドラインからローカルおよびネットワークのバジェットを差し引き、残りを伝播させます。
  • 失敗したすべてのレスポンスをリトライする → パラメータや権限のエラーは自然治癒しないため、リトライは負荷と遅延を増やすだけです → プロトコル、エラーコード、副作用の結果によって分類します。
  • すべてのレイヤーで3回の試行を有効にする → 5つのレイヤーによって1回の呼び出しが最下層で243回の試行に増幅する可能性があります → 適切な1つのレイヤーでリトライし、増幅率を測定します。
  • タイムアウトした書き込みを新しいIDで再送信する → 初回の試行がコミットされている可能性があり、二重請求やリソースの重複を引き起こします → 冪等性キーを再利用するか、ステータスを照会・照合します。
  • ジッターや上限なしで指数バックオフを使用する → クライアントが同期した波となって一斉にリトライする可能性があります → 試行回数と経過時間に上限を設け、遅延をランダム化します。
  • サーキットブレーカーをタイムアウトの代替として扱う → 実行中の呼び出しは依然としてスレッド、接続、キューを占有します → 試行ごとのタイムアウトを維持し、並行性を分離します。
  • ハーフオープンが始まった瞬間にすべてのトラフィックを復旧させる → 回復しかけた依存先が再び過負荷に陥ります → 制限されたプローブのみを許可し、回復の成功を明示的に確認します。
  • すべてのベンダーとエンドポイントに対して1つのグローバルブレーカーを使用する → 局所的な障害によって正常なリソースまでブロックされます → ブレーカーの状態を独立した障害ドメインにスコープします。
  • フォールバックから常に成功を返す → 不明なデータが正しいものとして提示され、ビジネスセマンティクスを破壊します → 明示的な制限を持つ承認済みの縮退のみを使用します。
  • 最終的な成功率のみを監視する → リトライストームによってダウンストリームの劣化が一時的に隠蔽される可能性があります → 論理呼び出し、物理試行、追加レイテンシ、飽和度を監視します。

フォローアップ質問と回答

フォローアップ 1:タイムアウトはダウンストリームのp99の何倍に設定すべきですか?

固定の倍数はありません。許容可能な誤タイムアウト率を選択し、一致するレイテンシパーセンタイルから開始して、ネットワーク遅延、クロスリージョン呼び出し、接続確立、わずかな変動に対する余裕を追加します。その結果はアップストリームのデッドラインに収まる必要があります。p99がp50に近い場合、わずかなレイテンシの変動で多数のタイムアウトが発生する可能性があるため、余裕を多めに取ることが適切な場合があります。DNS、TLS、コネクションプール待機がタイマーに含まれているか確認してください。

フォローアップ 2:429と503は両方ともリトライ可能ですか?

条件付きでのみ可能です。429にはRetry-Afterを優先しますが、待機と次回試行が残りバジェットに収まる必要があります。収まらない場合は認識可能な失敗を返します。503はベンダーのコントラクトで一時的と明記されている場合にのみリトライします。どちらもリトライバジェットを消費します。過負荷のサーバーからのシグナルを、さらなる即時トラフィックに変換してはなりません。

フォローアップ 3:高いリトライ回復率が、なぜシステムの異常を示している場合があるのですか?

最終的な成功が、余分な負荷と遅延を代償に得られている可能性があるためです。100回の論理呼び出しが180回のダウンストリーム試行を生成する場合、増幅率は1.8です。依存先がキャパシティの限界に近い場合、その80%の追加負荷が回復を遅らせる可能性があります。回復率は、増幅率、試行レイテンシ、バジェット枯渇、ダウンストリームの飽和度とともに評価してください。

フォローアップ 4:サーキットがオープンのときでも、レートリミットやバルクヘッドは必要ですか?

はい。ブレーカーは特定の依存先への新しい呼び出しをブロックしますが、ローカルリクエストはすでに処理中またはキューに入っている可能性があり、他の依存先がリソースを消費し続けることもあります。並行性制限、個別プール、有界キューはローカルキャパシティを保護し、入口のレートリミットは新規負荷を制御します。ハーフオープンプローブにも、無制限の共有キャパシティではなく、独立した小さな割り当てが必要です。

フォローアップ 5:各インスタンスが独自のブレーカー状態を保持することは問題ですか?

状態が分岐する可能性はありますが、強固に共有された状態は新たな同期依存関係とレイテンシを追加します。一般的な設計では、各インスタンスに同じ設定とローカル状態を持たせます。トラフィックが多い場合、各インスタンスは十分なサンプルを観測し、障害シグナルは自然に広がります。低トラフィックのインスタンスやグローバルに調整された障害ドメインでは、サービスメッシュや集中管理レイヤーが妥当となる場合があります。どちらの場合も、スコープ、サンプルサイズ、障害時の最大追加トラフィックを明記してください。

フォローアップ 6:これを非同期キューに移行すべきなのはどのような場合ですか?

HTTPレスポンスに税金計算結果が不要な場合、または回復にユーザーのデッドラインよりはるかに長い時間がかかる可能性がある場合は、非同期処理の方が適しています。タスクを永続化し、照会可能なステータスを返し、コンシューマーに独自のリトライおよびデッドレターポリシーを使用させます。キューを使用しても冪等性、有効期限、縮退の考慮事項はなくなりませんが、長時間の回復を同期接続および800 msのバジェットから切り離すことができます。

フォローアップ 7:設定事故を起こさずにこれをロールアウトするにはどうすればよいですか?

まずは実際のリトライを追加したりトラフィックを拒絶したりせず、行われるはずだったタイムアウト、リトライ、サーキットの決定をログ記録することから始めます。SDKが余分な試行を隠蔽していないことを確認します。次にベンダー単位または少数のインスタンスでカナリアリリースを行い、グローバルリトライバジェットに上限を設け、迅速な無効化スイッチ(キルスイッチ)を維持します。展開を拡大する前に、試行ボリューム、エンドツーエンドのテールレイテンシ、オープン持続時間、ハーフオープン時の失敗、依存先のキャパシティを監視してください。

公開情報ソース

関連する質問