System Design:大型系統設計不是先畫架構圖,而是先搞清楚需求與取捨

System Design(系統設計):先確認功能需求、規模、延遲、可用性、資料一致性與成本限制,再用容量估算找出主要瓶頸,最後選擇適合的 Load Balancer、Cache、Queue、Database 等元件並清楚說明取捨。

談大型系統設計時,最容易犯的錯,是還沒搞清楚問題,就急著畫架構圖。

「先放 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:

  1. 先查 Cache。
  2. 沒有命中,再查 Database。
  3. 把結果寫入 Cache。
  4. 下次直接從 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 等問題帶進來。

比較穩健的順序通常是:

  1. 先確認 Query 與 Index 是否合理。
  2. 垂直升級到合理範圍。
  3. 讀取壓力高時考慮 Read Replica。
  4. 用 Cache 降低重複讀取。
  5. 資料量或寫入量真的超過單一節點能力時,再考慮 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 流程

  1. 定義 Functional Requirements。
  2. 定義 Non-functional Requirements 與 SLO。
  3. 估算 QPS、Storage、Bandwidth、Peak Load 與 Headroom。
  4. 畫最小 High-level Design。
  5. 找出 Bottleneck、Failure Domain 與 Single Point of Failure。
  6. 只對需要的位置加入 Cache、Queue、Replica、CDN 或 Sharding。
  7. 定義 Consistency、RPO、RTO 與 Degradation Strategy。
  8. 定義 Observability、Alert 與 Capacity Signals。
  9. 定義 Deployment 與 Rollback。
  10. 最後列出 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。

元氣站長
元氣站長

經營網路事業,賺取持續收入!
網路賺錢修煉,成就理想人生!