代表的な面接トピック

分散ジョブスケジューラをどのように設計しますか?

システム設計難しい
Offer.cc 編集チーム公開日 更新日

質問

IANA タイムゾーンに対応した単発ジョブと cron ジョブをサポートするマルチテナント分散ジョブスケジューラを設計してください。2,000 万のアクティブなスケジュールを保存し、1 日あたり 2 億回のオカレンスを生成し、毎正時には 50 万回のトリガーが発生する可能性があります。通常のピーク負荷時において、厳密なジョブは p99 で予定時刻から 5 秒以内にキューに到達する必要があり、柔軟なジョブは 5 分間のウィンドウに分散させることができます。配信は At-least-once であり、スケジュールは一時停止、再開、更新、キャンセルが可能です。

プロンプトとスコープ

マルチテナント分散ジョブスケジューラを設計します。IANA タイムゾーンに対応した単発ジョブおよび cron ジョブをサポートし、2,000 万件のアクティブなスケジュールを保存し、1 日あたり 2 億件のオカレンスを作成し、正時には 500,000 件のトリガーが発生する可能性があります。通常のピーク負荷において、厳密なジョブは p99 で予定時刻の 5 秒以内に永続キューに到達する必要があります。正確な時刻を必要としないジョブは、5 分間の柔軟なウィンドウに分散させることができます。

API はスケジュールの作成、一時停止、再開、更新、キャンセルをサポートします。配信は少なくとも 1 回(at-least-once)です。スケジューラは、予定された時刻をオカレンスに変換し、ターゲットキューに配置する責務を持ちます。コンテナの配置、DAG 依存関係、任意のワークフローオーケストレーションは対象外です。実際のタスク完了時間は、5 秒のスケジューリング SLO には含まれません。

この問題は、ミドルからシニアのバックエンド、プラットフォーム、インフラストラクチャ、およびシステムデザインの役割に適しています。2026 年時点の一般的なシステムデザイン資料では、依然としてジョブスケジューラを単独の面接問題として扱っていますが、現在の AWS や Kubernetes のドキュメントでは、柔軟なウィンドウ、重複または欠落した作成、ミスクライ、タイムゾーン、およびリトライに関する実践的な境界が示されています。核心となる課題は cron のパースではありません。1 つの予定時刻を、追跡可能で、リトライ可能で、冪等な実行として確実に実体化することです。

面接官が評価するポイント

最初のシグナルは、ScheduleOccurrence、および Attempt の分離です。スケジュールは将来のタイミングを定義します。オカレンスは特定の 1 つの予定時刻を表します。試行は、そのオカレンスに対する 1 回の配信または処理の試行を表します。next_run_atstatus を持つ単一の行は、リトライ、履歴、更新、および次回のトリガーを混同してしまいます。

2 番目のシグナルは、exactly-once(厳密に 1 回)を安易に約束しないことです。スケジューラはエンキュー後、成功を記録する前にクラッシュする可能性があり、ワーカーは確認応答(ACK)を失う前に副作用を完了する可能性があります。Kubernetes は、CronJob が 2 つの Job を作成したり、Job をまったく作成しなかったりすることがあると明記しており、そのため冪等なジョブを推奨しています。標準的なキューの可視性リースも、重複配信を完全に防ぐことはできません。優れた回答は、at-least-once 配信を選択し、安定したオカレンス ID を使用してビジネス上の副作用を冪等にします。

3 番目のシグナルは、同期されたピークの処理です。1 日の平均は 1 秒あたり約 2,315 オカレンスにすぎませんが、500,000 件の厳密なトリガーを 5 秒以内にキューに配置するには、1 秒あたり約 100,000 件のディスパッチが必要です。平均に基づいたキャパシティ設計は毎正時に破綻します。また、単一のグローバルな時間順ヒープは、キャパシティおよび可用性のホットスポットを生み出します。

最後に、時間と障害の挙動はプロダクトの契約として定義される必要があります。cron が現地の壁掛け時計の時刻を意味するのか固定間隔なのか、夏時間における存在しない時刻や重複する時刻をどう扱うか、10 分間の障害発生時にキャッチアップするのかスキップするのか、実行の重複を許容するのか、そしてオカレンスがキューイングされた後の更新やキャンセルが何を意味するのかを明確にする必要があります。

