代表的な面接トピック

バックエンド面接:gRPC のデッドラインとキャンセレーションはどのように伝播させるか?

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

質問

API リクエストが 3 つの gRPC サービスを順次呼び出しますが、ユーザーが途中で離脱する可能性があります。デッドライン、キャンセレーション、リトライ、およびサーバーのクリーンアップの動作を設計してください。子呼び出しが親の予算内に留まる方法、ストリーミング RPC が終了する方法、およびキャンセル後に書き込みがその結果を確認する方法を説明してください。

課題とコンテキスト

API リクエストは、ユーザー、カタログ、レコメンデーションの各 gRPC サービスを順次呼び出します。レコメンデーションの呼び出しが遅延している間に、ユーザーが別のページへ移動する可能性があります。システムは、エンドツーエンドのレイテンシを制限し、サーバーリソースを解放し、リトライの増幅を回避し、外部の副作用の後にキャンセルが発生した場合でも書き込み結果を照会できるように維持する必要があります。

gRPC ガイドでは、デッドラインをクライアントの待機上限として定義し、DEADLINE_EXCEEDED を説明しています。キャンセレーションは不要になった RPC を終了します。優れた回答は、すべてのレイヤーに独立した 5 秒のタイムアウトを割り当てるのではなく、予算の伝播、リトライ制限、ストリームのクリーンアップ、および不明な結果の処理を明確に区別します。

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

絶対デッドラインまたは残り予算、子 RPC やデータベースへの伝播、同じ総予算を消費するリトライ、および「応答なし」が「書き込みが行われなかった」を意味すると推論しない姿勢を確認します。また、候補者は観測可能なフィールドと障害注入テスト計画を提示できる必要があります。

明確化のための質問

  • 最上位の SLO、クライアントのデッドライン、および部分的な結果に関するポリシーは何ですか?
  • 3 つの呼び出しは読み取り専用ですか、それとも決済やその他の副作用が含まれますか?
  • どのステータスがリトライ可能ですか?また、冪等性キーは存在しますか?
  • ストリームメッセージはリプレイ可能ですか?また、カーソルやシーケンス番号はありますか?
  • データベースや外部 API はキャンセレーションを受け入れますか?
  • キャンセル後のコントラクトは、ステータス照会、補償処理、それとも照合(リコンシリエーション)ですか?

30秒の回答

「エッジで 1 つの絶対デッドラインを作成し、子呼び出しは残り予算のみを継承します。キャンセレーションをすべての RPC、データベース、およびキャンセル可能なワーカーに伝播させます。試行回数の上限、バックオフ、ジッター、および同じ総デッドラインを設けて、安全な一時的障害のみをリトライします。書き込みにはブラインドな新規リトライではなく、操作 ID またはステータス照会を使用します。ストリーミング呼び出しはイテレータと接続を閉じ、ログには残り予算、キャンセル理由、試行回数、および最終ステータスを記録します。」

ステップごとのソリューション

ステップ 1: エンドツーエンドのタイムバジェットを定義する

クライアントのデッドラインを上限として扱います。各サービスは remaining = deadline - now を計算し、子デッドラインをその時点より遅くならないように設定し、キューイング、シリアライゼーション、およびレスポンス処理のための時間を確保します。

text
parent_deadline = 2.0s from request start
child_deadline = min(parent_deadline, now + remaining_budget)

すべてのレイヤーにさらに 2 秒ずつ追加してはいけません。3 ホップのチェーンでは 6 秒を超える可能性があります。RPC フレームワークがデッドラインを伝達する一方で、キューや作業のローカルな測定にはモノトニッククロックを使用します。

ステップ 2: キャンセレーションを伝播し作業を停止する

画面遷移、明示的なキャンセル、およびデッドライン切れは、サーバーハンドラーに到達する必要があります。ハンドラーのキャンセレーショントークンをデータベース、HTTP クライアント、キュー待機、およびストリームイテレータに渡します。バックグラウンド処理が継続している間にエラーを返すことは、リソースリークにつながります。

キャンセレーションは、接続、一時ファイル、ロックを解放し、新しいメッセージの生成を停止し、完了したステージを記録する必要があります。コミットされたトランザクションを元に戻すことはできないため、後でステータス照会や補償処理が必要になります。

ステップ 3: 残り予算内でリトライする

最大試行回数、指数バックオフ、およびジッターを伴い、UNAVAILABLE などの明示的に一時的なエラーのみをリトライします。すべての試行で同じデッドラインが使用され、期限切れになると残りのリトライはスキップされます。

text
for attempt in 1..maxAttempts:
  if remaining(deadline) <= backoff: stop
  result = call(child, deadline, cancellation)
  if result is success or permanent_error: return result
  sleep(jittered_backoff, cancellation)
return DEADLINE_EXCEEDED

呼び出しチェーン全体で 1 つのリトライオーナーを選択します。プロキシとクライアントが処理を重複させないように、リトライの増幅とレイヤーごとの試行を追跡します。

ステップ 4: 読み取りと書き込みを分離する

