Webhook 平台 (Design a Webhook Platform)

一句話定位 + TL;DR(含 35 分鐘心智時鐘)

設計一個 multi-tenant webhook 平台:內部 product services 產生 domain events(invoice.paidpull_request …),客戶註冊 endpoint 並訂閱想要的 event types,平台把每個事件 以 HTTP POST fan-out 投遞 到所有匹配的 endpoint。本質是一個 可靠的非同步投遞引擎:事件不能丟、接收端可能很慢或暫時掛掉、要在 數天內 努力投遞成功。
招牌取捨:投遞保證選 at-least-once 而非 exactly-once——接受「同一事件可能投遞多次(網路超時但對方其實收到了)」、把冪等責任推給客戶,換取「接收端暫時不可用也絕不丟事件」這個對 webhook 平台而言不可妥協的可靠性,並避開 exactly-once 的分散式協調成本。其餘決勝點:queue 解耦 + 緩衝(ingestion 失敗不波及內部服務)、retry(指數退避 + jitter)→ retryable store + scheduler → DLQ + suspend endpoint 的完整失敗階梯、read-heavy cache(每次事件都要查訂閱)、queue partition + per-endpoint 隔離、以及 HMAC 簽章 + endpoint 所有權證明 的雙向信任。
配速(35 min 預算):~5 min 釐清需求 → ~4 min 容量估算 → ~3 min API + ~4 min 資料模型 → ~6 min 高階架構 → ~9 min deep dive(at-least-once 失敗階梯 / queue 擴展 / 安全)→ ~3 min 擴展瓶頸 → ~1 min 收尾。開場先用一句話複述題目、宣告配速、把細節標記為「待會 deep dive」——宣告配速本身就是高分動作,讓面試官知道你有限時面試的心智模型,也替你爭取「先框架、後細節」的許可。

1. 釐清需求與範圍 (Clarify & Scope ~5 min)

開場後先問清楚邊界,別急著畫架構。Webhook 題的死穴在「投遞語意」——一定要在開頭就把 delivery guarantee 與失敗處理時限 問死。

釐清問題 為何要問 / 影響到哪個決策 本題假設(依來源)
投遞保證是 at-least-once 還是 exactly-once? 決定整個重試 / 去重架構 at-least-once(招牌取捨)
接收端暫時掛掉時,要嘗試多久? 決定 retry 階梯與 DLQ 設計 數天內 持續嘗試(例 SLA 3 天)
規模多大?多少使用者 / 多少 event types? 直接驅動估算與 queue / DB 選型 1 億使用者、100 種 event types
延遲要求? 決定是否非同步、queue 深度 near real-time(近即時,可非同步)
event 來源是誰?內部服務還是外部? 決定 ingestion 入口與信任邊界 內部 product services 發佈 domain events
客戶要能管理訂閱(啟用 / 停用 / 改 endpoint)? 決定 endpoint 管理 API 與資料模型 (activate / deactivate / 選 event types)
要不要驗證 endpoint 真的屬於客戶 / 防偽造? 決定 auth / 簽章設計 (所有權證明 + payload 簽章)
defer 話術

重試耗盡後怎麼辦 我先標記,等 deep dive 再展開失敗階梯——現在先假設 ingestion 把事件丟進一個會幫我重試的 delivery 子系統,這樣我們能先把 ingest → fan-out → deliver 端到端串起來。」把最深的可靠性細節推遲,保住架構敘事連貫。

2. 功能性與非功能性需求 (FR / NFR 與設計北極星)

設計北極星(NFR 直接導出後續所有選型的判準):

NFR 導出的設計北極星 後續對映選型
at-least-once + 數天重試 每次「送出嘗試」都要是 durable 的、失敗要能重放 queue 當 durable buffer、retry 階梯、DLQ
接收端可能慢 / 掛 慢的 receiver 不可阻塞 ingestion;要可隔離 / 熔斷 ingestion 與 delivery 解耦、per-endpoint partition
1 億用戶 × 100 types、高吞吐 訂閱查詢要避開 DB、queue 要可水平 partition cache、queue partition + sub-shard
near real-time + read-heavy 投遞路徑查訂閱不能每次打 DB user_id:event_type cache
雙向信任 要證明 endpoint 屬於客戶、也要讓客戶驗證 sender 所有權 challenge、HMAC 簽章
一句話北極星

「這是一個 絕不丟事件、可重放、慢 receiver 互不影響 的非同步投遞系統」——記住這三個詞,後面每個選型都能從它推回去。

