代表的な面接トピック

バックエンド面接:制限付きワーカープール(Bounded Worker Pool)をどのように設計しますか?

バックエンド普通
Offer.cc 編集チーム公開日 更新日

質問

バックエンドサービス向けの制限付きワーカープールを設計してください。キューのキャパシティ、受付または拒否、キャンセル、リトライの所有権、結果の関連付け、メトリクス、グレースフルシャットダウンについて説明してください。

お題とスコープ

ジョブを受け付け、最大で固定数を並行実行し、受け付けた作業を暗黙のうちに破棄することなくシャットダウンするバックエンドサービス向けワーカープールを設計してください。キューのキャパシティ、受付または拒否、キャンセル、リトライの所有権、結果の関連付け、メトリクス、グレースフルシャットダウンについて説明してください。

各ジョブは独立しており、ダウンストリームAPIを呼び出す可能性があると想定します。このプールはプロセス内(in-process)コンポーネントです。プロセス障害をまたいでジョブを存続させる必要がある場合、永続キューは別設計の対象となります。

面接官が見ているポイント

面接官は、明確な並行性制限、有界(bounded)なメモリモデル、および過負荷に対するポリシーが示されているかを確認します。また、キャンセルがワーカーに正しく届くか、リトライによって負荷が増幅しないか、シャットダウン時にキュー内・実行中・完了・拒否の各ジョブが適切に区別されているかも評価します。

回答前に確認すべき質問

  • プロセスクラッシュによるタスクの喪失は許容されますか? 許容されない場合、まずはジョブを永続ブローカーに配置します。
  • ジョブは冪等(idempotent)であり、安全にリトライできますか?
  • ダウンストリームのレート制限、平均実行時間、およびテールレイテンシの目標値はどのくらいですか?
  • キューが満杯の場合、投入(submit)は短時間ブロックするべきか、429 を返すべきか、あるいは低優先度の作業を破棄(shed)するべきですか?
  • キャンセルは「開始前の停止」のみを意味しますか、それともジョブが協調してI/Oを中断できますか?

30秒で答えるフレームワーク

「固定のワーカー数と有限のキューに支えられた、制限付きの投入APIを公開します。キャパシティが枯渇した際、受付処理は明確な過負荷結果を返し、コンテキストやキャンセルトークンによって、キュー内のジョブを実行前に離脱させ、実行中のジョブを協調的に停止させます。受け付けられた各ジョブにはIDと最終状態(terminal state)が存在し、リトライは上限設定とジッター(jitter)を伴って単一のレイヤーで管理されます。メトリクスではキューの深さ、滞留時間、アクティブワーカー数、拒否数、実行時間、キャンセル数を対象とします。シャットダウン時は新規受付を停止し、ポリシーに従ってキュー内の作業をキャンセルし、受け付け済みの作業を期限までドレイン(排出)し、完了できなかったものをすべてレポートします。」

ステップごとの詳細解説

ステップ1:ステートマシンの定義

submitted → queued → running → succeeded|failed|cancelled を使用します。キューが満杯の場合は、見えない待機ではなく rejected を返します。クラッシュ後に呼び出し元によるリカバリが必要な場合は、これらの状態をプロセスの外部に永続化する必要があります。

ステップ2:ワーカーとキューの双方を制限する

W 個のワーカーとキューのキャパシティ Q を選択します。メモリは Q 件のジョブペイロードとワーカースタックによって有界に保たれます。リクエストごとに1つのスレッドやgoroutineを作成してはいけません。Pythonの ThreadPoolExecutormax_workers を公開していますが、投入量が無制限である場合はアプリケーションレベルでの制限付き受付ポリシーが依然として必要です。

ステップ3:受付とバックプレッシャーの選択

ノンブロッキングな投入、制限付き待機、または優先度を考慮した拒否を提供します。安定した過負荷コードと Retry-After は、リトライが安全な場合にのみ返します。プロデューサーが満杯のキューで無限に待機するとリクエストパスがデッドロックする可能性があり、無制限のキューは過負荷をレイテンシ増加とメモリ増大に変換してしまいます。

text
submit(job, deadline):
    if stopping or deadline expired: return REJECTED
    if queue.try_push(job): return ACCEPTED(job.id)
    if policy == WAIT and wait_until(deadline) and queue.try_push(job):
        return ACCEPTED(job.id)
    return OVERLOADED

ステップ4:キャンセルを協調型(cooperative)にする

各ジョブにキャンセルコンテキストを渡します。まだ開始されていないキャンセル済みジョブは削除し、実行中のジョブは安全なポイントでキャンセルを確認し、ダウンストリームのクライアントに伝播させる必要があります。プールが任意のスレッドを安全に強制終了することはできないため、中断不可能なI/Oにおいて「キャンセル済み」が何を意味するかを明確に定義してください。

ステップ5:リトライの所有権を単一レイヤーに維持する

