代表的な面接トピック

バックエンド面接:部分的な失敗を伴うバッチ API をどのように設計しますか?

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

質問

1 リクエストあたり最大 1,000 件のリソースを受け付けるバッチ更新 API が必要です。一部のアイテムはバリデーション、認可、または一時的な依存関係により失敗する可能性があります。クライアントが部分的な成功を完全な成功と誤認しないように、リクエスト、レスポンス、冪等性、リトライ、可観測性をどのように設計しますか?

プロンプトとスコープ

この質問では、バックエンドエンジニアが「バッチ」に厳密な実行および結果セマンティクスを付与できるかどうかをテストします。顧客が注文ラベルやユーザー設定を一括更新したい場合を想定してください。各アイテムは独立している場合もあれば、クォータ、バージョン、依存関係を共有している場合もあり、サーバーが一部のアイテムを完了した後にネットワークタイムアウトが発生する可能性があります。クライアントが安全に処理を継続できるよう、同期と非同期の境界を定義する必要があります。

この設問は、バックエンドエンジニア、API デザイナー、プラットフォームエンジニアを対象としています。特定の REST、gRPC、キューの選択ではなく、アトミック性、アイテムごとの状態、リクエストの識別性、冪等なリトライ、認可とリソース制限、レスポンスの互換性、リカバリに焦点を当ててください。デフォルトの動作、部分的な成功がどのように明示的に選択されるか、そして完全な失敗、部分的な失敗、不明な結果の違いを述べてください。

面接官が評価している点

優れた回答は、アトミック処理、オプトイン方式の部分的成功、または非同期処理を選択する前に、アイテム間に依存関係があるかどうかを確認します。単に HTTP 200 と失敗件数だけを返すのではなく、すべての入力を安定した結果、エラークラス、リトライ可能性に対応付けます。Google AIP-234 では、既存の同期バッチメソッドを部分的成功に変更するとクライアントが破損する可能性があると指摘し、インデックスごとの詳細な失敗状態を推奨しています。Stripe の冪等性ガイダンスでは、リトライを同一パラメータにバインドし、最初のリクエストの結果を保持します。制限、監査、モニタリングについても言及してください。

確認すべき明確化のための質問

  • アイテム間に順序やトランザクションの依存関係はありますか?ビジネス要件は All-or-Nothing(すべて成功かすべて失敗)である必要がありますか?
  • 処理によって外部への副作用が発生しますか?また、冪等キー(Idempotency Key)によって再実行を安全にできますか?
  • クライアントは同期的な結果を必要としていますか?それとも操作(Operation)を受け取り、進捗をポーリングまたはサブスクライブできますか?
  • バッチの上限、ボディサイズ、タイムアウト、テナントクォータ、公平性(Fairness)の要件はどのようになっていますか?
  • どのエラーがリトライ可能で、どのエラーに入力の修正が必要ですか?また、不明な結果はどのように問い合わせ可能ですか?

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

「アトミックなバッチと部分的成功のバッチを分離し、より安全な動作をデフォルトにします。部分的成功は、クライアントが明示的にオプトインし、ビジネス上許容される場合にのみ有効にします。すべての入力に安定したインデックスまたはクライアントのリクエスト ID を付与し、サーバーはバッチレベルおよびアイテムレベルの冪等性状態、結果、エラークラスを記録します。同期処理には上限を設け、大規模な処理はシャードに分割して実行し進捗を公開する Operation を生成します。レスポンスでは成功、恒久的な失敗、一時的な失敗、不明な状態を区別し、リトライ時は同じアイテムキーを再利用します。副作用が重複したり失われたりしないことを証明するために、レート制限、監査ログ、メトリクス、フォールトインジェクションを用いて設計を検証します。」

ステップバイステップの回答

ステップ 1: アトミック性モデルを選択する

送金の双方の処理が同時に成功しなければならないなど、アイテム間で不可分なビジネス不変条件を共有している場合は、アトミックなバッチトランザクションを使用するか、バッチインターフェース自体を拒否します。各アイテムが独立している場合は、部分的成功を検討します。「ベストエフォート」という言葉でモデルを曖昧にしないでください。すべて成功したのか、すべて失敗したのか、一部完了したのか、サーバーが結果を確認できないのかをクライアントが把握できなければなりません。

ステップ 2: リクエストとアイテムの同一性を定義する

バッチ ID、アイテム配列、およびクライアントが生成したアイテムリクエスト ID を含めます。アイテム ID はそのビジネススコープ内で安定している必要があります。同一 ID による重複送信では重要なパラメータを比較する必要があり、異なるパラメータの場合はサイレントな上書きではなくコンフリクトを発生させるべきです。バッチ ID はトレーシングに役立ちますが、アイテムの冪等性を代替することはできません。部分的なリトライには元のバッチの失敗したアイテムのみが含まれる可能性があるためです。

