談大型系統設計時,最容易犯的錯,是還沒搞清楚問題,就急著畫架構圖。
「先放 Redis、再上 Kafka、資料庫做 Sharding、服務全部拆成 Microservices。」聽起來很專業,但如果一天只有幾千個請求、沒有高可用需求,也沒有跨區服務,這些設計可能只是在提前增加複雜度。
成熟的 System Design 會先知道現在的需求是什麼、未來可能在哪裡出問題,以及每一個架構選擇犧牲了什麼。元件多不代表設計更成熟。
第一步先把 Requirements 問清楚,再畫架構圖
System Design 應該從 Functional Requirements(功能需求)與 Non-functional Requirements(非功能需求)開始。
功能需求回答「系統要做什麼」,例如使用者可以上傳影片、學員可以觀看課程、系統可以寄送通知、管理員可以查看訂單。
非功能需求則回答「要做到什麼程度」:
- 同時多少人使用?
- 可以接受多慢?
- 需要什麼可用性?
- 資料可以延遲幾秒同步嗎?
- 如果失敗,可以接受多少資料遺失?
- 成本上限是多少?
同一個功能,在不同答案下可能會得到完全不同的架構。
先定義 SLO,才能知道「夠好」是什麼
如果沒有 Service Level Objective(服務等級目標),系統設計很容易變成無限加規格。
例如「網站要很快」太模糊;改成「95% 的主要頁面在某個延遲目標內完成」「付款流程每月可用性達到既定標準」,設計才有依據。
Google SRE 強調,Availability 與 Performance 應該用使用者感受到的結果衡量,而不是只看 Server 還活著。
因此 System Design 的第一個重要取捨,是 Business Impact 和 Engineering Cost 之間的平衡。
沒有 Scale,就沒有辦法討論要不要擴充
「很多人會用」不是容量需求。
至少要先做 Back-of-the-Envelope Estimation(粗略容量估算),把規模轉成可討論的數字。
常見估算包括每日活躍使用者、每人每天平均操作次數、平均與尖峰 QPS、每天新增多少資料、單筆資料大約多大、保存多久,以及上下載頻寬。
QPS 可以先用「每日總操作數 ÷ 86,400 秒」估平均,再另外考慮尖峰流量。重點是先確認系統規模到底是每秒十次、千次,還是十萬次,不需要追求小數點精度。
容量估算還要保留 Headroom
系統不應該平常就跑在接近極限的位置。
行銷活動、功能上線、媒體曝光、尖峰時段,都可能讓流量突然高於平均。容量規劃要考慮 Organic Growth,也要考慮 Launch 或 Campaign 帶來的 Inorganic Demand。
容量規劃更該看「尖峰來時還剩多少餘裕、少一個節點時還能不能服務」,而不是只看今天的平均流量。
先畫最小可行架構,再找哪裡會先壞
需求與容量有基本輪廓後,再畫 High-level Design。
一開始可能只需要 Client、Application Server、Database、Object Storage。
接著問:「按照剛才的流量和可靠性需求,哪個地方最可能先成為瓶頸?」
只有當具體瓶頸出現時,才加入 Load Balancer、Cache、Queue、Read Replica、CDN 或 Sharding。這樣每一個元件都有理由,不會把架構變成技術名詞展示圖。
Load Balancer 解決的是流量分配,不是所有效能問題
當單一 Application Server 無法承擔流量,或需要避免單點故障時,可以增加多個 Instance,再透過 Load Balancer 分配請求。
要順利 Scale Out,應用程式本身最好盡量 Stateless。若 Session 只能存在某一台 Server,流量一旦切到其他 Instance 就會產生問題。
Scale-out 的價值不只在效能,也能搭配多個 Availability Zone,降低單一 Failure Domain 故障造成的影響。
Failure Domain:冗餘要放在不同故障範圍才有意義
兩台 Server 如果都放在同一台實體主機、同一個機房或同一個 Availability Zone,遇到更大的故障時可能一起消失。
所以高可用設計更該確認「這些副本會不會被同一個事件一起打掉」,單純增加副本數量還不夠。
從 Process、Instance、Zone 到 Region,故障範圍不同,成本與複雜度也不同。需要做到哪一層,應該回到 SLO 與 Business Impact。
Cache 很快,但你同時買下了資料可能不是最新的問題
Cache 適合處理讀取頻繁、原始資料來源較慢,而且可以接受一定程度短暫不一致的資料。
常見做法是 Cache-aside:
- 先查 Cache。
- 沒有命中,再查 Database。
- 把結果寫入 Cache。
- 下次直接從 Cache 讀取。
這可以降低 Database 壓力,但馬上帶來新問題:TTL 要多久?資料更新時要不要 Invalidate?Cache 掛掉時系統能不能繼續服務?
因此 Cache 會用一致性與維護複雜度換取速度與負載下降,不能把它當成免費加速器。
Message Queue:把不需要同步完成的工作拆出去
如果使用者下單後,系統還要寄 Email、產生發票、更新分析資料、同步 CRM,而這些全部串在同一個同步 Request 裡,只要其中一個服務變慢,整個流程就一起變慢。
Message Queue 可以把非即時工作拆成非同步處理。
它的另一個價值是 Buffer。當流量突然暴增,Producer 可以先把工作放進 Queue,Consumer 再按照自身能力處理,不必讓瞬間尖峰直接把後端壓垮。
Queue 之後還要設計 Backpressure 和 Load Shedding
Queue 不是無限容量。如果 Producer 長時間比 Consumer 快,Lag 會持續累積,最後只是把故障延後。
因此要監控 Queue Depth、處理速率與等待時間,必要時增加 Consumer、限制上游流量,或對非關鍵工作做 Graceful Degradation。
Google SRE 也提醒,系統過載時要有合理降級方式;比起整個服務一起崩潰,先關閉次要功能或丟棄部分低優先工作,通常更能保住核心體驗。
Degradation Strategy:高負載時先保住核心路徑
大型系統不一定只有「全部正常」和「整站掛掉」兩種狀態。
如果推薦、即時統計、搜尋提示等次要功能非常耗資源,可以在壓力過高時暫時降低精度、延後處理或直接關閉,優先保住登入、付款、資料寫入等核心路徑。
這種 Graceful Degradation 應該在設計階段就決定,不要等事故發生時才臨時猜哪些功能可以關。
Failure Path 要在設計時先想過
Happy Path 只告訴你一切正常時資料怎麼流動;大型系統更需要先畫出失敗路徑。
下游 Service Timeout 時要 Retry 還是直接失敗?Retry 有沒有 Exponential Backoff?同一個 Request 重送會不會重複扣款?部分成功時怎麼補償?
這些問題如果沒有事先設計,故障發生時很容易把小問題放大成 Cascading Failure。
Database Scaling 不應該從 Sharding 開始
Sharding 很有吸引力,因為聽起來像大型系統必備,但它也會把查詢、交易、重新分片、Hotspot、跨 Shard Join 等問題帶進來。
比較穩健的順序通常是:
- 先確認 Query 與 Index 是否合理。
- 垂直升級到合理範圍。
- 讀取壓力高時考慮 Read Replica。
- 用 Cache 降低重複讀取。
- 資料量或寫入量真的超過單一節點能力時,再考慮 Sharding。
如果架構已經變得難以修改,也可以延伸看系統愈做愈慢、愈改愈亂?架構最佳化應該先從哪裡開始。
SQL 還是 NoSQL,要先看資料與 Access Pattern
選 Database 不應該從「哪個比較新」開始。
如果資料有明確 Schema、交易一致性很重要、關聯查詢多,SQL 往往更自然;如果資料模型彈性大、需要非常高的水平擴充能力,或存取模式適合 Key-value/Document,NoSQL 可能更合適。
比較重要的問題是:最常見 Query 是什麼、哪些資料必須強一致、可以接受 Eventual Consistency 嗎、讀多還是寫多,以及是否需要跨資料關聯。
Consistency 也是明確取捨,不是越強越好
跨節點或跨區域系統裡,強一致通常需要更多協調,也可能增加延遲與可用性成本。
像付款餘額、庫存扣減可能需要更嚴格的一致性;文章瀏覽數、推薦結果、分析報表則可能接受短暫延遲。
把所有資料都要求同一種一致性,通常會讓系統付出不必要成本。應該依資料的 Business Invariant 分類。
Reliability:先找 Single Point of Failure
大型系統不只要平常能跑,也要想元件失敗時怎麼辦。
可以逐層問:Application Server 掛掉怎麼辦?Database Primary 掛掉怎麼辦?一個 Availability Zone 故障怎麼辦?Queue 暫時不可用怎麼辦?新版本有 Bug 怎麼辦?
不是所有系統都需要 Multi-region Active-active。可靠性越高,成本與複雜度通常也越高,所以應該先從 Business Requirement 決定需要幾個 9。
Google SRE 的可用性表可以清楚看出差異:99.9% 與 99.99% 每年允許的不可用時間,已經從數小時縮到不到一小時。可靠性目標越高,架構與營運投入也會明顯增加。
RPO 與 RTO:備份不是有做就好
Disaster Recovery 至少要回答兩個問題:
- RPO:最多可以接受遺失多久的資料?
- RTO:故障後最多可以接受多久才能恢復?
如果 RPO 是 24 小時,每天備份一次可能足夠;如果只能接受幾分鐘資料損失,就需要完全不同的資料複寫策略。
沒有先定義 RPO/RTO,無法判斷目前的 Backup 和 Failover 到底夠不夠。
Observability 要在設計階段就進架構圖
系統上線後才補 Monitoring,通常會發現很多重要資訊根本沒有被記錄。
Google SRE 對服務監控提出四個 Golden Signals:Latency、Traffic、Errors、Saturation。
這四個訊號可以幫助回答:系統現在有多慢、承受多少需求、失敗多少,以及資源是否快到極限。
實務上還會搭配 Metrics、Logs、Tracing,讓問題從「網站變慢了」一路追到是哪個 Service、哪一段 Query 或哪個下游依賴。
Alert 不是越多越好
Monitoring 收集資料,Alert 則應該對應需要採取行動的狀況。
如果每個 CPU 尖峰都叫醒人,最後大家只會學會忽略通知。Google SRE 強調,Pager 類型的警報應該簡單、可靠,而且代表人現在真的需要處理。
這也是 System Design 的一部分:不只設計服務怎麼跑,也要設計故障發生時團隊怎麼知道、怎麼回應。
Deployment Strategy 也是 System Design
架構圖畫得再漂亮,如果每次 Deploy 都是一次把所有流量切到新版本,風險仍然很高。
常見策略包括 Rolling、Blue-Green、Canary。
Google SRE 也強調 Rollout 應該被監控;如果出現異常,優先 Rollback,再進一步診斷,以降低使用者受到影響的時間。
Security 和 Cost 不能等架構完成後才補
架構決策同時會改變 Attack Surface 與成本。
多一個公開 Service、多一個跨區 Replication、多一套 Queue 或 Cache,都意味著新的權限、加密、密鑰管理、監控與費用。
AWS Well-Architected 把 Security、Reliability、Performance Efficiency、Cost Optimization、Operational Excellence 等分成不同支柱,就是提醒設計不能只追求單一指標。
架構決策通常是「在目前 Business Requirement 下,哪個取捨最合理」。
Microservices 不是大型系統的起點
很多團隊把「可以擴充」直接等同於 Microservices。
但服務拆分會增加 Network Failure、Distributed Tracing、Deployment、Versioning、資料一致性與團隊協調成本。
如果單體系統仍然能清楚維護、部署速度正常、團隊沒有明確邊界需求,Modular Monolith 往往更簡單。
當服務邊界和團隊邊界開始互相影響時,可以延伸看Team Topologies:組織怎麼分隊,最後就會長出什麼樣的系統。
每加一個元件,都要寫出它解決什麼,以及新增什麼成本
| 元件 | 主要解決 | 新增成本 |
|---|---|---|
| Load Balancer | 流量分配、冗餘 | 健康檢查、Session、設定複雜度 |
| Cache | 降低延遲與 Database 負載 | 失效策略、資料一致性 |
| Queue | 非同步、削峰、解耦 | 重試、順序、重複處理 |
| Read Replica | 讀取擴充 | Replication Lag |
| Sharding | 資料與寫入水平擴充 | 跨 Shard Query、重新分片 |
| Multi-region | 區域故障與全球延遲 | 資料同步、成本、衝突處理 |
Design Review 要留下 Assumptions 和 Trade-offs
一份好的設計文件,不只畫最後答案,也要留下假設。
例如「預估兩年內流量不超過某個級距,所以暫不 Sharding」「目前只有單區需求,因此先採 Multi-AZ,不上 Multi-region」。
未來條件改變時,團隊就知道哪些決策需要重看,不必重新猜當初為什麼這樣做。
一個可以重複使用的 System Design 流程
- 定義 Functional Requirements。
- 定義 Non-functional Requirements 與 SLO。
- 估算 QPS、Storage、Bandwidth、Peak Load 與 Headroom。
- 畫最小 High-level Design。
- 找出 Bottleneck、Failure Domain 與 Single Point of Failure。
- 只對需要的位置加入 Cache、Queue、Replica、CDN 或 Sharding。
- 定義 Consistency、RPO、RTO 與 Degradation Strategy。
- 定義 Observability、Alert 與 Capacity Signals。
- 定義 Deployment 與 Rollback。
- 最後列出 Trade-offs、Assumptions 與下一階段才需要做的改善。
常見問題
System Design 一定是百萬使用者才需要嗎?
不用。任何需要考慮容量、可靠性、資料、部署與維運的系統都需要設計,只是規模小時架構可以簡單很多。
什麼時候需要加 Cache?
當讀取路徑重複、原始資料來源較慢,而且可以接受短暫不一致時,Cache 才比較有價值。不要只因為大型系統常見 Redis 就先加入。
什麼時候需要 Message Queue?
當工作不需要在同一個同步 Request 完成、需要吸收尖峰,或 Producer 和 Consumer 應該降低耦合時,可以考慮 Queue。
什麼時候需要 Sharding?
當單一 Database 節點經過 Query、Index、Vertical Scaling、Replica、Cache 等改善後,資料量或寫入量仍超過能力,才值得承擔 Sharding 的複雜度。
高可用是不是越高越好?
不是。可用性越高通常代表更多冗餘、複寫、監控與營運成本。應該依 Business Impact 決定合理 SLO,而不是所有系統都追求五個 9。
System Design 最重要的能力是什麼?
核心能力是把需求量化、找到瓶頸、選合適元件,並清楚說明每個選擇帶來的 Trade-off;背架構圖本身幫助有限。
好的 System Design,也知道哪些東西現在還不用做
大型系統設計困難的地方,往往在於判斷何時該用某項技術、何時暫時不需要。
先從需求、規模與失敗成本開始,畫出最簡單能工作的系統;只有當 Load、Reliability、Latency、Data Consistency 或 Team Boundary 出現具體壓力,再引入下一層架構。
這樣做出來的系統不一定看起來最複雜,卻更容易理解、營運、除錯與演進,也更能清楚回答:每一個元件,到底是為了解決什麼問題。
參考資料
AWS:Well-Architected Framework。
Microsoft:Azure Architecture Center;Azure Well-Architected Framework。
Google:Site Reliability Engineering,Monitoring Distributed Systems、Production Services Best Practices。
Martin Kleppmann:Designing Data-Intensive Applications。
Alex Xu:System Design Interview。
