代表的な面接トピック

バックエンド面接:サービスを過負荷から保護するにはどうすればよいか?

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

質問

ある注文サービスは、p99レイテンシ250ミリ秒において毎秒3,000リクエストの負荷テスト済み持続可能キャパシティを持っています。プロモーションによってリクエスト到着数が毎秒8,000リクエストに急増し、在庫依存関係が遅延し、クライアントがリトライを開始し、新しいインスタンスの準備に3分かかります。過剰なトラフィックが連鎖的な障害を引き起こすのを防ぎつつ、重要な注文送信の可用性を維持するにはどうすればよいですか?

プロンプトとこの質問が適用される場面

ある注文サービスは、p99レイテンシ250ミリ秒において毎秒3,000リクエストの負荷テスト済み持続可能キャパシティを持っています。プロモーション中、到着数は毎秒8,000リクエストに増加します。在庫依存関係が遅延し、処理中のリクエスト(in-flight)数とキューの滞留時間がともに増加し、クライアントがリトライを行い、オートスケーリングによって新しいインスタンスが準備されるまでに3分を要します。重要な注文送信、インタラクティブな在庫確認、および内部のバッチ照合がこのサービスを共有しています。

プロセスが応答し続け、可能な限り多くの有用な作業を維持できる過負荷保護ポリシーを設計してください。検出シグナル、並行性およびキューの境界、受付制御(admission control)、リクエストの優先順位付け、グレースフルデグラデーション、リトライ動作、回復、および検証について説明してください。提示された数値は面接用の前提条件であり、特定のプロダクションシステムに関する主張ではありません。

これは、1つのサービスが自身のCPU、メモリ、スレッド、コネクション、およびダウンストリーム呼び出しをどのように保護するかが中心的な判断となるため、バックエンドの質問です。分散レートリミッターはテナントや時間枠をまたいでトラフィックポリシーを強制しますが、この質問は許可されたトラフィックがサービスの現在のキャパシティを依然として上回っている段階から始まります。タイムアウト、リトライ、サーキットブレーカーは個々の依存呼び出しを保護しますが、過負荷制御はサービスがどの新しい作業を受け入れる余裕があるかを判断します。

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

優れた回答は、リクエストレート単体ではなく、ローカルの作業状況から飽和を特定します。1つのリクエストの処理コストが5倍になったり、依存関係の遅延によってコネクションが長く保持されたりすると、固定のリクエスト/秒のしきい値は破綻します。処理中の作業、キューの滞留時間、利用可能なワーカーやコネクションスロット、CPU、メモリプレッシャー、デッドラインの残余時間(slack)こそが、実際に枯渇しつつあるリソースを明らかにします。

次のシグナルは、有界な(境界が設定された)システムです。無制限のキューは過剰な需要をメモリの肥大化と古いリクエストの滞留へと変化させます。候補者はボトルネックとなるリソースで並行作業の上限を定め、キューを小さく保つか排除し、高コストなパースやダウンストリーム呼び出しの前に低コストで拒否する必要があります。目的はすべてのリクエストを受け入れたり生の試行回数を最大化したりすることではなく、有用な成功作業を最大化することです。