ステップ 3: 同期/非同期の境界を設定する

小さなバッチであれば、全体的な時間およびリソースのバジェットを設定した上で、アイテムごとの最終結果を同期的に返すことができます。大規模なバッチや外部システムを呼び出す操作では、Operation ID を返し、制限付きのシャードで実行し、進捗を永続化する必要があります。クライアントは完了、処理中、リトライ可能、恒久的な失敗の件数を問い合わせます。切断が発生した場合、クライアントは副作用を再開するのではなく状態を問い合わせます。

ステップ 4: パース可能なレスポンスを設計する

サーバー側で処理順序が変更された場合でも、クライアントが入力を基準に結果を特定できるようにレスポンスを設計する必要があります。安定したインデックスとクライアント ID を使用し、機械可読なエラーコード、リトライ可能性、リトライ時の状態変化の可能性、および安全なユーザーメッセージを含めます。例:

json
{
  "batch_id": "b_123",
  "status": "PARTIAL",
  "results": [
    {"index": 0, "request_id": "r_0", "status": "SUCCEEDED"},
    {"index": 1, "request_id": "r_1", "status": "FAILED", "error": {"code": "VERSION_CONFLICT", "retryable": false}}
  ],
  "next_page_token": null
}

Google AIP-234 では、非同期バッチ更新に対して入力インデックスから詳細ステータスへの failed_requests マップを推奨しています。これにより、クライアントがリクエスト ID とリクエストの対応マップを保持する負担を回避し、機密性の高いリクエストボディの再送を防ぎます。既存の同期 API に部分的成功セマンティクスを導入する必要がある場合は、新しいバージョンをリリースするか、明示的なフィールドでネゴシエーションを行い、既存のクライアントが成功ステータスをすべてのアイテムの完了と誤認しないようにします。

ステップ 5: 冪等性、リトライ、不明な結果を処理する

サーバーは、副作用が始まる前に無効なパラメータを拒否することができ、その際冪等な結果を保存する必要はありません。実行が開始されたら、結果または問い合わせ可能な進行中ステータスを保存します。ネットワークタイムアウトが発生した場合、クライアントは推測でバッチ全体を再試行してはなりません。バッチキーとアイテムキーを再利用し、不明なアイテムの状態を問い合わせ、明確にリトライ可能なアイテムのみを再試行します。一時的なエラーにはバックオフとジッターを使用し、恒久的なエラーには入力の変更を要求し、再利用されたキーに異なるパラメータが含まれている場合はコンフリクトを返します。

ステップ 6: リソースと順序付けを分離する

処理を制限付きのシャードに分割し、テナント、バッチ、依存関係ごとの並行処理数を制限します。依存関係のあるアイテムはトポロジーまたは明示的なフェーズに従って実行し、独立したアイテムは障害の嵐(failure storm)を防ぐために共有のリトライバジェットを用いて並列実行します。各アイテムの実行前に認可とクォータをチェックし、バッチエンドポイントが単一アイテム API のポリシーをバイパスできないようにします。

ステップ 7: エラー、キャンセル、リカバリのセマンティクスを定義する

入力バリデーション、認可、バージョンコンフリクト、クォータ、一時的な依存関係、不明な結果のエラーを分類します。キャンセルは開始されていないアイテムを停止します。完了した副作用をロールバックしたかのように見せかけることはできません。ビジネス上取り消しが必要な場合は、個別の補償トランザクション(Compensation operation)を提供します。バックグラウンドタスク、結果、元のリクエストを永続化し、ワーカーが再起動した際も各アイテムを最初から再実行するのではなく、アイテムの状態から再開できるようにします。

ステップ 8: 整合性と運用の可視性を検証する

全件成功、全件失敗、混在した結果、重複リクエスト、パラメータのコンフリクト、タイムアウト後の問い合わせ、依存関係のフラッピング(不安定化)、キャンセルの競合、ワーカーの再起動をテストします。バッチの成功率、アイテムごとのエラークラス、不明な状態、リトライの増幅、キューの滞留時間、処理レイテンシ、クォータによる拒否、重複送信を監視します。サポート担当者がどのアイテムが完了したかを正確に説明できるよう、バッチ、アイテム、実行者、認可判定、最終結果を監査ログに記録します。

設計のトレードオフと境界