明確にすべき質問

  • 5 秒は何を測定するものですか? enqueued_at - scheduled_for として定義し、スケジューリングとエンキューのみを対象とします。完了まで必要な場合、キャパシティモデルは根本から変わります。
  • 単発、cron、固定レートのスケジュールはすべて必須ですか? この設計では単発と cron をサポートします。24 時間ごとという指定は、夏時間の変更をまたぐと現地時間の 09:00 とは異なり、同じ計算を使用してはなりません。
  • 欠落よりも重複のほうが許容されますか? この設計では at-least-once 配信を選択し、冪等性によって重複を処理します。反復不可能な外部の副作用には、冪等性キーまたは明示的な残留リスクが必要です。
  • ダウンタイム中に見逃されたオカレンスはどうなりますか? 各スケジュールには misfire_policy、最大許容遅延、およびキャッチアップ上限が必要です。そうでなければ、リカバリ時に突如として数百万件の期限切れオカレンスが作成される可能性があります。
  • 1 つのスケジュールが自身の実行と重複することは許可されますか? デフォルトは ALLOW であり、SKIP_IF_RUNNING も別の選択肢となります。副作用を取り消せない場合、実行中の任意の処理を置き換えることは安全ではありません。
  • 更新とキャンセルの保証はどの程度強力ですか? 古いバージョンの保留中のオカレンスは無効化される必要があります。スケジュールを削除しても、ワーカーがすでに取得または完了した処理を取り消すことはできないため、API は実際の状態を報告する必要があります。
  • ターゲットは何ですか? この設計では、登録されたハンドラのためにオカレンスを永続キューに配置します。サンドボックス化やコンピュート配置によってスケジューリングの問題が曖昧にならないよう、任意のユーザーコード実行は除外します。
  • どのようなテナント分離が必要ですか? スケジュール数、トリガーレート、実行中の同時実行数、およびキャッチアップレートに対して別々のクォータを適用します。正時の 1 つのバッチがすべてのシャードとキューを消費してはなりません。

30秒の回答

「私はスケジュール、オカレンス、試行を分離します。スケジュールには式、IANA タイムゾーン、バージョン、次回のトリガーを保存します。スケジューラノードはタイムバケットとスケジュール ID ハッシュによってシャーディングし、制限されたホライズンのみを実体化します。オカレンス ID はスケジュール ID、バージョン、元の予定時刻から導出され、ユニーク制約によりフェイルオーバー時の重複実体化を防ぎます。オカレンス、次回のトリガー、およびアウトボックスを 1 つのトランザクションでコミットし、リレーが永続キューに at-least-once で書き込みます。ワーカーは更新可能な可視性リースの下で処理を行い、ビジネスハンドラはオカレンス ID で重複を排除します。厳密なジョブにはピークレートに応じたプロビジョニングを行い、柔軟なジョブには決定論的ジッターを使用します。ミスクライ、重複実行、タイムゾーン、キャンセルの挙動は明示的なポリシーとします。50 万件の同時トリガー、各ハンドオフでのクラッシュ、夏時間の境界、キャンセルの競合を用いて検証します。」

ステップバイステップの詳細設計

コンポーネントを図示する前にデータプレーンを計算します。

text
average = 200,000,000 / 86,400 ≈ 2,315 occurrences/s
strict_peak = 500,000 / 5 = 100,000 dispatches/s

厳密なピーク負荷は 1 日の平均の 43 倍以上になります。アクティブなスケジュールとオカレンスがそれぞれ約 1 KB の論理ストレージを使用する場合、スケジュールメタデータは約 20 GB であり、オカレンスは 1 日あたり約 200 GB 増加します。インデックス、レプリカ、キューメッセージ、および履歴の保持によってさらに増加します。これらはパーティショニングとストレージ階層のためのサイジング前提条件であり、実際のフィールドとインデックスは負荷テストが必要です。

コントロールプレーンは、式の検証、テナントの認可、クォータの適用、冪等な作成、および更新のバージョニングを行います。簡略化されたモデルは以下のとおりです。

text
Schedule(schedule_id, tenant_id, expression, time_zone, version,
         next_run_at, state, misfire_policy, max_lateness,
         overlap_policy, flexible_window)