面接官はまた、ビジネスを意識したポリシーも求めています。重要な注文送信には予約枠を割り当て、在庫確認には短期間のキャッシュを利用し、バッチ照合は一時停止できます。ただし、1つの大口顧客が予約スロットをすべて使い果たしてしまわないよう、優先度にはテナントごとの公平性が必要です。最後に、設計には閉じた回復ループが必要です。リトライの増幅を抑制し、緩やかなキャパシティ対応としてスケールさせ、受付を段階的に元に戻し、障害が発生する前にデグラデーションパスをテストします。

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

  • どのリソースが最初に飽和するか? CPUの飽和であれば、ローカルの並行性またはコスト制限が適しています。遅い依存関係には、個別のコネクション/並行性バルクヘッドが必要です。メモリの増加やキューの滞留時間には、キューの縮小とより早期の拒否が必要になる場合があります。ボトルネックごとに受付シグナルが変わります。
  • どの操作が必須で、何をデグレードできるか? ここでは注文送信が重要であり、在庫確認はわずかに古いデータを許容でき、バッチ照合は一時停止できます。すべての操作で最新の在庫データを使用することが法的に義務付けられている場合、キャッシュへのフォールバックは無効であり、明示的な失敗とする方が安全です。
  • エンドツーエンドのデッドラインはどれくらいか? キューの待機時間は実行時間と同じデッドラインを消費します。サービスは完了できなくなった作業を破棄し、ダウンストリームにキャンセルを伝播させる必要があります。
  • リクエストのコストは同等か? リクエストあたり1トークンのモデルは、コストが同等の場合にのみ機能します。コストの高いエンドポイントには重み付けされた許可証(permit)や個別のプールが必要になる場合があり、これにより低コストで重要な作業が高コストなバッチジョブの後ろに閉じ込められないようにします。
  • クライアントは安全にリトライできるか? 拒否された読み取りは後でリトライできます。注文の書き込みには冪等性キーと結果の参照が必要です。サービスは「後でリトライ」と「リトライ不可」を区別する必要があり、すべての層でリトライバジェットを共有する必要があります。
  • トラフィックのシフトは利用可能か? 健全なリージョンへのフェイルオーバーで作業を吸収できるのは、ターゲットに検証済みの空きキャパシティがある場合のみです。無計画に過負荷を転送すると、二次障害を引き起こす可能性があります。

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

「私は小さな閉ループを用いてボトルネックを保護します。まず、負荷テストを利用してインスタンスごとの並行性とキューの境界を設定し、ローカルの処理中作業、キューの滞留時間、CPU、メモリ、コネクションプール、および残りのデッドラインを監視します。飽和度が高まったら、高コストな処理の前に新しい作業を拒否し、注文送信用のキャパシティを予約し、各優先度内で公平性を維持し、在庫確認には承認されたキャッシュ応答を提供し、バッチ作業を一時停止します。拒否には明確な一時的過負荷応答を用い、クライアントはバックオフ、ジッター、共有リトライバジェットを伴って冪等な操作のみをリトライします。オートスケーリングはキャパシティを追加しますが、初期防御としては遅すぎます。ヒステリシスと段階的な受付ランプアップによって回復させ、過負荷テスト下で有界なメモリ、有用なスループット、重要処理の成功、公平性、リトライ増幅、および回復を検証します。」

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

測定されたキャパシティエンベロープから始めます。毎秒3,000リクエストという数値は、テストされたリクエストミックス、依存関係のレイテンシ、インスタンス数、および250ミリ秒のp99ターゲットに対してのみ有効です。対応するインスタンスごとの処理中作業、CPU、メモリ、ワーカー使用率、およびダウンストリームのコネクション使用率を記録します。リクエストレートは入力に過ぎず、受付の決定は障害に最も近いリソースに従う必要があります。たとえば、在庫サービスが遅延すると、同じ到着レートでも処理中の呼び出しが増加するため、レートのみに基づくコントローラーでは反応が遅すぎます。

希少なリソースの周囲に独立した並行性制限を設けます。注文ハンドラーには全体的な上限が必要であり、在庫呼び出しとバッチジョブにはより小さなバルクヘッドを割り当てます。許可証(permit)は高コストな作業を割り当てる前に取得され、成功、失敗、タイムアウト、またはキャンセル時に解放されます。リクエストのコストが著しく異なる場合は、重み付けされた許可証または個別のエンドポイントプールを使用します。ハードコードされた制限値は負荷テストから得られる安全な出発点です。適応型制限(adaptive limit)は利用率を向上させる可能性がありますが、安定したフィードバック、ガードレール、迅速なロールバック機能が必要です。

キューイングは明示的かつ有界に保ちます。短いキューは既知のバーストを吸収できますが、その制限は利用可能なメモリではなくデッドラインの残余時間に基づくべきです。キューがいっぱいになった場合や、推定キュー遅延によって完了までの時間が足りなくなる場合は拒否します。無制限のキューはキャパシティを生み出すことはできません。テールレイテンシを増加させ、メモリを保持し、クライアントにすでに待機中のリクエストをリトライさせる原因になります。高コストな作業からなる小さなキューでも滞留している可能性があるため、キューの深さと最も古いアイテムの滞留時間の両方を監視します。