3. 容量估算 (Back-of-Envelope ~4 min)

估算的目的是 驅動 queue / cache / DB 決策。回連 02-Distributed-Systems/05-Numbers-to-Know

訂閱資料量(DB rows)

1 億 users × 100 event types = 100 億 (10B) 訂閱列
每列 ≈ 1KB
→ 10B × 1KB ≈ 10TB

投遞吞吐(events/sec,採來源算式)

來源假設:每個 (user, event_type) 每天送出 1,000 筆 events
總量/day = 1,000 × 100M(users) × 100(event_type) = 10T events / day
events/sec = 10T ÷ 86,400 ≈ 120M events / sec   ← 來源給的數字

下表為 推導值(由「使用者數 × event types × 每對每日量」與「rows × 每列大小」算出)。注意:120M events/sec 是來源用「每對每天 1,000 events」這個偏激進假設推出的上界——面試時應點明這假設極端,真實量級多半小很多;重點是 這個數字逼我們把 queue 做成可 partition、把 DB 前面擋 cache,而非數字本身的精確度。

維度 數字 對後續決策的意義
訂閱列數 ≈ 100 億 rows(推導值) 決定 DB 容量;以 (user_id, event_type) 為查詢鍵
訂閱儲存量 ≈ 10TB(推導值) 現代 SSD 單機起步可行,先不 sharding,硬體瓶頸時再分片
投遞吞吐 ≈ 120M events/sec(來源數字) 必須 queue partition + 多 delivery worker 才扛得住
寫 vs 讀 寫一次(註冊)/ 讀極多(每事件查訂閱) read-heavy → DB 前必須加 cache
估算的最大收穫

兩個結論:(1) 10TB 訂閱表單機 SSD 可作為起點,硬體到頂才 sharding(別過早分片,對照 05-Database-Advanced/02-Sharding);(2) 投遞吞吐高到單一 queue 撐不住 → 必須 partition,這是後面 queue 設計的根因。

4. 用到的積木 / Building Blocks Used

架構即目錄:先給導覽圖,再展開。下表每個決策都對映一個既有 concept note(無 alias 連結);本表即「本案例練到哪些積木」的單一事實來源(single source of truth)。

設計決策 積木 在本題的作用(≤1 行)
容量估算驅動選型 02-Distributed-Systems/05-Numbers-to-Know 算出 10TB / 高吞吐,決定 cache 與 queue partition
ingestion 與 delivery 解耦 09-Messaging-Coord/02-Queue queue 當 durable buffer,吸收突發、隔離慢 receiver
高吞吐分流 09-Messaging-Coord/03-Kafka partition event/delivery queue 以水平擴展、保 per-endpoint 序
at-least-once 投遞與重試 12-Ops-and-Reliability/03-Reliable-Delivery 重試、DLQ、可見性逾時等可靠投遞機制的母題
失敗 / 重試 / 退避 01-Networking/07-Fault-Handling 指數退避 + jitter、超時、熔斷的通用機制
慢 / 過量 receiver 保護 12-Ops-and-Reliability/02-Overload-Protection per-endpoint throttling / circuit breaking,防 hot endpoint
訂閱查詢加速(read-heavy) 07-Caching-Storage/01-Caching user_id:event_type → endpoint 設定放 cache,擋住 DB
訂閱資料儲存 06-Database-Tech/01-Database 持久化 endpoint / 訂閱,10TB 單機起步
投遞 HTTP 語意 01-Networking/03-HTTP-and-HTTPS webhook = 對客戶 URL 發 HTTPS POST,TLS server auth
sender / receiver 雙向信任 03-API-Design/05-API-Security HMAC 簽章 + endpoint 所有權 challenge/response
delivery worker 水平擴展 02-Distributed-Systems/03-Scalability worker 無狀態,依 queue 深度 autoscale
管理 API(CRUD endpoint) 03-API-Design/02-REST 以 webhook 資源為中心定義註冊 / 更新 / 刪除

5. API 設計 (API Design ~3 min)

由 FR(管理 endpoint)驅動,採 REST 資源風格(理據見 03-API-Design/02-REST)。注意:投遞方向(平台 → 客戶 endpoint)不是這裡的 API,這些 API 只管「客戶管理自己的訂閱」。