Occurrence(occurrence_id, schedule_id, schedule_version,
           scheduled_for, available_at, state, attempt_count)

Attempt(attempt_id, occurrence_id, lease_token, started_at,
        finished_at, result)

UNIQUE(schedule_id, schedule_version, scheduled_for)

作成 API は呼び出し元の冪等性キーを受け付けます。cron 式とタイムゾーンを検証し、将来のいくつかのトリガーをプレビューすることで、構文的には有効であっても意図しない式を早期に検出します。元の式と IANA ゾーンを保持しつつ、next_run_at は UTC で保存します。毎日の現地時間ジョブは、前回の UTC タイムスタンプに 24 時間を加算するのではなく、ゾーンルールから次回の時刻を導出する必要があります。

タイムゾーンの境界ケースには安定した契約が必要です。この設計では、春の時刻繰り上げ(スプリングフォワード)時の存在しない現地時間をスキップし、秋の時刻繰り下げ(フォールバック)で壁掛け時計の時刻が繰り返される場合は 1 回実行します。タイムゾーンの更新は、新しいスケジュールバージョンにおける将来のオカレンスにのみ影響します。AWS Scheduler も同様のスキップおよび 1 回実行の動作をドキュメント化していますが、ここではスケジューラの普遍的なルールではなくプロダクトの選択肢となります。将来の固定レート型は経過時間を使用し、夏時間によってシフトしません。

スケジュールストアには、shard = hash(schedule_id) mod N を含む (time_bucket, shard, next_run_at) と同等のインデックスがあります。スケジューラノードは複数の論理シャードをリースし、次の数分間などの制限された計画ホライズンをスキャンします。単一のグローバルリーダーはキャパシティとリカバリを制限します。シャードリースは重複スキャンを減らし、データベースのユニーク性と条件付き書き込みが最終的な正確性の境界を提供します。

期限を迎えた各スケジュールに対して、1 つのトランザクションが決定論的な Occurrence を挿入し、現在のスケジュールバージョンから next_run_at を条件付きで進め、available_at を持つディスパッチアウトボックス行を書き込みます。リースの移行中に 2 つのノードが重複した場合、ユニーク性により 1 つの (schedule_id, version, scheduled_for) オカレンスのみが残ります。条件付き更新により、古いノードが次回のトリガーを過去に戻すのを防ぎます。

無限の cron 履歴を事前に生成しないでください。計画ホライズンが短すぎると、ストレージのジッターが 5 秒の SLO に直接影響します。長すぎると、更新やキャンセルによって古いバージョンのオカレンスの大規模なセットが無効化されることになります。スケジューラのフェイルオーバーとエンキューのバジェットをカバーするホライズンを選択し、測定された遅延から調整します。単発スケジュールは終端状態になります。定期的なスケジュールは次回トリガーのカーソルのみを保持し、古いオカレンスは階層化された履歴に移動します。

available_at の近くで、ディスパッチリレーがアウトボックスイベントをテナントと優先度でパーティショニングされた永続キューに書き込みます。キューがメッセージを受け入れた後、データベースの確認応答(ACK)の前にリレーがクラッシュした場合、同じ occurrence_id を再度送信します。その重複は意図的なものです。エンキュー前に送信済みとマークすると処理が欠落する可能性があり、マーク前にエンキューすると重複する可能性があります。トランザクショナルアウトボックスにより、このトレードオフを明示的で冪等にリトライ可能な at-least-once パスにします。

毎正時の 50 万件のジョブを処理するには、追加のポーリングスレッド以上のものが必要です。各タイムバケットを十分な数のスケジュール ID ハッシュシャードに分割します。負荷テストにより、1 つのシャードが実際のトランザクション、インデックス、キュー書き込みを伴って 1 秒あたり Q 件のディスパッチを安全に実行できることが示された場合、厳密なプレーンには少なくとも ceil(100,000 / Q) 個の同時利用可能なシャードに加えて障害用のヘッドルームが必要です。キューにはテナント加重公平性とレートクォータが適用されるため、1 つのテナントが厳密レーンを独占することはできません。