受付制御は低コストかつ一貫して失敗させる必要があります。ゲートウェイでは、契約上のテナントクォータと大まかなトラフィック制限を適用します。各サービスインスタンスでは、ローカルの飽和シグナルを使用してリソースを所有するタスクを保護します。ダウンストリームのマイクロサービスが過負荷になっている場合は、後で破棄される作業を実行する前にアップストリームで拒否します。リクエストの重要度を呼び出しパスに沿って伝播させ、同じ注文がある層で受け入れられ、別の層で作業を消費した後にランダムに拒否されることがないようにします。

明示的なキャパシティ予約を伴う優先度ポリシーを使用します。

クラス過負荷時のアクション理由
注文送信予約された並行性。自身の境界を超えた場合のみ拒否無制限のキャパシティを与えることなくクリティカルパスを保護する
在庫確認有効期限の規約が明確な短期間のキャッシュデータを優先。それ以外は拒否虚偽の回答を作成することなくダウンストリームの作業を削減する
バッチ照合取り込みを一時停止し、後で永続化された進捗から再開作業は重要だが、インタラクティブなデッドラインを必要としない

公平性のない優先順位付けは、小規模なテナントを飢餓状態(スタベーション)に陥らせる可能性があります。クラス内でテナントクォータまたは公平なスケジューリングを適用し、回復用や制御用のトラフィックのために最小限の割り当てを確保します。何十もの優先度レベルを設けることは避けてください。オペレーターはインシデント中にどのリクエストが受け入れられるかを予測できなければなりません。

制限に達した場合は、データベースや依存関係の作業を行う前に応答を返します。HTTP 503は一時的な過負荷を表し、Retry-Afterを含めることができますが、すべてのクライアントが一斉にリトライすることを許可するものではありません。クライアントは、上限付きのエクスポネンシャルバックオフ、ジッター、デッドライン、およびリトライバジェットを使用します。すべての層ではなく、適切な1つの層でのみリトライします。注文作成では冪等性キーを再利用し、あいまいなタイムアウトの後は以前の結果を問い合わせます。呼び出し元のデッドラインが切れたリクエストはキャンセルされ、サーバーが無駄な作業を完了しないようにします。

グレースフルデグラデーションは、単に拒否するのではなくコストを削減します。プロダクトの仕様上許容される場合、在庫確認ではオプションのエンリッチメントをスキップしたり、滞留期限付きのキャッシュを使用したりできます。バッチコンシューマーはメッセージの取得(pull)を停止できます。古い在庫を最新として報告するフォールバックは不誠実です。最新性が必須である場合は、明示的な利用不可の結果を返します。使用されていない緊急パスは必要なときに失敗する可能性が高いため、トラフィックのごく一部でデグラデーションパスを継続的に実行しておきます。

オートスケーリング、リージョン間のスピルオーバー、およびキャパシティの追加は引き続き有用ですが、これらは受付制御の後に動作します。生のリクエスト数に基づくスケーリングは、低コストなトラフィック時にインスタンスを追加し、高コストなトラフィック時に遅れる可能性があります。並行性、キューの滞留時間、またはリソースの飽和度を含めてください。新しいインスタンスは、完全な負荷シェアを受け取る前にコネクションをウォームアップする必要があります。フェイルオーバーには宛先のキャパシティチェックが必要です。どちらのメカニズムも、ローカルの境界設定を不要にするものではありません。

回復には、突入しきい値よりも低い退出(復帰)しきい値を使用します。処理中の作業、キューの滞留時間、および依存関係の健全性が保持期間にわたってそのしきい値を下回り続けた後、受け入れ負荷を段階的に増やします。通常のレイテンシが安定するまで、優先度の予約を維持します。このヒステリシスとランプアップにより、システムがオープン状態と過負荷状態の間で激しく切り替わったり、部分的にしか回復していない依存関係に過剰な負荷をかけたりするのを防ぎます。

公称キャパシティテストを超えてポリシーを検証します。プロダクションのリクエストコストミックスを再現し、在庫サービスを遅延させオートスケーリングを3分間遅らせた状態で、到着数を毎秒3,000から8,000リクエストに増やします。同期したクライアントリトライ、1つの不正なテナント、期限切れのデッドライン、および依存関係の回復を追加します。有界なキューとメモリ、安定したワーカーとコネクションの使用率、低コストな拒否、予約枠内での注文成功、テナントの公平性、誠実なデグラデーション、制御されたリトライ増幅、および通常状態への段階的な復帰を確認します。受け入れられたリクエストやダウンストリームの試行回数とは別に、完了した有用な操作を測定します。

