代表的な面接トピック

RPC間でデッドライン伝播とキャンセルはどのように機能すべきか?

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

質問

リクエストの予算が2秒で、複数のサービスを呼び出します。ダウンストリームの処理がリクエストよりも長く生き残らないように、デッドラインとキャンセルをどのように伝播させますか?

質問とスコープ

サービスAがBを呼び出し、BがCを呼び出すリクエストパスを設計しています。クライアントは受け付けから応答まで2秒を許容しています。AはBを呼び出す前に500ミリ秒を消費する可能性があり、BはCと重要度の低い監査サービスを呼び出す可能性があります。デッドライン、キャンセル、リトライ、可観測性の規約について説明してください。

各ホップは独立して失敗する可能性があり、クロックは完全に同期されておらず、タイムアウトした読み取り専用RPCは操作が冪等である場合にのみリトライできるものとします。目標は、応答のシリアライズと転送に十分な予算を確保しつつ、無駄になる処理を停止することです。

面接官がテストしていること

InterviewStackのスタッフレベルのシステムデザイン評価基準では、候補者がレイテンシと障害の制約を明確にし、ボトルネックを特定し、トレードオフを正当化することが求められます。優れた回答は、これらのシグナルを明示的な予算規約へと変換します:

  • エッジで1つの絶対的なリクエストデッドラインが設定され、各ホップは新しいタイムアウトを作成するのではなく、残りの予算を導き出します。
  • キャンセルはヘッジされたブランチや並行ブランチを含むコールツリーに従うため、完了した応答はもはや役に立たない処理を停止します。
  • リトライは同じ残り予算を消費し、冪等性と試行回数の上限によって制限されます。
  • メトリクスは、デッドラインの枯渇、呼び出し元によるキャンセル、キュー遅延、ダウンストリームの拒否、キャンセル後に完了した有用な処理を区別します。

回答前の明確化のための質問

  1. 2秒の制限はユーザーから見えるSLOですか、それとも正確性に関する厳格なデッドラインですか?厳格なデッドラインでは遅延した結果は無効になりますが、SLOであれば慎重に制限されたバックグラウンド完了パスが許可されます。
  2. 応答にとって重要な呼び出しはどれですか?重要度の低い監査やレコメンデーションの呼び出しは、切り離したり、サンプリングしたり、縮退応答で返したりできますが、課金の承認はそれができません。
  3. 書き込みは冪等ですか?リトライ予算は、読み取りや冪等性キーを持つコマンドに対しては安全ですが、保護されていない副作用に対しては安全ではありません。
  4. RPCスタックはコンテキストを自動的に伝播しますか?そうでない場合は、インターセプターまたはミドルウェアの規約を定義し、すべての言語境界でテストします。
  5. チェックポイント付きのエクスポートなど、キャンセル後も処理を完了させる必要がありますか?その例外には、キャンセルを暗黙的に無視するのではなく、永続的なジョブ規約が必要です。

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

「インgressで1つのデッドラインを設定し、RPCコンテキストを通じて残り予算を引き継ぎます。各サービスはキューイング前および時間のかかる処理中にキャンセルを確認し、応答とネットワークのバッファを残した、より短い子予算を渡します。リトライは残り時間のみを使用し、上限が設けられ、冪等性が必要です。呼び出し元が結果を取得するかキャンセルすると、キャンセルシグナルはすべてのブランチにファンアウトされます。デッドライン超過率、各ホップでの残り予算、キュー時間、キャンセルレイテンシ、遅延処理を測定し、チェーンの負荷テストを行って、遅延またはキャンセルされた依存関係を注入します。」

ステップバイステップの詳細な回答

1. 予算の設定と表現

エッジは絶対デッドライン(例えばt0 + 2s)を記録します。これをある一時点として扱うことで、各ホップが暗黙的にさらに2秒を追加することを防ぎます。サービスはremaining = deadline - nowを計算し、シリアライズとネットワーク転送のために小さなローカルマージンを確保します。このマージンはポリシーであり、普遍的な定数ではありません。パーセンタイルレイテンシデータで検証してください。

gRPCのガイダンスでは、デッドラインとタイムアウトを区別し、明示的なクライアントデッドラインを推奨しています。また、クロックスキューによってダウンストリームサーバーが元の予算を超えて待機することがないように、伝播されたデッドラインを残り時間に変換します。したがって、通信規約(wire contract)は、呼び出し元が忘れる可能性のある自由形式のアプリケーションヘッダーではなく、RPCライブラリを介してデッドラインコンテキストを運ぶ必要があります。

2. クリティカルパスによる予算の消費

Aが500ミリ秒を消費したと仮定します。Bには最大1.5秒が転送されます。Bがローカル処理に400ミリ秒を必要とし、Cを呼び出す場合、Bは『自身の残り予算から100ミリ秒の応答マージンを差し引いた値』と『Cのサービス固有の上限』のいずれか小さい方を転送します。並行呼び出しは同じ親デッドラインを共有します。それぞれが完全な1.5秒を受け取るわけではありません。

キュー受付時に残り予算を確認します。推定キュー遅延がすでにそれを超えている場合は、即座に拒否するか、文書化された縮退応答を返します。これにより、成功が不可能な場合にリクエストがワーカーを占有し続けるのを防ぎます。

3. 単なる期限切れだけでなくキャンセルを伝播する

デッドラインの期限切れはキャンセルの理由の1つです。呼び出し元が接続を閉じることや、別のブランチがヘッジに勝つことも理由になります。すべての子RPCは親のキャンセルコンテキストを受け取ります。ワーカーは負荷の高いステップの前やループ中の一定の間隔でそれを確認し、パーミットを解放してストリームを閉じます。