5 分のウィンドウを持つジョブには、決定論的ジッター(available_at = scheduled_for + hash(occurrence_id) mod 300s)を使用します。同じオカレンスはリトライやフェイルオーバー後も同じ時刻を取得するため、相関するトリガーを分散させながら動作を再現可能にします。厳密なジョブは元の時刻と予約されたキャパシティを維持します。Amazon の Builders' Library でも同様に、相関障害を減らす方法として定期的なメンテナンスジョブにジッターを付与することを挙げています。

ワーカーは時間制限のある可視性リースの下でオカレンスを受け取り、長時間の処理ではハートビートによってそれを更新します。リースの期限が切れるとメッセージは再び可視化され、ウィンドウ内であっても重複配信を排除することはできません。したがって、ステートマシンはリースを exactly-once として扱うことはできません。

text
SCHEDULED -> ENQUEUED -> RUNNING -> SUCCEEDED
                         |   |
                         |   +-> RETRY_WAIT -> ENQUEUED
                         +------> DEAD

Side states: MISSED, CANCELED, SKIPPED_OVERLAP

各クレームは新しい lease_token または増加する試行ジェネレーションを取得します。完了時の更新は現在のトークンと一致する必要があるため、タイムアウトしたワーカーが新しい試行の状態を上書きすることはできません。このフェンシングはスケジューラの状態を保護するものであり、古いワーカーによってすでに実行された外部の副作用を保護するものではありません。ビジネス上の冪等性は、副作用と同じトランザクション内のユニークキーとして、またはダウンストリーム API の冪等性キーとして occurrence_id を使用します。外部ターゲットに冪等性がなくクエリもできない場合、重複リスクは残ります。

エラークラスごとにリトライします。永続的なパラメータエラーは DEAD に送られます。ネットワーク障害、スロットリング、一時的な 5xx 応答は、最大試行回数、オカレンスのデッドライン、テナントバジェットによって制限されたフルジッター付き指数バックオフを使用します。キューの DLQ は使い果たされたオカレンスを保持します。手動での再実行は同じ occurrence_id を保持します。新しい ID を割り当てると重複排除がバイパスされてしまいます。

コントロールプレーンのリカバリには明示的なミスクライポリシーが必要です。SKIP は最大遅延を超えたオカレンスを MISSED としてマークします。FIRE_ONCE は見逃された最後のオカレンスのみを発行します。CATCH_UP はテナントのキャッチアップレートに従い、最大で K を発行します。Kubernetes の startingDeadlineSeconds や見逃しスケジュールの制限も同じ決定を示しています。無制限のリカバリは必要な処理を失うか、リカバリストームを引き起こします。

SKIP_IF_RUNNING は、新しいオカレンスを SKIPPED_OVERLAP とマークする前に、同じスケジュールの現在の RUNNING オカレンスをアトミックにチェックします。アトミックな状態遷移がない場合、2 つのオカレンスが共に先行処理が存在しないと認識してしまう可能性があります。キャンセルまたは更新を行うとスケジュールバージョンがインクリメントされ、未取得の古いバージョンのオカレンスが無効化されます。ワーカーは副作用を実行する前にバージョンとキャンセルマーカーを再確認します。開始済みの処理には協調的なキャンセルが必要であり、完了した外部の副作用をスケジューラがロールバックすることはできません。

オブザーバビリティは保証に沿って設定します。schedule_lag = enqueued_at - scheduled_for の p50/p95/p99、タイムバケットごとの保留行数、実体化の遅延、ユニーク性コンフリクト、アウトボックスのバックログ、キューの滞留時間、重複クレーム、リトライと DLQ、MISSED および重複スキップ、テナントスロットリング、クロックオフセット、各オカレンス状態で費やされた時間などを監視します。API の可用性は、期待されるオカレンスが時間どおりにキューに到達したことを証明するものではありません。

受け入れテストでは、まず正時に 50 万件の厳密なオカレンスを注入し、5 秒の p99 とテナントの公平性を確認します。次に、挿入前、トランザクションコミット後、キュー送信後にスケジューラをクラッシュさせ、サイレントな欠落がなく、同一のオカレンス ID で重複排除されることをアサートします。また、副作用完了後にワーカーが ACK を喪失するケース、リースの期限切れ、10 分間のコントロールプレーン障害後の 3 つのミスクライポリシーすべて、夏時間の両方の遷移、更新とキャンセルの競合、長時間実行の重複、および 1 つのテナントによるキャッチアップストームを網羅します。