部分的成功は、自動的により高度な選択肢となるわけではありません。顧客が 1 件ずつ修正できる独立したアイテムには適していますが、残高、在庫、リソース間の不変条件は、アトミック性または明示的なワークフローを採用する方が安全です。HTTP 207 Multi-Status は複数のリソースステータスを保持できますが、RFC 4918 では WebDAV 向けに定義されています。一般的な JSON API は、単に 207 を返したからといってクライアントが部分的な結果を理解できると仮定すべきではありません。アイテムのセマンティクスは安定したレスポンスボディに含め、既存クライアントとの互換性を考慮してステータスコードを選択してください。

どのような場合にアトミックなバッチを選択すべきですか?

いずれかのアイテムの失敗によって全体の集約状態が無効になる場合や、補償処理が許容されない場合は、アトミック性を選択します。トランザクション、プリフライトチェック、またはワークフローを使用しますが、シャーディングされた処理や外部の副作用は単一のデータベーストランザクションを共有できないことを認識してください。これらにはリザーブ、コミット、補償のフェーズが必要になる場合があります。

どのような場合に非同期処理を選択すべきですか?

所要時間が予測できない場合、バッチが大きい場合、外部呼び出しが含まれる場合、またはクライアントが接続を開いたまま保持すべきでない場合に Operation を返します。その状態は再問い合わせ可能である必要があり、結果はページネーションされるべきです。進捗において「受付済み(Accepted)」を「完了(Completed)」と呼んではいけません。

障害訓練と移行計画

限定されたテナントセットでパイロット運用を行い、アイテムの状態とリトライ動作を記録してから、バッチの上限を段階的に引き上げます。30 件目のアイテムが成功した後のネットワーク切断、503 を返し続ける依存関係、同一キーで異なるパラメータの送信、Operation ワーカーの再起動などを注入(カオスエンジニアリング)します。クライアントが不明な結果を問い合わせ可能になり、サーバーが副作用を重複させないことが証明された後にのみ、クォータを拡張するか部分的成功を有効化します。

同期 API を部分的成功に対応させるにはどのように移行しますか?

古いバージョンのアトミックなセマンティクスを維持しつつ、Operation とアイテムごとの失敗を返す新しいバージョンを追加します。あるいは、明示的な return_partial_success フィールドを必須とし、そのフィールドがない場合は従来の動作を維持します。クライアントが独自に解釈を変更してしまわないよう、ステータスコード、レスポンスフィールド、リトライルール、非推奨(Deprecation)日を明確にドキュメント化します。

クライアントの実装の正しさをどのように評価しますか?

クライアントがアイテム ID を永続化しているか、リトライ可能な失敗のみを再試行しているか、不明な結果を問い合わせているか、重複した副作用や無効なリトライを回避しているかを観察します。サーバー側で未知のフィールドや重複リクエストを許容しつつ、主要な SDK にパースおよび問い合わせ用のヘルパーを提供します。

よくある間違いとフォローアップ

HTTP 200 と失敗件数のみを返す

古いクライアントは部分的な完了を完全な完了として処理してしまう可能性があり、どのアイテムをリトライすべきかも判断できません。レスポンスには全体の状態、アイテムの識別子、エラークラス、次のアクションが必要です。

いずれかの失敗に対してバッチ全体をリトライする

すでに成功した副作用が繰り返される可能性があります。まずバッチとアイテムの冪等性状態を問い合わせ、明示的な一時的エラーのみをリトライし、パラメータが変更された場合は新しいビジネスリクエスト ID を使用します。

結果はどのように順序付けられるべきですか?

完了順ではなく、入力インデックスまたは安定したリクエスト ID によって結果を関連付けます。結果がページネーションされる場合も識別子を保持し、クライアントが安全にページをマージできるようにします。

1 件のアイテムが成功した後にバッチがキャンセルされた場合、ロールバックできますか?

キャンセルは開始されていないアイテムにのみ影響します。すでに発生した外部の副作用には補償 API または手動での対応が必要です。「バッチのキャンセル」はロールバックを保証するものではありません。

バッチエンドポイントが認可をバイパスするのをどのように防ぎますか?

バッチレベルでテナントと操作の権限をチェックし、各アイテムの実行前に対象リソースの所有権、バージョン、フィールド権限を再チェックします。バッチ処理はスケジューリングを変更するだけであり、認可のスコープを変更するものではありません。

最終的な部分的な失敗をどのように説明しますか?

監査可能なバッチ ID、アイテム ID、ステータス、エラーコード、リトライ可能性、タイムスタンプを提供します。サポート担当者には安全なビジネス向けの説明を提供し、エンジニアリングの診断用にトレースと依存関係のエラーを保持します。

公開情報ソース

関連する質問