Articles
系統設計面試怎麼準備?從需求、容量到可靠性的完整推演
系統設計面試準備:以原創預約案例,推演需求、DAU、平均與尖峰流量、九十天儲存和熱門場次的鎖競爭。區分快取列表與權威預約交易,檢查最後一席、重複取消、提交後回應遺失及付款回調遲到。數字是明示假設,不是壓測結果;附容量算式、失敗路徑與五個延伸練習,避免背元件名取代一致性與可靠性判斷。
系統設計面試的準備,不是背更多架構圖,而是練習把需求變成數字、資料狀態和失敗路徑。你應能解釋某個元件為什麼存在、什麼情況會失效,以及新的限制如何改變原先選擇。
下面用一個完整的預約系統推演:從需求確認、平均與尖峰流量,到最後一席的競爭、提交成功但回應遺失,以及服務過載時的取捨。所有流量、儲存與服務目標都是原創假設,不是任何公司的實際規模、標準答案或量測結果。
開場先縮小問題,再說成功條件
假設題目是「設計預約平臺」。先問預約的物件是有固定數量的席位,還是可分割的時間區間;是否要付款、取消、候補與通知;席位是否屬於特定場次。本篇選擇固定場次的有限席位,不處理跨場次連續時段、座位選圖或候補排名。
核心功能是瀏覽場次、建立預約、查詢結果與取消。先把付款排除,最後再說明加入付款會新增哪些狀態。這不是逃避難題,而是先有一條完整可推演的核心路徑,再決定值得深挖的變體。
成功條件也要分開:場次列表可以有短暫陳舊;確認預約不能超出席位數;網路重試不能建立重複預約。對這個模型,先提議列表可陳舊兩秒,再與面試官確認。兩秒是討論假設,不是使用者已接受的正式服務承諾。
系統設計中的不變條件,是任何合法操作與故障恢復後都必須成立的性質。這裡最重要的是已佔用席位不超過容量,以及同一次預約請求不產生兩筆預約;它們比「使用高可用資料庫」更具體。
把日流量換成有單位的估計
假設每日活躍使用者為兩百萬,每人每天讀八次場次資訊,平均每人每天產生 0.1 次預約,讀寫尖峰皆暫用平均的十倍。這四個假設應寫在紙上;若題目後來改了讀寫比例,就能追蹤哪些結果需要重算。
每日秒數是 86,400。讀流量為 2,000,000 × 8 = 16,000,000 次每天,平均約 185.2 次每秒,十倍尖峰約 1,852 次每秒。預約為二十萬筆每天,平均約 2.31 次每秒,十倍尖峰約 23.1 次每秒。不要把每日請求數直接寫成 QPS。
| 本篇假設與公式 | 結果 | 它能支持什麼,不能支持什麼 |
|---|---|---|
| 讀取:2M DAU × 8 ÷ 86,400 | 約 185.2/s;十倍約 1,852/s | 提供初步讀路徑量級,不證明實際 CPU 成本 |
| 預約:2M × 0.1 ÷ 86,400 | 約 2.31/s;十倍約 23.1/s | 不能排除熱門場次同時湧入的寫入 |
| 每筆 1 KiB × 200,000 筆/日 | 204,800,000 bytes/日 | 只含假設的預約資料,不含日誌與索引 |
| 保留 90 天,三份完整副本 | 18.432 GB 原始;55.296 GB 三副本 | 使用十進位 GB;尚未計入索引、WAL 與其他表 |
| 單一熱門場次瞬間 600 次預約/s | 與全站平均分開處理 | 是另外加入的突發情境,不是由 DAU 自動推導 |
若預約紀錄一筆是 1 KiB,即 1,024 bytes,上表的 18.432 GB 不等於 18.432 GiB。口頭估算可以取整,但寫進後續設計前要固定單位。同樣地,三副本是儲存乘數,不表示每臺節點都能承受全部請求或自動達成某個 RPO。
需要重現這部分算式時,可以跑下面的原創小程式。它只驗證數量與單位,不是壓測工具,也沒有估出資料庫能承受多少並發。
def capacity_estimate(dau=2_000_000, reads=8, bookings=0.1,
peak_factor=10, row_bytes=1024,
days=90, copies=3):
read_day = dau * reads
booking_day = dau * bookings
raw_bytes = booking_day * row_bytes * days
return {
"read_average_rps": read_day / 86400,
"read_peak_rps": read_day / 86400 * peak_factor,
"booking_average_rps": booking_day / 86400,
"booking_peak_rps": booking_day / 86400 * peak_factor,
"raw_bytes": raw_bytes,
"replicated_bytes": raw_bytes * copies,
}
這個函式假定輸入是有意義的非負數量,副本與天數為正;它不是公用 API 的完整驗證器。面試中更重要的是能重新推算並指出排除的資料,例如冪等結果表、取消歷史、outbox、備份與付款記錄。
熱點會推翻全站平均的安全感
現在加入兩萬個席位同時開放、一個熱門場次收到 600 次預約每秒的情境。若每次操作都更新同一列剩餘數量,這列的鎖競爭可能比全站服務 CPU 更早成為瓶頸。多開幾臺 API server,並不自動提高同一權威資料列的更新能力。
也不能用 20,000 ÷ 600 宣稱約 33 秒一定售完:請求可能重複、被拒絕、取消,或尚未成功提交。這個除法只能描述理想化接納速度的量級,不能取代成功預約量測。
先提出要測的資訊:該場次的成功提交速率、鎖等待、重試次數、尾端延遲,以及席位衝突比例。若真正瓶頸是鎖,才討論拆分席位單位、分配配額或序列化命令;每個方案都會改變公平性、利用率或跨分片協調成本。
Google SRE 的過載章節提醒不同請求的資源成本可能不同,單一 QPS 指標不總能代表容量。用它來檢查這個推演:讀列表、建立預約、發通知不是相同成本;假設每臺機器某個固定吞吐而不說明工作內容,無法辯護。
最小架構先走完一筆預約
讀路徑可以是瀏覽器到 API、場次快取、資料庫。寫路徑則由 API 到權威預約交易,成功後再由 outbox 工作者處理通知。這個順序說明瞭誰儲存事實、誰可以延遲,不需要一開始畫十個微服務。
資料模型先有 Session 的容量與剩餘量、Booking 的身份與狀態,以及以使用者和請求鍵為範圍的 RequestResult。請求結果儲存參數摘要與預約 ID,用於辨識重試和衝突。唯一約束保護鍵身份;業務交易仍需驗證餘量與合法狀態。
列表快取可以暫時顯示還有席位,真正預約時必須向權威狀態確認。如果最後一席已售出,回傳不可預約,並讓介面重新整理;不要把快取查到的數量視為承諾。相反,已成功預約的確認結果不應被較舊的列表讀取推翻。
如果從單服務拆到預約、付款、通知三個服務,先解釋新邊界。通知晚幾秒可能可接受;席位成立但付款結果未知,則需要中間狀態與補償。服務數量增加的是協調問題,不只是框線數量。
第一版可以選擇單一權威寫入區域,其他地區讀取快取或副本。理由是先減少席位確認的跨區競爭;代價是遠端使用者寫入延遲,以及該區故障時無法立即確認新預約。這是本篇模型的選擇,不是所有預約系統的最佳方案。
如果題目要求兩個區域在網路分割時都繼續售出同一場次的最後一席,就不能同時默認一個即時共享的餘量。你需要改變承諾:事先分配不可重疊的區域配額、讓其中一方暫停確認,或接受之後可能需要處理超額承諾。選擇必須回到產品允許的損失與使用者體驗,而不是只說「使用多主複製」。
分配配額也有成本:某區用完配額,另一區仍有席位,可能出現暫時無法使用的容量。重新平衡配額的操作同樣需要權威與故障邊界。面試時先指出這項代價,再決定是否值得,以免新方案只把原本的瓶頸移到另一條協調路徑。
最後一席靠什麼避免超賣
一個簡化的 PostgreSQL 模型,可以用條件更新扣除席位,並檢查受影響列數。下列 SQL 只是交易內的一步,不是完整預約實作;還要在同一交易記錄預約、請求結果與必要的 outbox 事件。
UPDATE sessions
SET remaining = remaining - 1
WHERE session_id = :session_id AND remaining > 0
RETURNING remaining;
PostgreSQL 目前的交易隔離文件說明,Read Committed 下,簡單更新等待另一個更新提交後會重新檢查目標列的條件。對這個特定單列模型,第二個請求不能只憑較早讀到的一席繼續扣除;不能把這項行為泛化成所有跨列業務不變條件都安全。
錯誤做法是先 SELECT remaining,在應用程式判斷大於零,再無條件更新。兩個請求都可能讀到一。另一種錯誤是條件更新成功,但預約寫入失敗後沒有回滾,留下少一席卻沒有預約的狀態。
冪等鍵競爭也要談清楚:同一鍵同時抵達的兩個交易,只有一個能建立那次請求的權威結果,另一個等結果或讀回已完成紀錄。不同參數卻使用相同鍵應拒絕。具體 SQL、唯一約束、交易順序及錯誤處理仍需資料庫整合測試;本文沒有執行資料庫並發實驗。
再推演一次取消:原本容量是十,剩餘九,一筆已確認預約改成取消後,剩餘應回到十。若兩個取消請求同時到達,不能各自加一。取消操作必須驗證預約仍處於可取消狀態,只有成功改變狀態的交易才能釋放容量;重複取消讀回原結果,不再修改餘量。
這也提醒你,剩餘量是一份衍生狀態,需要定義對帳方式。可以比較有效預約與保留所占席位,檢查剩餘加占用是否符合容量;但對帳讀取也需要一致的觀測範圍。跨時間讀到兩個不同快照,不能直接宣布資料錯誤。
提交成功但回應遺失,不能當成失敗重做
時間線從一次請求開始:使用者送出鍵 K,交易寫入預約 B 和 K 的結果,提交成功,回應卻在網路中遺失。使用者此時只知道結果未知,不知道資料庫失敗。若重試換成新鍵,可能建立第二次業務請求。
冪等契約是服務對重複請求的行為約定:同一範圍、同一請求鍵與相同參數,回到同一業務結果。它不保證封包只送一次,也不等於不同業務意圖使用相同參數就必須去重。
AWS Builders’ Library 的冪等 API 說明討論呼叫方提供的請求身份、結果儲存與變更的原子性。依此原則,本篇模型讓使用者重試 K 時讀回 B;若參數不同,回傳衝突而非靜默接受。具體儲存期限依產品契約設定,不從一個例子推導所有服務都永久保留。
若通知服務故障,預約本身仍已成立。Outbox 讓通知工作稍後重試,但通知消費者也要面對重複事件;不能宣稱 outbox 讓所有外部效果 exactly once。通知失敗應監控和補償,不應在使用者重新整理時偷偷再建立一筆預約。
加入暫時保留與付款後,狀態會怎麼變
若面試官要求付款前保留席位,新增 HELD、CONFIRMED、EXPIRED 等狀態與期限。期限由服務端約定;使用者裝置顯示的倒數是提示,不是權威。釋放保留與扣取席位也要維持同一業務不變條件。
最容易漏掉的是付款回呼遲到。保留已過期並釋放,別人已取得席位,原回呼才聲稱付款成功。不能直接把舊保留改成確認而超賣;先定義業務處理:拒絕確認、進入退款或人工處理,或在仍有容量時重新取得有效保留。選哪一種需要產品約束。
取消與回呼同時發生也要有合法轉換與版本檢查。一次取消只釋放一次,同一回呼重送不反覆修改容量。這些邊界應留在權威交易與事件身份中,而不是讓不同工作者各自在快取裡判斷。
此時再評估資料保留、審計與對帳。容量不只包含成功預約,還包括保留、失敗請求與事件歷史。如果你延長結果儲存視窗,上一節儲存估計就要加上那部分;新的可靠性要求應該回到數字,不能孤立地補一個元件。
故障恢復要說明犧牲什麼、量測什麼
資料庫不可用時,可以繼續提供有限的快取列表,但暫停確認預約。這是選擇避免錯誤承諾,不是全面可用。告知使用者結果未知或暫時無法預約,並保留可重試身份,比假裝成功後補救更容易說明邊界。
RPO 是可接受的資料損失視窗,RTO 是恢復服務的時間目標。它們是要求,不是增加三副本就自動達成的結果。同步複製、跨區延遲、故障切換與備份復原各有代價;若目標尚未給定,應詢問或標註假設。
過載時限制接納、使用有界佇列,並讓客戶端採用有限次數、退避與抖動的重試。若工作排隊時間已經超過使用者耐心,繼續接納只會擴大未知結果與重試。明確佇列容量與拒絕行為,比說“加訊息佇列即可削峰”更完整。
驗證計畫可以分四組:單席位並發競爭、提交後斷線、通知重送、過期後遲到回呼。還要測恢復後查詢結果是否一致。這是建議的驗證範圍,不是本文已經跑完的生產測試;程式中的算式通過不能替代這些故障實驗。
把推演練成回答,而不是背結論
用英文說明時,可先說:“I can tolerate stale availability on the read path, but confirmation must use authoritative capacity.” 再給出最後一席的時間線。若對方把規模改成十倍,指出平均讀寫數要重算,而熱點鎖競爭不一定按同一比例變化。
以下題目提供不同的容量和狀態邊界,不是某家公司必考清單。練每一題時都留下假設、數量與一條失敗路徑,而不是只留下漂亮的架構圖。
| 題目 | 用它改變哪個約束 | 應解釋的取捨 |
|---|---|---|
| Design an Inventory Service with Reservations, Hot SKUs and Oversell Prevention | 席位換成熱門商品庫存 | 熱點、預佔與防超賣 |
| Design a Grocery-Ordering Backend with Reliable Inventory Reservations | 多商品與訂單狀態 | 部分成功和補償 |
| At-Least-Once Job Queue with Timeouts, Retries, a Dead-Letter Queue and Dependencies | 通知變成有依賴的工作 | 重送、逾時與終止策略 |
| Design a URL Shortener (High-Level and Low-Level Design) | 讀多寫少且沒有席位競爭 | 快取、唯一性與過期 |
| Design a Concurrent Car Reservation Service | 固定場次換成時間區間 | 區間衝突與並發確認 |
你能重算數字、修改狀態並保留正確邊界時,才是在練設計;只換掉服務名稱,仍是在背圖。
資料來源與延伸閱讀
- PostgreSQL:Transaction Isolation,目前版本的隔離與更新行為
- AWS Builders’ Library:Making retries safe with idempotent APIs
- Google SRE:Handling Overload
資料核對:2026 年 10 月 6 日。本文數字為假設估計,未以資料庫並發測試、壓測或故障切換驗證。