優れた回答例

「私は SLO を、実行時間を除いた、予定時刻から永続キューの受け入れまでの時間として定義します。1 日あたり 2 億回のトリガーは平均で毎秒約 2,315 回ですが、正時に 5 秒以内で発生する 50 万件のトリガーを処理するには、毎秒 10 万件に対応できるよう厳密プレーンをサイジングする必要があります。柔軟な処理は 5 分間にわたって決定論的に分散させることができます。

モデルはスケジュール、オカレンス、試行を分離します。スケジュールには cron 式、IANA ゾーン、バージョン、および next_run_at を保存します。オカレンス ID はスケジュール ID、バージョン、scheduled_for から導出され、ユニーク制約を持ちます。スケジューラノードはタイムバケットとスケジュール ID ハッシュによってシャーディングし、スキャンのためにシャードをリースします。トランザクションがオカレンスを挿入し、次回のトリガーを進め、ディスパッチアウトボックスを書き込むため、フェイルオーバーによって処理がサイレントに欠落することはなく、繰り返されるリレーは同一のオカレンス ID のみを送信します。

キューとワーカーは at-least-once セマンティクスを使用します。ワーカーは更新可能な可視性リースを持ち、完了処理は現在のリーストークンと一致しなければなりません。ビジネスハンドラは、データベースのユニークキーまたはダウンストリームの冪等性キーとしてオカレンス ID を使用します。これにより、リースの期限切れによって試行が増加してもビジネス上の副作用は増加しません。冪等性のない外部ターゲットには明示的な重複リスクが残ります。

ミスクライ、重複実行、タイムゾーン、およびキャンセルの挙動は API ポリシーです。ダウンタイムの後、スケジュールはスキップ、1 回トリガー、または最大 K 回のキャッチアップが可能です。存在しない cron 時刻はスキップされ、繰り返される壁掛け時計の時刻は 1 回実行されます。更新によってバージョンがインクリメントされ、ワーカーは副作用の前に再確認します。正時のピーク、あらゆるクラッシュウィンドウ、重複メッセージ、夏時間の境界、キャンセルの競合をテストしつつ、スケジュール遅延、期日超過バックログ、重複クレーム、見逃されたオカレンス、DLQ、テナントスロットリングを監視します。」

よくある間違い

  • スケジュール、オカレンス、試行を 1 つの行にまとめる → リトライによって履歴が上書きされ、更新時に古いオカレンスを特定できなくなる → ScheduleOccurrenceAttempt を分離する。
  • 1 秒あたり 2,315 回という 1 日の平均値でサイジングする → 正時の 50 万件のトリガーで深刻な遅延が発生する → 厳密なピークでシャードの負荷テストを行い、柔軟な処理を決定論的に分散させる。
  • インメモリの最小ヒープを持つ単一のグローバルリーダーを使用する → リーダーがキャパシティとリカバリのボトルネックになり、再起動時にすべての将来の処理を再構築することになる → 永続的な時間インデックス、論理シャード、および制限されたホライズンを使用する。
  • キューに書き込む前に送信済みとマークする → キューの障害によりオカレンスがサイレントに欠落する → トランザクショナルにアウトボックスへ書き込み、at-least-once でリレーする。
  • ロックと可視性タイムアウトが exactly-once を保証すると主張する → タイムアウトと ACK の喪失により試行が依然として重複する → 安定したオカレンス ID、フェンシングトークン、およびビジネス冪等性を組み合わせる。
  • リカバリ後に見逃された過去のすべてのオカレンスを再実行する → 数百万件の期限切れタスクが二次障害を引き起こす → 最大許容遅延、キャッチアップ上限、およびテナントレートを設定する。
  • 現地の毎日の cron に対して UTC で 24 時間を加算する → 夏時間をまたぐと壁掛け時計の時刻がずれる → IANA ゾーンを保持し、次回の現地オカレンスを計算する。
  • 一時停止やキャンセルのために行を削除する → 実体化またはキューイングされた処理が実行されてしまう可能性があり、監査履歴が消失する → バージョンによって無効化し、副作用の前に状態を再確認する。
  • すべてのテナントを 1 つの FIFO キューに配置する → 正時の大規模なバッチが他のテナントをブロックする → テナントを考慮した公平なスケジューリングを使用し、トリガーと同時実行のクォータを分離する。
  • API の成功のみを監視する → スケジュールは正常に作成されても時間どおりにトリガーされない可能性がある → スケジュール遅延、期日超過バックログ、見逃されたオカレンス、および状態の照合を追跡する。