高品質な回答例

「持続可能な毎秒3,000リクエストというのは特定のリクエストミックスに対する負荷テスト結果ですので、まずはその境界におけるリソースを特定します。在庫遅延中は、リクエストレートよりも処理中の呼び出しやコネクション占有率の方が有用な指標となります。サービスに対してテスト済みのインスタンスごとの並行性上限、在庫呼び出し用の小さなバルクヘッド、そして待機時間がリクエストのデッドライン内に収まる小さなキューを設定します。いずれかの境界に達した場合、サービスは高コストな作業の前に拒否します。

トラフィックを注文送信、在庫確認、照合に分類します。注文には予約キャパシティが与えられますが、依然として厳格な上限を設けます。在庫確認は、APIがその鮮度規約を公開している場合にのみ短期間のキャッシュを使用できます。照合は一時停止し、永続化された進捗から再開します。すべてのクラス内でテナントの公平性を強制し、1つの顧客が予約全体を独占できないようにします。また、後続のサービスでランダムにドロップされるリクエストに作業を費やすのを防ぐため、優先度をダウンストリームに伝播させます。

一時的な過負荷に対しては認識可能な503を返し、推定可能な場合はRetry-Afterを返します。クライアント側では上限付きバックオフ、ジッター、デッドライン、および共有リトライバジェットが必要です。リトライを行うのは1つの層のみとし、注文の書き込みには冪等性キーを再利用します。期限切れの呼び出し元はダウンストリームの作業をキャンセルします。

インスタンスの準備に3分かかるため、オートスケーリングはより緩やかなキャパシティループとなります。飽和シグナルに基づいてスケールさせ、新しいインスタンスをウォームアップする一方で、ローカルの受付制御によって既存のフリートを維持します。回復には、より低く設定された復帰しきい値と段階的なランプアップが必要です。

実証手段としては、遅い在庫、遅延するスケーリング、リトライ、混在するリクエストコスト、ノイズの多いテナントが存在する状態での毎秒8,000リクエストの過負荷テストを実施します。有界なメモリとキュー、安定した有用なスループット、約束された注文予約と公平性、低コストな拒否、リトライストームの防止、および在庫復旧後の制御された回復を期待します。」

よくある間違い

  • エラーが消えるまでキューの制限を増やす → 受け入れられたリクエストの待機時間が長くなり、メモリを消費し、期限切れとなり、実行キャパシティを増やすことなくリトライを引き起こす → デッドラインの残余時間に基づいてキューイングを有界にし、早期に拒否する。
  • リクエスト/秒のみから過負荷を検出する → リクエストコストと依存関係のレイテンシは変化するため、同じレートでも安全な場合と致命的な場合がある → ローカルの処理中作業、キューの滞留時間、リソース飽和度、および依存関係プールを使用する。
  • オートスケーリングを第一の防御策とする → 3分の遅延により、キューとリトライが現在のフリートを不安定にする → ローカルの受付境界を維持し、ヘッドルームを回復するためにスケールさせる。
  • クリティカルトラフィックに無制限の優先権を与える → 同じリソースを枯渇させ、回復作業を飢餓状態にする可能性がある → キャパシティを予約しつつ、厳格な上限と公平性を維持する。
  • すべてのマイクロサービスでランダムに負荷を遮断する → 後のランダムな拒否の前にアップストリームの作業が消費され、エンドツーエンドの有用な成功が減少する → 重要度を伝播させ、ボトルネックが判明した時点で可能な限り早期に拒否する。
  • 503を返してすべてのクライアントにリトライさせる → 同期したリトライが過剰な負荷を何倍にも増幅させる → ジッター、デッドライン、単一のリトライ層、およびリトライバジェットを使用する。
  • 明示されていない古いフォールバックデータを返す → 在庫セマンティクスに違反しながら、システムが見かけ上利用可能に見えてしまう → 鮮度規約を公開するか、明示的に失敗させる。
  • すぐに全トラフィックを復旧させる → 回復途中の依存関係が再び過負荷になる → ヒステリシス、制限付きプローブ、および段階的な受付ランプアップを使用する。
  • 受け入れられたトラフィックのみを追跡する → 高い受け入れ率は、タイムアウト、無駄な作業、リトライを覆い隠す可能性がある → 有用な完了数、拒否コスト、デッドラインの浪費、および試行の増幅を測定する。

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