リトライのスケジューリングは、プール、ジョブハンドラー、または永続キューのいずれか1つに任せます。試行回数に上限を設け、ジッターを伴う指数バックオフ(exponential backoff)を追加し、エラーをリトライ可能と恒久的なものに分類します。そうしないと、3つのレイヤーでタイムアウトが発生した際に、同一の依存先に対してリトライの嵐(retry storm)を引き起こす可能性があります。

ステップ6:結果とエラーの関連付け

共有の可変結果スロットではなく、ジョブIDまたはfutureを返します。最初のエラー、試行回数、および最終状態を記録します。呼び出し元がポーリングする場合は、結果ストアの保持期間と認可を明示し、呼び出し元がawaitする場合は、切断が作業にどのように影響するかを定義します。

ステップ7:グレースフルシャットダウンの設計

シャットダウン時は、まず受付を停止し、ポリシーに従ってキュー内のジョブをマークし、実行中のジョブを期限(deadline)までドレインさせます。Goのパイプラインガイダンスでは done シグナルを通じてキャンセルを伝播させますが、ここでも同じ原則が適用されます。期限後は、未完了のジョブを再実行用にレポートするか、明示的な規約がある場合にのみ破棄としてマークします。

ステップ8:ボトルネックの計測(計装)

キューの深さと滞留時間、アクティブワーカー数、使用率、受付/拒否/キャンセル数、実行時間、リトライ回数、ダウンストリームエラーを追跡します。CPUだけでなく、継続的なキューの滞留時間や拒否に対してもアラートを設定します。W は、コネクションプール、外部レート制限、またはCPU容量などのダウンストリームのボトルネックからサイズを決定します。

トレードオフと境界線

トレードオフ1:固定ワーカーか動的スケーリングか

固定ワーカーは並行性を予測可能にし、依存関係を保護します。動的スケーリングはスループットを向上させることができますが、厳格なグローバル制限を適用し、すべてのレプリカを考慮に入れる必要があります。そうしないと、各インスタンスが独立してスケールし、依存先を過負荷にしてしまいます。

トレードオフ2:満杯時の拒否か待機か

拒否は呼び出し元に迅速なフィードバックを提供し、レイテンシを維持します。待機は一時的なバーストを平滑化できますが、待機には期限が必要であり、貴重なリクエストスレッドを無制限に保持してはなりません。

トレードオフ3:プロセス内キューか永続キューか

プロセス内プールは低レイテンシでシンプルです。永続ブローカーはリカバリ、リプレイ機能、および運用コストを追加します。受け付けられた作業がデプロイ、クラッシュ、またはマルチインスタンスのルーティングをまたいで存続する必要がある場合は、永続オプションを使用します。

障害訓練と進化計画

訓練1:ダウンストリームの障害

すべてのジョブを意図的に遅延失敗させ、キューの滞留時間、拒否、タイムアウト、上限付きリトライを検証します。依存関係が不健全な間、プールが並行数を増やさないことを確認します。

訓練2:キャンセルストーム

10,000件のジョブを投入し、実行前に半数をキャンセルし、ドレイン中にサービスを停止します。キュー内のキャンセルされたジョブが実行されず、受け付けられて実行中のジョブがレポートされる最終状態に到達することを確認します。

訓練3:レプリカのローリングアップデート

ローリングデプロイ中に2つのバージョンを実行します。各インスタンスがローカル制限を適用していること、および必要に応じて永続キューまたはグローバルリミッターがシステム全体の制限を適用していることを確認します。

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

間違い1:無制限のバッファリング

無制限のキューは、メモリまたはレイテンシが破綻するまで過負荷を覆い隠します。キャパシティと拒否ポリシーを観測可能にしてください。

間違い2:二重リトライ

HTTPクライアントとジョブハンドラーの双方がリトライを行うと、試行回数が乗算されます。リトライの所有者を1つ割り当て、試行メタデータを伝播させてください。

間違い3:キャンセルをスレッドの強制終了として扱うこと

ほとんどのランタイムは、任意の作業を安全に強制終了できません。協調的なチェック、キャンセル可能なI/O、および明確な破棄作業ポリシーを使用してください。

間違い4:受付を停止せずにドレインすること

新しいジョブによってキューがいつまでも空にならない可能性があります。ドレイン期限を待つ前に受付を終了してください。

間違い5:レプリカごとの制限の考慮漏れ

10個のレプリカにそれぞれ20のワーカーを持つプールがある場合、並行呼び出しは200になります。制限がローカルなのか、シャーディングされているのか、それともグローバルに調整されているのかを明示してください。

間違い6:結果保持ポリシーの欠如

futureやポーリングレコードには、有効期限、認可、および障害パスが必要です。そうしないと、受け付けられたジョブが無制限のストレージ消費につながります。

公開情報ソース

関連する質問