Google SREは、デッドラインの伝播だけでは、より深い呼び出しが早期に失敗したときに処理のリークが発生する可能性があると警告しています。キャンセルは上位に伝わり、兄弟ブランチにファンアウトする必要があります。チェックポイント付きの永続ジョブは安全なチェックポイントを完了できますが、それはキャンセルを無視する同期RPCではなく、独自のステータスAPIを持つ明示的な非同期ワークフローです。

4. リトライを予算認識型にする

attempt_deadline = min(parent_remaining - response_margin, per-attempt_cap)を計算します。次の試行が親のデッドライン前に完了できない場合は停止します。一時的な障害のみ、かつ本質的に冪等であるか冪等性キーによって保護されている操作のみをリトライします。ヘッジされたリクエストは同じ親予算を使用し、受け入れ可能な応答が1つ届き次第、敗北した試行をキャンセルします。

長いリトライタイムアウトを、残り予算を認識しないサーキットブレーカーと組み合わせないでください。ダウンストリームサービスが新規リクエストと期限切れ間近のリトライを区別できるように、試行回数と元のデッドラインを記録します。

5. 観測可能な障害セマンティクスを定義する

gRPCのDEADLINE_EXCEEDEDなど、デッドライン枯渇に対して安定したステータスを返し、ログに元のキャンセル原因を保持します。トレースフィールドには、元のデッドライン、進入時および終了時の残り予算、キュー遅延、試行回数、キャンセルから停止までのレイテンシを含める必要があります。エンドツーエンドのレイテンシだけでなく、キューで消費された予算や、キャンセル後の有用な処理に対してもアラートを設定します。

3ホップの統合テストハーネスで規約をテストします。Cを残り予算を超えて遅延させ、Bがキューに入っている間にAでキャンセルし、最初のヘッジを成功させ、クロックオフセットを注入します。明示的に宣言されたチェックポイントを除き、親のデッドラインを超えて実行される子が存在しないこと、リトライが停止すること、パーミットが解放されることを検証(assert)します。

高品質な回答例

「私はイングレスを2秒のデッドラインの唯一のオーナーとします。AはRPCコンテキストでそれを受け取り、500ミリ秒後に残り予算をBに転送します。BはCを呼び出す前に自身の応答マージンを差し引きます。並行ブランチも同じ親デッドラインを共有します。すべての子はキューイング前および制限された処理を実行中にキャンセルを監視し、クライアントが切断された場合やヘッジが勝利した場合にキャンセルがファンアウトされます。リトライは残り予算に制限され、冪等性が必要です。遅延した監査呼び出しは永続ジョブに切り離すことができますが、クリティカルな応答では遅延処理が有用であるかのように見せかけることはできません。遅延依存関係、キュー遅延、キャンセルの競合、クロックスキューを注入し、各ホップでの予算を示すトレースを使用して規約を実証します。」

よくある間違い

  • すべてのホップに新しいタイムアウトを与える → コールツリーがユーザー予算のN倍実行される可能性があります → 1つのデッドラインを伝播し、残り時間を計算します。
  • 期限切れを伝播するが呼び出し元のキャンセルを無視する → ヘッジされたリクエストや破棄されたリクエストがワーカーを消費し続けます → キャンセルコンテキストをファンアウトし、停止レイテンシを測定します。
  • すべてのタイムアウトをリトライする → 非冪等な書き込みによって副作用が重複する可能性があり、期限切れのリトライは負荷を増幅させます → 冪等性を要求し、エラーを分類し、残り予算によって試行回数を制限します。
  • 予算を確認する前にキューに入れる → 失敗が確定しているリクエストが貴重なキャパシティを占有します → キュー遅延が残り時間に収まらない場合は、拒否または縮退させます。
  • 測定なしで固定マージンを使用する → 大きなペイロードが常に失敗するか、レイテンシのバッファが無駄になります → 転送およびシリアライズのパーセンタイルからマージンを導き出します。
  • 遅延した処理を成功として隠蔽する → リソースがリークしている間にダッシュボードには正常な応答が表示されます → 同期デッドラインと明示的なチェックポイント付きジョブを分離します。

フォローアップと回答

サービス間でクロックが異なる場合はどうなりますか?

独立したマシンの生のウォールクロックタイムスタンプを比較しないでください。RPCライブラリの残り時間変換を使用するか、経過時間を差し引いた後にタイムアウトを伝播します。テストハーネスにクロックオフセットテストを追加します。

ダウンストリームでデッドラインを延長すべき状況はありますか?

プロダクトの規約を非同期ジョブに変更する場合のみです。同期的な子のデッドラインを延長しても、親の遅延応答を有効にすることはできません。無駄な処理が増えるだけです。

時間のかかるペイロードに対してどのように予算を確保しますか?

シリアライズとネットワークのテールレイテンシを別々に測定し、制限されたマージンを確保し、ペイロードサイズに上限を設けるかストリーミングを使用します。マージンが繰り返し予算を消費する場合は、すべてのタイムアウトを暗黙的に増やすのではなく、SLOまたは応答の形式を変更します。

キャンセル後も存続するものは何ですか?

チェックポイントの書き込みや監査イベントなど、明示的に永続的で冪等な処理のみです。これはジョブ識別子の下で観測可能でなければならず、同期リクエストのワーカーや接続を保持してはなりません。

公開情報ソース

関連する質問