フォローアップ

フォローアップ 1: キュー送信後、アウトボックスの更新前にスケジューラがクラッシュした場合はどうなりますか?

リレーは同じ occurrence_id を再度送信します。キューのコンシューマとビジネスターゲットはその ID で重複を排除し、アウトボックスの確認応答は条件付きで行われます。データベースとキューは 1 つのトランザクションを共有しないため、欠落よりも重複を選択します。トランザクションログによってウィンドウを狭めることはできますが、外部の副作用には依然として冪等性が必要です。

フォローアップ 2: アクティブ - アクティブ構成のリージョンをどのようにサポートしますか?

各スケジュールに 1 つのホームリージョンと増加するジェネレーションを割り当てます。現在のジェネレーションを保持しているリージョンのみがオカレンスを実体化でき、実行キューはターゲットリージョンにルーティングされる場合があります。フェイルオーバーは、新しいリージョンが永続カーソルから再開する前にジェネレーションを進めます。グローバルに一意なオカレンスキーまたは集約層が最終的な重複防止の境界として残ります。リージョンをまたぐ書き込みが不要な場合は、リージョンフェイルオーバーを備えたシングルライターのコントロールプレーンのほうがシンプルです。

フォローアップ 3: 秋の時刻繰り下げ時、現地の 01:30 が 2 回発生します。何回実行されるべきですか?

普遍的な正解はありません。これはスケジュール契約の一部です。この設計では、現地のカレンダー日付、壁掛け時計の時刻、ゾーンを用いて 2 つの UTC 候補を 1 つにまとめ、1 回実行します。物理的な両方の瞬間を必要とする財務照合などの場合は、異なる scheduled_for 値を持つ 2 つのオカレンスを選択することになります。API は保存前に将来の実行をプレビューできるようにすべきです。

フォローアップ 4: ジョブの実行に 2 時間かかりますが、1 時間ごとに繰り返されます。何が起こりますか?

ALLOW はそれらを並行して実行します。SKIP_IF_RUNNING は新しいオカレンスをアトミックに SKIPPED_OVERLAP とマークします。プロダクトとして待機が必要な場合は、QUEUE_ONE を追加し、保留中のオカレンスを最大 1 つ保持します。古い実行を置き換えることが安全なのは、すでに副作用が存在している可能性があるため、ハンドラが協調的キャンセルをサポートしている場合のみです。

フォローアップ 5: スケジューラが 1 日間ダウンしました。ターゲットに負荷をかけすぎずにどのようにリカバリしますか?

スケジュールごとに SKIPFIRE_ONCE、または制限付きの CATCH_UP を適用し、テナントとターゲットのキャパシティによってスロットリングします。カーソルを現在の時刻で置き換えるのではなく、永続化された next_run_at からスキャンを再開します。最も古い保留時刻と推定ドレイン時間を追跡し、厳密な新規処理をキャッチアップ用のキャパシティから分離します。

フォローアップ 6: サイレントな欠落がなかったことをどのように証明しますか?

オフラインの照合プロセスが、スケジュールバージョンと時間ルールから期待されるオカレンスセットを独立して再計算し、それを Occurrence のユニークキーセットと比較します。期待される各アイテムは、成功、失敗、キャンセル、見逃し、または不在のいずれかでなければなりません。オンラインメトリクスは遅延を検出し、照合プロセスはオカレンスが一度も作成されなかったためにシステムがエラーを報告しなかったギャップを検出します。

公開情報ソース

関連する質問

関連面接ツール

システム設計の回答には「回答する」を使用

まず要件を明確にし、スケール、アーキテクチャ、コンポーネント選定、トレードオフの順に進めます。

ツールを見る