Method Path 用途 重點
POST v1/webhook 註冊 endpoint(body 帶 event_typeendpoint 建立訂閱、回 { id };此時做所有權 challenge + 配 secret
PUT v1/webhook/:id 更新 endpoint(body 帶 endpoint 改接收 URL / 設定;改 domain 時可重新 challenge
DELETE v1/webhook/:id 刪除 endpoint 連帶 cache invalidation
細節話術

「啟用 / 停用」可走 PUT 改一個 status 欄位(active / deactivated / suspended),不必額外開 endpoint。suspended 是系統在重試耗盡後自動設的狀態——這個欄位之後在失敗階梯會用到,先埋伏筆。

6. 資料模型與儲存選型 (Data Model & Storage Choice ~4 min)

核心是訂閱表(endpoint registry),它是投遞路徑每次都要查的地基(故獨立成段)。

WebhookEndpoints  (訂閱 / endpoint registry)
--------------------------------------------------
id            UUID/BIGINT   PK
user_id       BIGINT        客戶(租戶)
event_type    VARCHAR       訂閱的 domain event(100 種之一)
endpoint_url  VARCHAR       接收 webhook 的 HTTPS URL
secret        VARCHAR       HMAC 簽章用 shared secret(註冊時配發)
status        ENUM          active / deactivated / suspended
created_at    TIMESTAMP
--------------------------------------------------
查詢鍵:(user_id, event_type)   ← 投遞時用它找出所有匹配 endpoint
~1KB / row × 10B rows ≈ 10TB
RetryableEvents   (重試耗盡前的待重放事件,見 deep dive 8.1)
--------------------------------------------------
event_id      UUID          冪等去重 / 追蹤
endpoint_id   BIGINT        目標 endpoint
payload       BLOB/JSON     原始事件內容
attempt_count INT           已重試次數
next_retry_at TIMESTAMP     scheduler 掃描依據
first_failed_at TIMESTAMP   判斷是否超過 SLA(如 3 天)
--------------------------------------------------

儲存選型:

選項 適配本題嗎 理由
關聯式 / KV DB(訂閱表) ✅ 主選 查詢恆為 (user_id, event_type) 點查;10TB 單機 SSD 起步,到頂才 sharding
Cache(Redis)擋訂閱查詢 ✅ 必備 read-heavy,每事件查一次,cache 才能保護 DB + 降 ingestion latency
DB / store 存 retryable events ✅ 輔助 重試耗盡前需 persistent storage 暫存,由 scheduler 掃描重放
Queue(非 DB,但是儲存層) ✅ 關鍵 每個「送出嘗試」要 durable,worker crash 也不丟(見 8.2)

→ 積木:DB 選型見 06-Database-Tech/01-Database;cache 見 07-Caching-Storage/01-Caching;queue 見 09-Messaging-Coord/02-Queue

邊界

「10TB 單機足夠起步」是 當前規模 的結論。命中硬體瓶頸(容量 / 寫入)時才引入 05-Database-Advanced/02-Sharding,依 user_id 分片即可——詳見第 9 段。

7. 高階架構 (High-Level Design ~6 min)

把前面的積木串成端到端流程。先看兩條核心 flow,再看 scaled 架構。

(A) Endpoint 管理 flow(register / edit / delete)

Client (客戶)
  │  POST v1/webhook { event_type, endpoint }
  ▼
Management API Server (stateless)
  │ 1. 對 endpoint 做所有權 challenge/response(POST 隨機 token,驗回傳)
  │ 2. 配發 shared secret
  │ 3. INSERT WebhookEndpoints (user_id, event_type, endpoint_url, secret, status=active)
  ▼
DB (訂閱表)  ──► 回 { id }
       └─ 同步 invalidate / 更新 cache (user_id:event_type)

(B) 投遞 flow(ingest → fan-out → deliver)

內部 product service
  │  publish domain event (e.g. invoice.paid)
  ▼
 Message Queue (Event Queue)        ← 解耦 + 緩衝:ingestion 掛掉不波及內部服務
  │  poll
  ▼
 Webhook Ingestion Service
  │ 1. 查 Cache:(user_id, event_type) → 匹配的 endpoints?(miss 才打 DB)
  │ 2. 對每個匹配 endpoint,產生一個「送出嘗試」
  ▼
 Delivery Queue (per-endpoint partition)   ← durable buffer,含 retries
  │  poll
  ▼
 Delivery Workers (stateless, autoscale)
  │ 1. 用 secret 對 (timestamp + raw_body) 做 HMAC-SHA256,放 header
  │ 2. HTTPS POST 到 endpoint_url(驗 TLS server cert / hostname)
  │      ├─ 2xx          → ACK,從 queue 移除(visibility timeout 機制)
  │      ├─ 5xx / timeout → 指數退避 + jitter 重試(見 8.1)
  │      └─ 4xx schema 等 non-retryable → 直接 DLQ
  ▼
 客戶 endpoint(自行驗 HMAC、做冪等去重)

(C) Scaled 架構(端到端)

                        Management API (stateless, N台) ──► DB(訂閱表) ──► Cache
                                                                  ▲
   內部服務 ─► Event Queue ─► Ingestion Service(N台) ─查訂閱─────────┘
   (publish)   (partition by         │  fan-out:每個匹配 endpoint 產生一筆
                event_id)            ▼
                          Delivery Queue (partition by endpoint_id)
                                     │   ┌──────────────────────────────┐
                                     ├──►│ Delivery Worker pool (autoscale)│──HTTPS POST──► 客戶 endpoint
                                     │   └──────────────┬───────────────┘
                                     │       失敗(5xx/timeout)│  retry 耗盡
                                     │                       ▼
                                     │        RetryableEvents store ◄── Scheduler 週期掃描 → 重放回 queue
                                     │                       │  超過 SLA(3天)
                                     ▼                       ▼
                              per-endpoint throttle /     DLQ + endpoint.status=suspended + 通知客戶
                              circuit breaking

關鍵 hand-off:內部服務 → event queue(解耦)→ ingestion(查 cache fan-out)→ delivery queue(per-endpoint partition)→ worker(HMAC + POST);失敗走 退避重試 → retryable store + scheduler → DLQ + suspend 的階梯,與正常路徑解耦。

8. 深入探討 (Deep Dives ~9 min)

不重複原則(link-don't-restate)

通用機制(queue 語意、cache 策略、replication)只在 concept note 解釋;這裡只講「本題怎麼用、選哪個、為什麼」。任一機制超過 ~3 行就砍掉改連結。本題無 home note 的新材料(退避+jitter、HMAC 簽章、所有權 challenge、SQS visibility timeout)才在此 inline 教學並登記到 new_concepts_introduced

8.1 如何實現 at-least-once 投遞(招牌取捨 + 失敗階梯)

問題:投遞時 endpoint 回錯或連不上,怎麼辦?耗盡重試還是失敗呢?
為何是瓶頸:webhook 平台的全部價值就是「事件最終會到」。接收端的可用性 不在我們掌控 內,所以失敗是常態而非例外,必須把「失敗處理」設計成一級流程。

新材料:Exponential backoff with jitter(本案例首次 introduce,尚無 atomic note → 已登記 new_concepts_introduced)

  • 指數退避:重試間隔逐次倍增(100ms → 200ms → 400ms → 800ms …)。目的是「東西壞了就快速退讓」——對方或網路有問題時,立刻 / 等間隔猛重試只會加重負載、引發連鎖故障;指數退避快速降低重試壓力,給對方恢復時間。
  • Jitter(隨機抖動):在退避間隔上加隨機量。若上萬個 client 同時失敗、用同一節奏重試,會形成 thundering herd(同步重試風暴);jitter 把重試打散到不同時間點,讓 receiver 承受平滑、可控的負載。

失敗階梯(本題核心設計)

投遞失敗 (5xx / timeout)
  │
  ├─ ① 初始重試:exponential backoff + jitter,最多 N 次(例 5 次)
  │        └─ 期間成功 → 完成
  │
  ├─ ② N 次後仍失敗 → 標記 retryable error,寫入 persistent storage
  │        └─ Scheduler 週期掃描到期事件 → 重新放回 queue 再試
  │
  ├─ ③ 在 SLA 內(例 3 天)反覆 ②,直到成功
  │
  └─ ④ 超過 SLA 仍失敗 → 送 DLQ + endpoint.status=suspended + 通知客戶
        (另:一開始就 non-retryable 的(schema 錯、缺 secret)→ 直接 DLQ,不重試)
階段 機制 為什麼這樣設計
初始重試(秒~分) 退避 + jitter,in-queue 重投 多數失敗是 瞬時 的(對方重啟、暫時 5xx),快速重試即可救回
中期重放(分~天) retryable store + scheduler 不能讓事件在 queue 裡占位數天;移到 DB 由 scheduler 排程重放
終局(超 SLA) DLQ + suspend endpoint + 通知 對方長期掛掉,停止無謂重試、保護平台、把問題回饋給客戶
non-retryable 直接 DLQ schema / secret 錯重試無意義,省資源

選擇與理由at-least-once 而非 exactly-once。網路超時下我們無法區分「對方沒收到」與「對方收到了但 ACK 丟了」,重試就可能造成重複。要做 exactly-once 需跨平台/客戶的分散式協調,成本極高且仍脆弱。所以 接受重複、把冪等責任交給客戶(事件帶唯一 event_id,客戶用它去重)——這就是本題的 core_tradeoff
→ 積木:12-Ops-and-Reliability/03-Reliable-Delivery01-Networking/07-Fault-Handling09-Messaging-Coord/02-Queue

8.2 如何擴展(queue partition + cache + DB)

問題:吞吐高(來源估到 120M events/sec)、訂閱表 10TB、慢 receiver 不能拖垮全局。
為何是瓶頸:單一 queue、單一 DB、共享 worker pool 都會在這個量級崩潰;且「一個爛 endpoint」可能拖死所有投遞。

新材料:SQS visibility timeout(達成 at-least-once 的 queue 機制)

consumer 取走訊息後,該訊息進入「不可見」狀態一段時間(visibility timeout),其他 consumer 看不到它;只有 consumer 成功處理並刪除後才真正移除。若 consumer 在逾時內沒刪(crash / 處理失敗),訊息重新變可見、被重投——這正是 queue 層保證 at-least-once 的關鍵。

手段 做什麼 取捨 / 理由
DB 前加 cache user_id:event_type → endpoint 設定;ingestion 先查 cache,miss 才打 DB read-heavy(每事件查一次),cache 保護 DB 並降 ingestion latency
Event queue partition partition key = event_id 平均分散負載、最大化吞吐(事件之間無序依賴)
Delivery queue partition partition key = endpoint_id 同一 endpoint 的投遞落同一 partition → 可做 per-endpoint throttle / 熔斷
Hot endpoint sub-shard key = endpoint_id + ":" + hash(event_id) % K 某 endpoint 爆量時再切 K 份,分散單一 endpoint 的壓力
獨立 delivery workers ingestion 與 delivery 解耦,worker 依 queue 深度 autoscale 慢 / 失敗 receiver 不阻塞 ingestion;worker 可獨立水平擴展
DB sharding(後備) 命中硬體瓶頸時依 user_id 分片 10TB 單機可起步,先不做,到頂才上

選擇與理由:兩個 partition key 的差異是精髓——event queue 用 event_id 求最大分散;delivery queue 用 endpoint_id 求隔離(把同一 endpoint 的投遞收攏,才能對它限流 / 熔斷,避免一個慢 endpoint 拖垮共享 worker)。這是「分散」與「隔離」兩種相反目標在不同層的體現。
→ 積木:09-Messaging-Coord/03-Kafka09-Messaging-Coord/02-Queue07-Caching-Storage/01-Caching12-Ops-and-Reliability/02-Overload-Protection

8.3 驗證與安全(雙向信任)

問題:(1) 怎麼確認 endpoint 真的屬於這個客戶,而不是隨便填別人 URL 來濫用平台?(2) 怎麼讓客戶(receiver)確認收到的 webhook 真的來自我們、沒被偽造 / 竄改?
為何重要:webhook 是「我們主動對外部 URL 發 POST」——既可能被用來打別人(SSRF / 濫用),收到的一方也無從天然信任 sender。

新材料:所有權證明 + HMAC 簽章(登記 new_concepts_introduced)

  • TLS server authentication(基礎且必須):只允許送 https://,並驗 server 憑證鏈由可信 CA 簽發、憑證 hostname(CN/SAN)與 receiver.com 相符——確認我們真的在跟該 domain 擁有者通訊。
  • Endpoint 所有權 challenge/response(註冊時):註冊時先 POST 一個隨機 token 給 receiver,只有對方正確回傳該 token 才接受該 endpoint;在關鍵變更(domain / IP / 憑證)或定期可重新 challenge。防止客戶把別人的 URL 註冊進來當攻擊放大器。
  • Payload signing(HMAC-SHA256):註冊時 sender / receiver 共享一組 secret。每次投遞,sender 用 secret 對 timestamp + raw_body 算 HMAC-SHA256 放 header;receiver 用同 secret 重算並做 constant-time comparison(定時比較,防 timing attack)。timestamp 一併簽是為了讓 receiver 能拒絕重放的舊請求。

方向 機制 防的是什麼
我們 → 確認 endpoint 屬實 TLS server auth + 所有權 challenge/response URL 偽填、把平台當 SSRF / 濫用放大器
receiver → 確認 sender 屬實 HMAC-SHA256(timestamp + raw_body) + 定時比較 偽造 webhook、竄改 payload、重放攻擊

→ 積木:03-API-Design/05-API-Security01-Networking/03-HTTP-and-HTTPS

9. 擴展與瓶頸 (Scaling & Bottlenecks ~3 min)

主動找出系統會先在哪裂開並補強。

瓶頸 / 風險 補強手段 積木
一個慢 / 故障 endpoint 拖垮共享 worker endpoint_id partition + per-endpoint throttle / circuit breaking 12-Ops-and-Reliability/02-Overload-Protection09-Messaging-Coord/03-Kafka
投遞吞吐超單一 queue queue partition(event_id 分散 / endpoint_id 隔離)+ hot endpoint sub-shard 09-Messaging-Coord/03-Kafka
每事件查訂閱壓垮 DB user_id:event_type cache 擋住絕大多數讀 07-Caching-Storage/01-Caching
訂閱表 10TB 命中硬體上限 user_id sharding(先不做,到頂才上) 05-Database-Advanced/02-Sharding
worker crash 導致事件遺失 queue durable + visibility timeout:未 ACK 自動重投 09-Messaging-Coord/02-Queue12-Ops-and-Reliability/03-Reliable-Delivery
重試事件長期占用 queue 達初始上限後移入 retryable store + scheduler 排程重放 12-Ops-and-Reliability/03-Reliable-Delivery
ingestion 失敗波及內部服務 內部服務 → event queue 解耦,ingestion 掛掉不影響內部流程 09-Messaging-Coord/02-Queue
drive 的關鍵句

「系統會 先在「慢 endpoint 污染共享 worker」處裂開 → 所以 delivery queue 按 endpoint_id 分流並 per-endpoint 熔斷;其次是讀路徑(每事件查訂閱)→ cache 擋;吞吐到頂 → queue partition + sub-shard。寫只發生在註冊一次,寫不是瓶頸。」

10. 取捨總表與收尾 (Trade-off Recap & Wrap-up ~1 min)

決策點 選擇 放棄了什麼 為什麼值得
投遞保證 at-least-once exactly-once(客戶免去重) 絕不丟事件 + 避開分散式協調成本(招牌取捨
失敗處理 退避+jitter → retryable store → DLQ+suspend 簡單「失敗即丟」 數天內努力投遞、保護平台、回饋客戶
ingestion/delivery queue 解耦 + 獨立 worker 同步直送的簡單 慢 receiver 不阻塞、可獨立 autoscale
queue 分流 event_id 分散 / endpoint_id 隔離 單一 partition key 的一致 一個求吞吐、一個求隔離 + 可熔斷
訂閱查詢 cache + 單機 DB(不預先 sharding) 預先分片的炫技 read-heavy 靠 cache,10TB 單機起步足夠
安全 TLS + 所有權 challenge + HMAC 簽章 無驗證的簡單 防濫用 + 雙向信任
收尾話術

「整題的單一錨點是 at-least-once:我們選擇『寧可重複、絕不丟失』,把冪等推給客戶(事件帶 event_id),換來接收端掛掉也不丟事件——並用 退避重試 → retryable store + scheduler → DLQ + suspend 的階梯把這個保證落實到數天。其餘是這個保證的支撐:queue 解耦與緩衝、delivery 按 endpoint 隔離以熔斷慢 receiver、cache 擋住每事件的訂閱查詢。10TB 訂閱表單機起步,不過早 sharding。」

後續追問演練 (Interviewer Follow-ups — "What if…")

為什麼選 at-least-once 而不是 exactly-once?

為什麼在內部服務與 ingestion 之間放一層 queue?

重試為什麼要加 jitter?

event queue 用 event_id、delivery queue 用 endpoint_id,為什麼不同?

某個 endpoint 變成 hot spot(單一 endpoint 接收量爆增),怎麼辦?

delivery worker 在投遞中途 crash,事件會丟嗎?

客戶 endpoint 連掛三天,重試耗盡後怎麼處理?

客戶抱怨收到重複事件,平台該不該負責去重?

流量放大 100x,架構哪裡先撐不住?怎麼補?

怎麼防止有人把別人的 URL 註冊進來當攻擊放大器(SSRF / 濫用)?

主動回想:白板重建 (Whiteboard from Scratch)

不看上文,照下面提示把整個設計重建一次。每格都該能 30 秒內口述。

常見陷阱 (Exam Traps for This Problem)