題目與適用情境
設計一個共用通知平台,供登入、訂單、社群和行銷系統呼叫。平台支援行動推播、簡訊、Email 和站內通知;交易訊息包括 OTP 與付款結果,批次訊息包括活動提醒和行銷觸達。呼叫端可以立即傳送或預約傳送,使用者可以依管道和通知類型退訂,行銷訊息還要遵守使用者時區內的勿擾時段。
本題採用以下面試假設:
- 每日產生 10 億個「管道投遞任務」。一則業務通知若同時走簡訊與 Email,會計為兩個任務。
- 平均吞吐約為
1,000,000,000 / 86,400 ≈ 11,600個任務/秒;活動尖峰以平均值的 20 倍估算,約 232,000 個任務/秒。 - API 成功受理的可用性目標為 99.99%,受理延遲 p99 小於 200 毫秒。
- OTP 從受理到提交給管道供應商的 p99 小於 2 秒;行銷任務允許在 15 分鐘內平滑傳送。這兩個目標都不承諾終端實際顯示時間。
- 若一個投遞任務的持久化信封平均為 1 KB,邏輯寫入量約 1 TB/天;保留 30 天約 30 TB,尚未計入索引、副本、回執與壓縮。
這些數值用來推導分區、積壓和流量隔離,不代表任何供應商的效能。答案不展開範本編輯器、行銷受眾演算法、計費和 A/B 測試。目標職位是中高階後端、平台或系統設計工程師,核心問題是如何在外部管道語意各異時,守住優先級、可復原性和真實可觀測性。
面試官考察重點
第一項訊號是候選人會不會定義「傳送成功」。API 回傳 202 只表示平台已持久化受理;供應商回傳成功通常表示它接受了請求;終端收到、系統顯示和使用者開啟又是後續狀態。Firebase 的 Sends 指標可能只表示訊息已排隊或已交給 APNs;APNs 的成功回應對應單次請求成功。若把這些狀態全寫成 delivered,營運指標和故障處置都會失真。
第二項訊號是能否用資源隔離落實優先級。一個帶優先級欄位的共用佇列仍可能被 5,000 萬人行銷活動占滿磁碟、消費者連線和供應商配額。強回答會把交易與批次任務分開排隊、分開設定消費者和受保護管道預算,同時允許批次流量借用閒置容量。OTP 洪峰也需要自身上限,避免「高優先級」繞過所有保護。
第三項訊號是對投遞保證的說法精確。標準佇列可能重複投遞;工作程序應依穩定的 delivery_id 做冪等處理。網路逾時若發生在供應商已接受請求之後,內部去重無法撤回外部副作用。若供應商沒有冪等傳送介面,系統只能降低重複機率、記錄不確定結果,再依通知風險決定是否重試或切換供應商,不能承諾端到端 exactly-once。
第四項訊號是故障路徑能否閉環:永久錯誤停止重試並清理失效端點;429、5xx 和網路錯誤進入帶抖動的退避;過期任務直接終止;死信重播仍使用原 delivery_id,並重新檢查過期時間與退訂狀態。回執可能重複和亂序,因此要先保存原始事件,再依管道允許的狀態轉換產生目前檢視。
回答前需要釐清的問題
- 「送達」的業務定義是什麼? OTP 的可控目標應到「供應商已接受」,因為裝置離線、電信商和使用者權限不受平台控制。若業務要求使用者已讀,只能依賴支援該能力的管道回執或用戶端埋點,並標示涵蓋率。
- 通知類型和優先級由誰決定? 伺服器維護受控的通知類型目錄,由類型對應優先級、可用管道、範本和退訂規則。若讓呼叫端自由傳
critical=true,所有團隊都會搶占緊急通道。 - 哪些訊息可略過勿擾或退訂? 只有經過法遵確認的交易類型可以例外。行銷訊息在真正進入管道佇列前檢查最新偏好;使用者在預約後退訂,不應因早期快照而繼續收到訊息。
- 是否允許跨管道降級? 簡訊失敗後改發推播會改變成本、可達性和使用者預期。必須依通知類型設定降級圖,並區分「明確拒絕」和「結果未知」;結果未知時切換管道可能造成兩次觸達。
- 順序要求是什麼? 同一使用者的密碼重設訊息可能需要保序,普通社群提醒通常不需要全域順序。只在
(userid, notificationtype)等必要鍵上保序,避免把整個系統序列化。 - 資料保留和隱私邊界是什麼? 訊息本文可能包含敏感資訊。佇列只保存範本版本和最少參數,依欄位加密並設定保留期;日誌不得記錄 OTP、完整手機號碼或 Email 本文。
30 秒回答框架
「我會先把受理成功、供應商接受、終端送達和使用者已讀拆成不同狀態。入口把通知與 outbox 原子寫入資料庫後回傳 202,非同步展開收件者,並在接近投遞時檢查偏好、勿擾、過期時間和去重鍵。每個管道依交易、普通和批次訊息拆佇列與消費者,替交易流量保留供應商預算,批次任務只借用閒置容量。工作程序用穩定 delivery_id 處理至少一次任務,依永久錯誤、限流和暫時故障分類;暫時錯誤採指數退避並加入抖動。供應商回執先追加到事件表,再依管道狀態機折疊,避免亂序把 delivered 降回 sent。最後用行銷洪峰疊加 OTP、重複任務和亂序回執做壓測,驗證 2 秒 SLO 與積壓復原。」
分步深入解答
入口 API 不直接呼叫簡訊或推播供應商。同步路徑完成授權、通知類型驗證、固定範本版本和最小參數驗證,接著在同一個資料庫交易寫入通知紀錄與 outbox:
~~~text POST /v1/notifications { request_id, notification_type, recipients | audience_id, template_version, template_params, schedule_at, expire_at }
202 Accepted { notification_id, accepted_at } ~~~
requestid 用於呼叫端重試去重,notificationid 識別一次業務通知,delivery_id 識別「使用者 × 管道」的一次投遞。三者不能混用:同一通知可能展開為多個使用者和多個管道。入口提交後再由 outbox relay 發布事件,避免資料庫已提交但訊息未入列。對 5,000 萬收件者的活動,入口只保存受眾快照參照和游標,不在單一請求或交易中產生 5,000 萬筆資料。
受眾展開器分片讀取快照,產生小批次規劃任務。策略服務在任務接近傳送時讀取通知類型規則和使用者最新偏好,依序判斷退訂、勿擾、管道可用性、頻率控制、過期時間和降級順序。勿擾時間以使用者 IANA 時區計算,並涵蓋日光節約時間跳變;使用者沒有時區時採用產品明確指定的預設值,不可私下使用伺服器時區。被篩掉的任務寫入 SUPPRESSED 和機器可讀原因,才能解釋「為什麼沒發」。
核心資料可拆成三類:
~~~text Notification( notificationid, tenantid, notification_type, templateversion, scheduleat, expire_at )
Delivery( deliveryid, notificationid, user_id, channel, trafficclass, state, provider, providermessage_id, attemptcount, nextattemptat, stateversion )
DeliveryEvent( deliveryid, providereventid, eventtype, providertime, receivedat, rawpayloadref ) ~~~
Delivery 是查詢目前狀態的具體化檢視,DeliveryEvent 保留供應商回執事實。原始本文和個人資料放在受控儲存中,事件表只保留參照。providereventid 存在時建立唯一限制;供應商未提供事件 ID 時,可對正規化欄位產生去重雜湊。回呼簽章必須先驗證,未知欄位要相容保存,不能因供應商增加欄位而拒絕合法事件。
狀態模型至少區分:
~~~text ACCEPTED -> PLANNED -> QUEUED -> SENDING -> PROVIDER_ACCEPTED | v DELIVERED -> READ
Terminal side states: SUPPRESSED, EXPIRED, FAILED_PERMANENT ~~~
這張圖表達業務階段,不代表所有回執會依圖中順序抵達。供應商可能先送來 delivered,之後才到 sent;重複 webhook 也很常見。處理器先追加冪等事件,再用「管道 + 目前狀態 + 新事件」的轉換表更新投影。已經 DELIVERED 的簡訊不會被遲到的 sent 降級;支援已讀的管道可以從 DELIVERED 升到 READ。無法對應的事件留在原始表並告警,避免猜測狀態。
傳送資料面依「管道 × 流量等級」拆佇列,例如 sms.transactional、sms.bulk、push.transactional。每組有獨立積壓指標、消費者並行數和重試佇列。假設簡訊供應商上限為 10,000 次/秒,可以替交易流量保留 3,000 次/秒;批次流量平時可借用未使用部分,交易流量上升時便歸還。數字是設定範例,實際值來自供應商合約與壓測。租戶級公平排程可防止單一活動占滿批次預算,任務老化機制則避免普通任務長期飢餓。
獨立佇列比共用佇列內的優先級欄位占用更多主題、連線和維運設定,卻能隔離磁碟積壓與消費者資源。若規模較小、管道只有一個,共用佇列加加權公平排程會更簡單;當交易 SLO 與批次期限差了數個數量級,實體隔離更容易證明。無論採用哪種佇列,供應商端的實際限額都由每管道排程器統一執行,不能讓每個消費者都認為自己擁有完整額度。
管道工作程序領取至少一次任務後,以 deliveryid 原子認領一次 attempt。若該投遞已處於終態,就直接確認佇列任務;否則檢查 expireat、轉譯範本、呼叫供應商並保存回應。佇列重複不會重複建立投遞紀錄。外部請求逾時仍有三種結果:供應商未收到、已收到但回應遺失、處理結果未知。優先沿用供應商提供的冪等鍵;缺少這項能力時,把 attempt 標為 UNKNOWN,再透過供應商查詢或回執對帳。關鍵通知可在業務明確接受重複風險後重試,行銷通知通常可以等待對帳或過期。
錯誤分類決定後續動作:
- 無效參數、無效範本、明確的失效裝置 token 屬於永久錯誤,立即進入
FAILED_PERMANENT,並在證據明確時停用該端點。APNs 的 410 表示裝置 token 對該 topic 已不再有效。 - 429、供應商 5xx、連線中斷通常可重試,使用指數退避、完整抖動、最大嘗試次數和總截止時間。重試速度仍受管道預算控制,避免故障復原時形成第二次尖峰。
- 驗證失敗或帳戶設定錯誤是平台事件,應暫停對應供應商管道並告警;逐則重試只會放大故障。
- 到達
expireat的 OTP 或過時提醒進入EXPIRED。死信重播不能修改原過期時間,也不能換新deliveryid繞過去重。
供應商接受不等於終端送達。APNs 為每個 POST 回傳狀態與 apns-id,成功只證明請求層成功;Firebase 也把排隊或交給 APNs 計入 Sends。簡訊供應商可能進一步提供 queued、sent、delivered、undelivered 和 read 等狀態,而且 webhook 可能亂序。對外 API 因此回傳分層狀態與 status_reason,報表分別統計受理率、供應商接受率、可觀測送達率和可觀測開啟率,並把「該管道不提供此狀態」顯示為未知,不能算成失敗或成功。
擴充時,通知與投遞表依 notification_id 或時間分區,工作佇列依管道、等級和分片鍵水平擴充。單一使用者保序任務使用穩定的使用者分區鍵;不要求順序的批次任務可更平均地雜湊。30 TB 的 30 天邏輯信封不適合全部留在高成本線上索引:近期投遞狀態保留在線上,舊事件壓縮封存,只保留法遵與客服所需索引。容量計算還要另加副本、索引、回執放大和重試產生的寫入。
最關鍵的驗收情境是「批次洪峰不能拖慢 OTP」。先持續注入約 232,000 個任務/秒的活動負載,再疊加交易流量,檢查 OTP 從受理到提交供應商的 p99 是否仍低於 2 秒,批次積壓是否在 15 分鐘窗口內消化。接著注入 429 與 5xx,驗證退避沒有形成同步重試波;重複投遞同一佇列訊息,確認只沿用原 delivery_id;依 delivered -> sent -> delivered 傳送亂序與重複回執,確認狀態不回退;再涵蓋傳送前退訂、日光節約時間切換、過期死信和供應商結果未知。這些故障路徑都通過後,架構圖上的「可靠」才具備可驗證依據。
高品質示範回答
「我先定義四層結果:平台持久化受理、供應商接受、終端可觀測送達、使用者已讀。介面只承諾第一層,寫入通知紀錄與 outbox 後回傳 202。非同步展開受眾,在接近投遞時檢查最新退訂、勿擾、管道可用性和過期時間,再產生穩定的 delivery_id。
每日 10 億個管道任務平均約 11,600 次/秒,20 倍活動尖峰約 232,000 次/秒。OTP 要求 2 秒內提交供應商,行銷允許 15 分鐘平滑,因此我依管道和交易、普通、批次等級拆佇列與消費者,並替交易訊息保留供應商預算。批次可以借用閒置容量,但交易流量上升時要歸還;每個租戶還有公平份額。
工作程序依至少一次消費設計。佇列重複只會認領相同的 delivery_id。永久錯誤停止重試,429、5xx 和網路錯誤使用帶抖動的退避,並受總截止時間限制。供應商請求逾時可能已產生外部副作用;有供應商冪等鍵就沿用,沒有時標成 UNKNOWN 並對帳,不能宣稱端到端 exactly-once。
回執先寫入不可變事件,再依管道轉換表產生狀態投影,避免遲到的 sent 覆蓋 delivered。上線驗證會同時壓入行銷尖峰和 OTP,再注入重複任務、429、供應商逾時、亂序回執和傳送前退訂。核心指標是各等級佇列年齡、供應商提交延遲、錯誤分類、UNKNOWN 數量和狀態回退次數。」
常見錯誤
- API 回傳 202 就記錄 delivered → 平台只完成持久化受理 → 拆分 ACCEPTED、PROVIDER_ACCEPTED、DELIVERED 與 READ。
- 所有任務放進一個優先佇列 → 批次積壓仍共用磁碟、消費者與供應商額度 → 依管道和流量等級隔離資源,並允許受控借用。
- 高優先級無限制通行 → OTP 異常流量也能壓垮供應商和平台 → 替交易等級設定獨立上限、告警與租戶公平性。
- 看到佇列至少一次就宣稱使用者只收到一次 → 外部供應商呼叫可能已成功但回應遺失 → 內部依
delivery_id冪等,外部結果未知時對帳並揭露重複風險。 - 對所有錯誤立即重試 → 永久錯誤浪費資源,429 與 5xx 會形成重試風暴 → 依永久、暫時、限流和平台設定錯誤分類,暫時錯誤退避加抖動。
- 直接覆蓋目前狀態 → 亂序回執可能把 delivered 改回 sent → 先保存冪等事件,再用管道狀態轉換表更新投影。
- 預約時一次凍結使用者偏好 → 使用者隨後退訂仍會收到行銷訊息 → 在接近進入管道佇列時重新檢查偏好與法遵規則。
- 死信重播產生新 ID → 去重失效,過期通知可能再次觸達 → 沿用原
delivery_id,重查退訂與過期時間。 - 用全域順序換取簡單性 → 無關使用者互相阻塞,吞吐受單分區限制 → 只替確有業務需求的使用者與通知類型建立局部順序。
追問及應對
追問一:5,000 萬使用者都要求當地時間上午 9 點收到活動通知,怎麼處理?
受眾展開時依 IANA 時區和當地日期建立時間桶,提前產生可限速的批次;每個桶在目標窗口內加入抖動平滑,避免整點同時放行。日光節約時間造成的不存在或重複本地時間要有產品規則,例如移到下一個有效時刻且每天最多一次。行銷的「上午 9 點」應定義為交給供應商的窗口,終端顯示仍不可控。
追問二:供應商回傳成功,但一直沒有送達回執,狀態是什麼?
保留 PROVIDERACCEPTED,不自動升級為 DELIVERED。到達管道規定的觀察窗口後可標記 DELIVERYUNKNOWN 供營運查詢,但不能把未知計入失敗率。若供應商提供查詢 API 或彙總報表,可以做非同步對帳;對帳資料的延遲與涵蓋率也要隨指標顯示。
追問三:delivered 回執先於 sent 抵達怎麼辦?
兩個事件都冪等追加,投影規則允許 PROVIDER_ACCEPTED 或 SENDING 升到 DELIVERED,之後抵達的 sent 只補充稽核事實,不降低狀態。若同一供應商之後送出明確撤銷或錯誤事件,應依該管道文件設計獨立轉換,不能用全域整數大小猜順序。
追問四:兩個簡訊供應商能否自動主備切換?
明確拒絕、連線前失敗或供應商整體熔斷時,可以依設定切換。請求逾時若發生在傳送之後,主供應商可能已投遞;立刻切換會增加重複簡訊。OTP 可在成本和安全團隊接受重複風險時切換,並保留相同業務操作識別;行銷訊息更適合等待查詢、回執或過期。切換策略應依通知類型設定,不能全域統一。
追問五:如何證明優先級隔離真的有效?
用封閉負載測試同時製造三類壓力:批次佇列持續積壓、某個租戶形成熱點、供應商連續回傳 429。驗收條件包括 OTP 供應商提交 p99 仍低於 2 秒、交易消費者未被批次任務占用、供應商總速率不超過設定、批次任務在 15 分鐘期限內復原。再故障一個交易消費者分區,確認預留容量能由其餘執行個體接手;只看平均吞吐無法證明隔離。