読み取りは、コントラクトで許可されている場合にリトライできます。書き込みには、操作 ID、ユニーク制約、およびステータス照会が必要です。外部書き込みの後にキャンセルが発生した場合、呼び出し元は UNKNOWN の結果を受け取るため、直ちに新しい ID を作成してはなりません。

text
PENDING -> CONFIRMED
       \-> FAILED
       \-> UNKNOWN (reconcile before retry)

ステータスエンドポイントはオーソリティの結果を返します。デッドラインは待機を制御するものであり、システム間のアトミックなロールバックを作成するものではありません。

ステップ 5: ストリーミング RPC のライフサイクルを処理する

総デッドラインを設定し、適切な場合にはアイドルタイムアウトを設定します。読み取りごとにキャンセレーションを確認します。クライアントが離脱した場合は、生成を停止し、データベースカーソルまたはサブスクリプションを閉じます。最初から副作用をリプレイするのではなく、カーソルまたはシーケンス番号を使用して再接続します。

最終メッセージ時刻、キャンセル理由、再接続、およびバックログを記録します。正常なシャットダウン中は、アクティブなストリームが残りのデッドライン内で終了できるようにするか、識別可能なキャンセルステータスを返します。

ステップ 6: 伝播とリソース制限を検証する

親のキャンセル、各子のタイムアウト、低速なデータベース処理、一時的な UNAVAILABLE、永続的エラー、ストリームの中断、書き込み後の応答喪失、多層リトライ、および正常なサーバー停止をテストします。子が親のデッドライン前に停止すること、キャンセルによってバックグラウンド処理が残らないこと、および試行回数が予算内に収まることをアサートします。

センシティブなペイロードを含めずに、RPC メソッド、トレース ID、残りデッドライン、試行回数、ステータス、キャンセル元、および操作 ID を記録します。負荷テストでは、平均成功率だけでなく、接続、キュー、リトライ増幅、CPU、およびテールレイテンシを観察する必要があります。

模範回答

「エッジで絶対デッドラインを作成し、残り予算を 3 つのサービスすべてに伝播させます。すべてのハンドラーはキャンセレーションをデータベース、HTTP 呼び出し、ストリームイテレータに渡し、リソースを解放してからリターンします。読み取り呼び出しは、単一の総デッドラインと制限付きジッターを用いて、安全な一時的エラーのみをリトライします。」

「書き込みには操作 ID を付与し、キャンセル後は結果を UNKNOWN とマークしてステータスを照会します。ストリームには総デッドライン、カーソル、および再接続ポリシーを使用します。親のキャンセル、子のタイムアウト、失われたレスポンス、正常停止を注入し、リークやリトライの増幅がないことを検証します。」

よくある間違い

  • 各レイヤーで完全なタイムアウトをリセットする → チェーンがエッジ SLO を超過する → 単一の絶対デッドラインを伝播させる。
  • ハンドラーからリターンしてキャンセル済みとみなす → ダウンストリームの作業がまだ実行されている → キャンセレーションを伝播してクリーンアップする。
  • すべてのエラーをリトライする → 負荷が増幅する → セマンティクス、予算、試行回数制限に基づいてリトライする。
  • タイムアウト後に新しい書き込み ID を作成する → 最初の書き込みが成功している可能性がある → オーソリティに問い合わせるか補償処理を行う。
  • ストリームを最初から再接続する → メッセージと副作用が重複する → カーソル、シーケンス、および冪等なコンシューマーを使用する。
  • 最終エラーのみをログに記録する → どのレイヤーも予算枯渇を説明できない → ホップごとの残りデッドラインと試行回数をログに記録する。

フォローアップと回答

フォローアップ 1: 子サービスが親のデッドラインを延長することはできますか?

できません。処理により長い時間が必要な場合は、非同期化して操作 ID を返します。同期リクエストを密かに延長することは、エッジ SLO を損ないます。

フォローアップ 2: キャンセレーションによってコミット済みのデータベーストランザクションを元に戻すことはできますか?

確実にはできません。開始されていない、または完了していない作業は停止しますが、コミットされた作業にはステータス照会、補償処理、または照合が必要です。API は観測可能な状態を公開する必要があります。

フォローアップ 3: 二重リトライをどのように防ぎますか?

1 つのリトライオーナーを割り当て、他のレイヤーにはエラーを伝播させます。試行回数と総予算のフィールドを共有し、試行回数に上限を設け、すべてのレイヤーで増幅を監視します。

フォローアップ 4: アイドル状態のストリームは常にキャンセルすべきですか?

意図的な長時間接続ストリームと、放棄されたストリームを区別します。総デッドライン、アイドルタイムアウト、またはハートビートを使用し、リソースを永久に保持する代わりに再接続可能な状態を返します。

フォローアップ 5: デッドライン後もサーバーが計算を続けている場合はどうしますか?

ハンドラーとダウンストリームの作業をキャンセレーション対応(cancellation-aware)にします。キャンセルできない作業は、リクエストスレッドを占有し続けるのではなく、操作 ID とリソース制限を持つ有界なバックグラウンドキューに移動します。

公開情報ソース

関連する質問