問題とスコープ
あるSaaSプラットフォームが invoice.paid、order.shipped、user.disabled などのイベントを発行します。顧客はHTTPSエンドポイントを登録し、各エンドポイントを選択したイベントタイプにサブスクライブします。コミットされたビジネスイベントから顧客のHTTPレスポンスまでのアウトバウンドプラットフォームを設計してください。エンドポイント管理、配信履歴、シークレットのローテーション、手動リプレイはスコープ内です。リクエストを受理した後の顧客側の内部処理は、プラットフォームの制御範囲外となります。
面接では以下の前提条件を使用します。
- 製品は1日あたり5,000万件のビジネスイベントを生成します。各イベントは平均4つのエンドポイントに一致し、1日あたり2億件の論理配信が発生します。
- 平均負荷は毎秒約579イベント、2,315件の新規配信です。10倍のピーク時は毎秒約5,787イベント、23,148件の新規配信になります。
- リトライによって試行が10%増加する場合、ピーク時のディスパッチパスは毎秒約25,463件の試行を維持する必要があります。
- 通常のピーク時において、対象となる新規配信の99%が10秒以内に最初の試行を開始する必要があります。リトライの期限は24時間で、配信履歴は30日間クエリ可能です。
- 1.5 KBの不変イベントペイロード、300バイトの配信メタデータ、試行あたり250バイト、配信あたり1.1回の試行を前提とすると、論理ストレージは1日あたり約190 GB、30日間で5.7 TBになります。これにはインデックス、レプリカ、圧縮、オブジェクトストレージのオーバーヘッドは含まれません。
これらは規模を見積もるためのインプットであり、業界標準ベンチマークではありません。課金、任意のペイロード変換、インバウンドWebhook、顧客側のアプリケーション設計はスコープ外です。主要なコントラクトはat-least-once配信です。重複や順不同の到着は起こり得ますが、約束された保持およびリトライの境界内におけるサイレントな消失は、検出および復旧可能でなければなりません。
面接官が評価するポイント
第1の評価シグナルは、候補者が識別子と保証を正確に定義しているかどうかです。1つの event_id はコミットされたビジネスファクトを表します。1つの delivery_id はそのイベントが1つのエンドポイントに送られることを表し、自動リトライや手動の再配信を通じても安定して同一に保たれます。(event_id, endpoint_id) に対する一意性制約により、ファンアウトリプレイが2つ目の論理配信を作成するのを防ぎます。ただし、ワーカーがレスポンスを見失った場合に同じHTTPリクエストが顧客に2回到達することを防ぐものではありません。
第2の評価シグナルはトランザクション境界です。ビジネスデータベースのコミット直後に直接パブリッシュすると、デュアルライト(二重書き込み)のギャップが生じます。トランザクションがコミットされた後、パブリッシュ前にプロセスがクラッシュする可能性があります。トランザクショナルアウトボックス、Change Data Capture(CDC)ストリーム、または同等の永続的なイベントソースによってそのギャップを解消します。その後、ファンアウトによってディスパッチ前に配信状態が具現化されるため、システムはどのエンドポイントが選択され、どの試行が実行され、何が未完了であるかを把握できます。
第3の評価シグナルは障害の分離です。遅いエンドポイントがすべての接続を占有してはなりません。数千の失敗エンドポイントを抱えるテナントが、健全なテナントのリトライバジェットを消費してはなりません。キューのパーティション分割だけでは公平性は担保できません。設計には、エンドポイントごとの制限された並行性、テナントクォータ、リトライスケジューリング、およびサーキットブレーカーまたは一時停止状態が必要です。
第4の評価シグナルは双方向のセキュリティです。受信側には、正確なバイト列に対するHMAC署名と認証された配信メタデータが必要です。送信側も顧客が制御するURLをSSRFの攻撃対象領域として扱い、スキームと送信先を制限し、DNS結果を再検証し、リダイレクトを制約し、エグレスを分離する必要があります。優れた回答は、これらの制御をシークレットのローテーション、リプレイ保護、監査可能性、およびインシデント対応と結びつけて説明します。
回答前に確認すべき質問
- 成功の定義は何か? この設計では、任意の
2xxレスポンスをエンドポイントの受理とみなします。3xx、タイムアウト、接続エラー、408、429、5xxは成功ではありません。受理されたことは、顧客側のその後のビジネス処理が成功したことを証明するものではありません。 - どのイベントとペイロードバージョンが保証されるか? 各イベントタイプには、文書化されたスキーマとバージョニングポリシーが必要です。リトライでは同一の不変ペイロードバイトが送信され、現在のデータベース状態から古いイベントを再構築してはなりません。
- どのような順序性が求められるか? デフォルトの配信は順不同です。顧客が1つの集約に対して順序を必要とする場合は、
object_idと単調増加するobject_versionを含めるか、ヘッドオブラインブロッキングを許容するオプトインの順序キーを提供します。グローバルな順序付けは必須でもなく、実現コストも見合いません。 - プラットフォームはどのくらいの期間リトライやリプレイを行えるか? ここでは、自動リトライは24時間後に停止し、ログは30日間保持されます。自動ウィンドウ経過後のリプレイは、元のイベントと配信IDを使用しますが、新しい試行レコードを作成します。
- エンドポイントは任意の宛先を指定できるか? この製品はパブリックHTTPSエンドポイントのみを受け付けます。プライベート、ループバック、リンクローカル、マルチキャスト、予約済み、およびクラウドメタデータの宛先は、IPv4およびIPv6において拒否されます。
- どのようなデータがプラットフォーム外に出る可能性があるか? サブスクリプションの認可とペイロードの最小化はファンアウトの一部です。機密フィールドは省略されるか、インテグレーションが認証されたAPIを介して取得できる場合はリソース参照によって表現されます。
30秒での回答
「私は各ビジネスイベントをアウトボックス経由でコミットし、永続的なイベントログにパブリッシュして、ファンアウトサービスでアクティブなサブスクリプションを解決します。キューに処理を投入する前に、(event_id, endpoint_id) あたり1つの配信行を作成します。ワーカーはリースを用いて試行を要求(クレーム)し、エンドポイント単位およびテナント単位の制限を適用し、不変のバイト列に安定した配信IDと現在の試行タイムスタンプを付加して署名し、HTTPS経由で送信します。2xx で配信は完了し、リトライ可能な失敗には24時間の期限までフルジッター付きの指数バックオフを適用し、恒久的な失敗は停止します。送信後のタイムアウトは結果が曖昧であるため、コントラクトはat-least-onceとし、顧客側で配信IDによる重複排除を行います。さらに、署名付きシークレットローテーション、SSRFセーフなURL検証、配信ログとリプレイ、エンドポイントサーキットブレーカー、およびアウトボックス、ファンアウト、キューのギャップを検出するリコンシリエーションを追加します。」
ステップごとの詳細解説
イベントの作成から始めます。ビジネス状態を変更するのと同じローカルトランザクション内で、event_id、テナント、タイプ、スキーマバージョン、オブジェクトの識別子とバージョン、発生時刻、および正規化されたシリアライズ済みペイロードバイトへの参照を含むアウトボックス行を書き込みます。アウトボックスリレーがパーティション化された永続ログにパブリッシュします。リレーは重複パブリッシュする可能性があるため、下流のコンシューマーは event_id で重複排除します。ビジネスデータが複数のシステムにまたがる場合、信頼できるプロデューサーがイベントを所有します。Webhookサービスは変更可能なテーブルをポーリングしてファクトを再構築すべきではありません。
コントロールプレーンのAPIは小さく明示的なものにできます。
POST /v1/webhook-endpoints
PATCH /v1/webhook-endpoints/{endpoint_id}
POST /v1/webhook-endpoints/{endpoint_id}/rotate-secret
GET /v1/webhook-deliveries?endpoint_id=&status=&cursor=
POST /v1/webhook-deliveries/{delivery_id}/replayエンドポイントの作成では、テナントを認証し、HTTPS URLとイベントタイプフィルターを受け入れ、宛先を検証し、署名シークレットを一度だけ返します。チャレンジ配信によって所有権を証明できますが、チャレンジの成功が将来のDNS応答の安全性を保証するものではありません。シークレットのローテーションでは、一定のオーバーラップ期間中に現在および以前のバージョンを有効に保ち、どの署名バージョンが使用されたかを公開します。URLの更新は監査可能な設定バージョンを作成し、過去の試行を勝手に書き換えることはありません。
4つの永続レコードを使用します。
Endpoint(endpoint_id, tenant_id, url, status, event_types,
current_secret_version, previous_secret_version, config_version)
Event(event_id, tenant_id, type, schema_version, object_id,
object_version, occurred_at, payload_ref, payload_hash)
Delivery(delivery_id, event_id, endpoint_id, endpoint_config_version,
status, attempt_count, next_attempt_at, expires_at, lease_version)
Attempt(attempt_id, delivery_id, attempt_number, started_at, finished_at,
http_status, latency_ms, error_class, response_digest)ファンアウトサービスはイベントを読み取り、そのテナントとイベントタイプに対して認可されたアクティブなサブスクリプションをロードし、制限付きのバッチで配信行を挿入します。データベースは (event_id, endpoint_id) の一意性を強制します。行が存在することを確認した後にのみ、delivery_id 値をエンキューします。エンキューに失敗した場合、スイーパーがアクティブなキュー処理のない期限切れの PENDING 行を検出します。ファンアウトプロセスが途中でクラッシュした場合、リプレイによってルックアップが繰り返され、一意性制約により欠落している行のみが入力されます。保存されたサブスクリプションスナップショットまたはエンドポイント設定バージョンにより、後の監査で説明が可能になります。
新しい処理とリトライ処理が1つの無制限のFIFOを共有すべきではありません。スケジューラは期限を迎えた行をタイムバケットに選択し、重み付けされたテナントの公平性、エンドポイントのトークンバケット、および制限されたエンドポイント並行性を適用します。低速なエンドポイントが自身のインフライト上限に達しても、健全なエンドポイントは処理を継続します。失敗が繰り返されるとサーキットが開き、以降の試行は先延ばしにされますが、配信は可視状態を維持し、プローブ試行の対象となります。テナント全体のクォータにより、何百万もの不正なエンドポイントがフリートを消費するのを防ぎます。リトライ処理はアイドル状態のキャパシティを利用できますが、最も古い新規配信の滞留時間がSLOを超過してはなりません。
ワーカーはリースと増加する lease_version を使用してアトミックに配信を要求します。保存されたペイロードバイトを読み取り、試行タイムスタンプを作成し、HMAC-SHA256を使用して delivery_id.timestamp.payload_bytes などの正規化された文字列に署名します。署名された正確なバイト列と、イベントタイプ、配信ID、イベント発生時刻、試行時刻、スキーマバージョン、署名バージョンを含むヘッダーを送信します。コンシューマーは定数時間比較を使用して生バイトを検証し、タイムスタンプの許容範囲をチェックし、安定した配信IDで重複排除します。各リトライでは、配信IDとイベントバイトを保持したまま、新しい試行タイムスタンプと署名が付与されます。
レスポンスポリシーは決定論的である必要があります。任意の 2xx は配信を SUCCEEDED としてマークします。3xx に自動的に追従してはいけません。408、429、5xx、接続リセット、タイムアウトはリトライ可能として扱い、429 または 503 の制限付き Retry-After を尊重します。その他の 4xx レスポンスのほとんどは、その設定において恒久的な失敗となります。TLSおよびDNSの障害は最初はリトライ可能として始まりますが、エンドポイントのサーキットを迅速に開きます。フルジッター付きの指数バックオフ、最大遅延、最大試行回数、および24時間の絶対期限を使用します。リクエストがワーカーから離れた後のタイムアウトは結果が不明です。リトライによって顧客側の処理が重複する可能性があるため、プラットフォームがexactly-onceを保証してはなりません。
手動リプレイは、同じ論理 Delivery に対して別の Attempt を作成します。新しいビジネスイベントを発行するわけではありません。オペレーターは、元の不変ペイロード、使用されたエンドポイント設定、すべてのレスポンスクラス、および自動リトライがまだアクティブかどうかを確認できます。リプレイ前に認可が再チェックされ、エンドポイントの現在アクティブなシークレットが新しい試行に署名できます。製品が過去のバイト単位での認証を保証する場合は、定義されたキー保持ポリシーの下で必要な過去のキーを保持する必要があります。
顧客指定のURLには専用のエグレス境界が必要です。十分にテストされた単一のURL実装でパースし、HTTPSと承認されたポートを許可し、URL内の認証情報を拒否し、すべてのAおよびAAAA応答を解決し、プライベート、ループバック、リンクローカル、予約済み、マルチキャスト、メタデータの範囲をブロックします。接続時に再度検証するか、検証済みアドレスを固定(ピン留め)して、DNSリバインディングおよびTime-of-Check/Time-of-Useのギャップを低減します。リダイレクトを無効にするか、シークレットを転送せずにすべてのホップに対して完全なポリシーを再実行します。ワーカーをコントロールプレーンや内部サービスに到達できないエグレスネットワークに配置し、接続時間、合計リクエスト時間、レスポンスバイト数、解凍を制限します。
キャパシティはイベント数だけでなく、ファンアウトにも依存します。5,000万件のイベントに4つのサブスクリプションを掛けると、1日あたり2億件の配信が発生します。それぞれ1.1回の試行が行われる場合、試行履歴は1日あたり2億2,000万行になります。想定される生サイズに基づくと、50M × 1.5 KB = 75 GB、200M × 300 B = 60 GB、220M × 250 B = 55 GB となり、合計で1日あたり約190 GBになります。現在の未処理状態のインデックスを小さく保ち、時間とテナントハッシュで履歴をパーティション分割し、不変ペイロードと古い試行をより安価なストレージにアーカイブします。イベントからファンアウトまでの遅延、最も古い新規およびリトライの経過時間、初回試行レイテンシ、エンドポイントおよびステータスクラスごとの成功率、リトライ増幅率、サーキット状態、テナントスロットリング、リプレイ結果を測定します。
最後に、すべての永続境界をリコンシリエーションします。コミットされたアウトボックス行とパブリッシュされたイベントID、イベントと期待されるサブスクリプションスナップショットおよび配信数、未処理の配信行とスケジューラのリース、終了カウントと試行履歴を比較します。フォールトインジェクションでは、パブリッシュ後にリレーをクラッシュさせる、ファンアウトの途中でクラッシュさせる、キューメッセージを重複させる、リモートエンドポイントがPOSTを処理した後かつローカルの成功書き込みの前にワーカーを強制終了する、同期した 429 レスポンスを返す、特定のテナントのエンドポイントをハングさせるといったテストを行います。受け入れ基準は、単にデモリクエストが成功することではなく、永続カウント、制限されたキュー滞留時間、公平な復旧、および説明可能な重複によって表現されます。
優れた回答例
「私はコミットされた各ビジネスファクトに不変の event_id を付与し、各イベントとエンドポイントのペアに安定した delivery_id を付与します。プロデューサーはビジネストランザクション内でイベントをアウトボックスに書き込みます。リレーが永続ログにパブリッシュし、ファンアウトがキューイングの前に一意の (event_id, endpoint_id) 制約の下で配信を具現化します。これにより、リプレイ時に2つ目の論理配信を作成することなく、欠落した処理を修復できます。
提示されたピーク時では、ファンアウトは毎秒約23,148件の新規配信を作成し、ディスパッチパスはリトライを含めて毎秒約25,463件の試行を計画します。スケジューラは新規処理とリトライ処理を分離し、重み付けされたテナントの公平性、エンドポイントごとの並行性およびトークンバケットを適用し、ワーカーがバージョニングされたリースで試行を要求できるようにします。したがって、1つの遅い顧客はその顧客自身の割り当てのみを消費します。度重なる失敗によりエンドポイントのサーキットが開きますが、プローブと24時間のリトライ期限は可視のまま維持されます。
すべての試行において、delivery_id、試行タイムスタンプ、および正確な不変ペイロードバイトがHMAC-SHA256で署名されます。顧客は定数時間比較で署名を検証し、古いタイムスタンプを拒否し、安定した配信IDで重複排除を行います。2xx は成功とみなされます。408、429、5xx、ネットワークエラー、タイムアウトはフルジッター付きの指数バックオフでリトライされ、その他の 4xx レスポンスのほとんどは停止します。顧客がリクエストを処理した後、成功の記録前にワーカーがクラッシュする可能性があるため、私はat-least-onceを保証し、コンシューマーが冪等である必要があることを明記します。
コントロールプレーンは、エンドポイントおよびサブスクリプション管理、制限付きオーバーラップを伴うシークレットローテーション、配信ログ、リプレイを提供します。エンドポイントURLは、分離されたエグレスネットワーク内でHTTPS、IP範囲、DNSリバインディング、リダイレクト、タイムアウト、レスポンスサイズの制御を通過します。私はすべての永続境界をリコンシリエーションし、重複パブリッシュ、部分ファンアウト、失われた確認応答、大量の 429 レスポンス、およびハングしたエンドポイントを抱えるテナントを注入することで設計を検証します。合格基準は、初回試行SLO、説明のつかない配信ギャップがないこと、制限されたリトライ増幅、および健全なテナントに対する公平な復旧です。」
よくある間違い
- ビジネスデータのコミット後にパブリッシュする → 2つの操作の合間にクラッシュするとWebhookイベントが失われます → トランザクショナルアウトボックスに書き込むか、同等の永続的な変更ストリームを消費します。
- リトライごとに新しい配信IDを作成する → 受信側で1つの論理配信を重複排除できなくなります →
delivery_idを安定して維持し、個別の試行レコードを作成します。 - exactly-onceなHTTP配信を約束する → リモート処理後にレスポンスが失われると、送信側は結果を知ることができなくなります → at-least-once配信を提供し、コンシューマー側での冪等な処理を要求します。
- リトライ時にペイロードを再構築する → 現在のデータベース状態によって過去のイベントが変更され、古い署名が無効になります → 正規化された不変のイベントバイトとそのスキーマバージョンを保存します。
- すべてのエンドポイントに単一のFIFOを使用する → 低速な宛先が接続を占有し、健全な顧客の配信を遅延させます → エンドポイントごとの処理を制限し、テナントの公平性を考慮してスケジュールします。
- 2xx以外のすべてを直ちにリトライする → 恒久的な失敗によってリソースが無駄になり、同期したリトライがインシデントを増幅させます → エラーを分類し、フルジッター付きの指数バックオフと期限を使用します。
- 顧客のURLからのリダイレクトに追従する → パブリックURLによってワーカーが内部サービスにリダイレクトされる可能性があります → リダイレクトを無効にするか、分離されたエグレスですべてのホップを完全に再検証します。
- パースされたJSONに署名する → 再シリアライズによってバイト列が変化し、有効な署名が失敗します → 正確な生の本文と、認証されたIDおよびタイムスタンプのメタデータに対して署名および検証を行います。
- ダッシュボードからのリプレイを新しいイベントとして扱う → 下流の顧客が新しい識別子の下でビジネスファクトを二重適用する可能性があります → 既存の配信をリプレイし、新しい試行を記録します。
フォローアップの質問と回答
フォローアップ1:プラットフォームがexactly-once配信を保証できないのはなぜですか?
エンドポイントが処理をコミットして 200 を返したものの、ワーカーがレスポンスを読み取る前に接続が切断されたとします。リトライすると顧客の副作用が重複する可能性があり、リトライしないと届かなかったイベントが失われる可能性があります。送信側は、任意の顧客サーバーとの間にアトミックなトランザクションを持っていません。安定した配信ID、受信側の冪等性、およびリコンシリエーションによってat-least-once配信を管理可能にしますが、2つのデータベースとネットワークを1つのexactly-onceコミットに変えることはできません。
フォローアップ2:1つの壊れたエンドポイントが他のすべてのエンドポイントを遅延させるのをどのように防ぎますか?
各エンドポイントに小さなインフライト制限とトークンバケットを与え、重み付けされた公平性でテナント間にわたってスケジュールします。タイムアウト時は、ワーカー内でスリープするのではなく、リースを解放して遅延リトライをスケジュールします。連続して失敗するとサーキットが開き、健全なエンドポイントがフリートを使用している間は制御されたプローブのみが実行されます。エンドポイントとテナント両方のリトライ増幅を追跡します。多数のエンドポイントを持つ攻撃者であっても、テナント全体のバジェット内に収める必要があります。
フォローアップ3:どのような順序保証を提供しますか?
デフォルトは配信順序なしです。occurred_at、object_id、および単調増加する object_version を含めることで、コンシューマーが古い更新を破棄したり、現在の状態を取得したりできるようにします。有料プランで1つのオブジェクトに対する順序付けが必要な場合は、そのサブスクリプションを順序キーでパーティション分割し、キーごとにアクティブなシーケンスを1つだけ許可します。先行するアイテムが失敗するとそのキーの後続アイテムがブロックされるため、グローバルな順序を主張するのではなく、レイテンシと可用性のトレードオフを明示する必要があります。
フォローアップ4:署名シークレットのローテーション中には何が起こりますか?
新しいバージョンを作成し、制限されたオーバーラップ期間中は前のバージョンを保持し、受け入れられるバージョンをヘッダーまたはドキュメントで特定します。オーバーラップ中は、両方のキーからの署名を含めるか、コンシューマーが両方を安全に試行できるようにします。新しい試行にはアクティブなキーが使用され、不変のペイロードバイトと配信IDは変更されません。実行者、時刻、エンドポイント設定バージョン、最終的な破棄を監査します。侵害されたキーの場合は、オーバーラップを設けずに即時破棄し、顧客に可視化されたリプレイガイダンスが必要になる場合があります。
フォローアップ5:ファンアウトが部分的にしか完了しなかった場合、どのように復旧しますか?
ファンアウトを通じて永続イベントをリプレイします。一意の (event_id, endpoint_id) 制約により、完了した挿入はno-opとなり、欠落している配信のみが作成されます。リコンシリエーションでは、イベントに保存されたサブスクリプションスナップショットまたは設定バージョンを、具現化された行と比較します。サブスクリプションのセマンティクスが「イベント発生時の設定」である場合は、そのスナップショットを保持します。修復中に現在のサブスクリプションを使用すると、イベント発生時に権利がなかった宛先にイベントが送信される可能性があります。
フォローアップ6:手動リプレイによって24時間の期限はリセットされるべきですか?
自動リトライとオペレーターによるリプレイは別個のコントラクトです。自動処理は元の期限で停止します。30日間の履歴ウィンドウ内でのリプレイは、認可とエンドポイント状態のチェックを経て初めて新しい試行を作成し、UI上でも手動であることが明確にマークされます。自動スケジュールをサイレントに再アクティブ化すべきではありません。セキュリティイベントや削除イベントの場合、ログが可視のままであっても、ポリシーによってより短いビジネス期限後のリプレイが禁止される場合があります。