フォローアップ1:分散レートリミッターでこれを解決しないのはなぜですか?

分散レートリミッターは、契約上のクォータ、不正利用の制御、およびリクエストがサービスに到達する前のトラフィックシェーピングに有用です。これ単体では、在庫レイテンシによって許可された各リクエストのコストが増加したことを検知できません。ゲートウェイの制限を維持した上で、リソースオーナー側でローカルの並行性とキューの保護を追加します。リクエストコストが安定しており、サービスにボトルネックが1つしかない場合は、保守的なレート制限がよりシンプルで十分な解決策になる可能性があります。

フォローアップ2:並行性制限はどのように決定しますか?

プロダクションのリクエストミックスを用いた負荷テストから開始し、ヘッドルームを持った状態でレイテンシとリソースのターゲットを満たす最大の並行性を見つけます。依存関係が遅延した状態でもこれを繰り返します。並行性、スループット、時間の定常状態の関係(リトルの法則など)は妥当性の確認(サニティチェック)にはなりますが、バースト的な到着や混在するコストの下でのキャパシティを保証するものではありません。制限は監視専用モードでロールアウトし、カナリア拒否を実施してから強制適用します。コード、インスタンスサイズ、または依存関係が変更されたときに見直します。

フォローアップ3:トラフィックの90%が重要(critical)とマークされている場合はどうなりますか?

その場合、そのラベルは有用な受付決定を下すために機能しなくなります。ビジネスオペレーションから重要度を定義し、それを設定できる権限を認証し、各クラスに上限を設け、測定された割り当て分のみを予約します。クリティカルクラス内では、過負荷によって最もノイズの多いクライアントが恣意的に優遇されないよう、テナントの公平性またはユーザーごとに安定した優先順位を使用します。真にクリティカルな需要が物理的なキャパシティを超える場合、一部のクリティカルな作業であっても失敗させる必要があります。その方法は規約で明記されるべきです。

フォローアップ4:適応型並行性制御(Adaptive Concurrency)によって設計を改善できますか?

静的な上限よりもサービス時間の変化に密従できますが、ノイズの多いレイテンシやフィードバックの遅延によって振動や誤ったシェディングが発生する可能性があります。テスト済みの静的な境界から始めてください。適応型制御を追加する場合は、最小・最大制限、平滑化されたシグナル、ヒステリシス、安定したフォールバック値、および急速な悪化と緩やかな回復をカバーするリプレイテストを必ず伴わせてください。

フォローアップ5:深いコールグラフにおいてロードシェディングはどこで行われるべきですか?

リソースオーナーにはローカルの最終防衛線が必要です。そのサービスが過負荷をシグナルしたら、アップストリーム層はより早期に作業を停止し、呼び出しパスに沿って同じ重要度の決定を維持すべきです。エッジでのみシェディングを行うと正確なダウンストリームの状態が不足し、リーフでのみシェディングを行うとアップストリームの作業が無駄になります。実用的な設計では、大まかなエッジポリシー、ローカル保護、およびアップストリームの呼び出し元が対処できる過負荷シグナルを組み合わせます。

フォローアップ6:ポリシーが機能していることを示すプロダクションメトリクスは何ですか?

操作および優先度ごとの有用な完了数とレイテンシ、受け入れられたリクエスト、キューイングされたリクエスト、デグレードされたリクエスト、拒否されたリクエスト、最も古いキューの滞留時間、処理中の作業、CPU・メモリ・ワーカー・コネクションの飽和度、デッドライン切れの作業、テナントごとの公平性、論理リクエストあたりの物理試行回数、オートスケーリングの準備遅延、過負荷モードで費やした時間を追跡します。クリティカル予約枠の喪失、持続的な過負荷、拒否コストが通常のリクエストコストに近づく現象、または回復ランプアップが何度もフォールバックする状態に対してアラートを設定します。

公開情報ソース

関連する質問