Skip to main content

Command Palette

Search for a command to run...

System Design Interview Ch 1

Updated
14 min readView as Markdown
System Design Interview Ch 1

前言

這本書的重點面試中關於系統設計問題的環節。因為系統設計沒有範圍沒有固定模式,什麼都能提出來討論跟討論,問題範圍太大,整個過程非常開放,且幾乎沒有標準答案。

所以在這過程中,主要考察的是溝通與問題解決能力。評估面試者如何分析與拆解問題,並且與面試官說明想法與討論。

系統設計問題是開放式的。就像真實世界一樣,系統之間存在各種差異與變體。期望的結果是提出一個能達成系統設計目標的架構。討論方向會依面試官有所不同:有人會涵蓋所有層面的高階架構,也有人會挑出一兩個領域深入鑽研。一般而言,系統需求限制(constraints)與瓶頸(bottlenecks)必須被充分理解,才能引導雙方後續的討論方向。

小弟的心法

  1. 不急著畫架構——先問清需求與假設。

  2. 每個決策都能說明「為什麼不是另一種」。

  3. 面試更像協作設計會議,而非單人獨白。

  4. 有條理比「炫技」重要;框架 + 邏輯推進 > 記憶片段。

討論流程

  1. 通用流程框架(需求 → 容量估算 → 高階架構 → 核心元件 → 資料流 → 優化 → 風險 & 改進)

  2. 主題化整理(儲存、Cache、索引、排程、排隊、監控)

Ch1 從 Zero 擴展到支援 Millions 的使用者

一個系統能從支援在線幾百人、幾千人、幾萬人、幾百萬人,這是一個演化的過程,幾乎不太能一步到位的。基本都從支援極少的使用者開始設計,再逐步擴展到能服務百萬使用者。

單一伺服器架構(Single Server Setup)

打造一個複雜系統也是如此。都是從最簡單開始:所有東西都跑在同一台伺服器(web service、資料庫、cache service 等全部同一主機運行。)上。目標是完成業務功能與驗證。

請求與回應

為了理解這個架構,先看「Request 流程」與「Traffic 來源」。

Request 流程︰

  1. 使用者透過網域名稱(例如:api.mysite.com)存取網站。通常 DNS 是第三方的付費服務(例如 Route53, Cloudflare DNS…),而不是由我們自己的伺服器託管。

  2. DNS 解析後會將 IP 位址回傳給瀏覽器或手機應用。在例子中,回傳的 IP 是 15.125.23.214。

  3. 一旦取得 IP 位址,瀏覽器或行動 App 會直接向你的 Web 伺服器送出 HTTP request。

  4. Web 伺服器回傳 HTML 頁面或 JSON 回應,供前端渲染。

接著看流量來源︰到達你 Web 伺服器的流量來自兩種來源:Web 應用與 App 應用。

  • Web 應用:使用Server side language(例如 Java、Python 等)處理商業邏輯、資料儲存等,再用前端技術(HTML、JavaScript)呈現。

  • 行動應用:行動 App 與 Web 伺服器之間以 HTTP 協定溝通。

不論哪種 Web service 都要提供 Web API 例如 GET /users/12 請求.取得 JSON 內容

{
  "id": 12,
  "firstName": "xxx",
  "lastName": "yyy",
  "address": {
    "city": "Taipei",
    "postalCode": "100"
  },
  "phoneNumbers": [
      "23939889",
      "8825252"
   ]
}

分層,將資料庫獨立出去

隨著使用者數量逐漸成長,一台主機不夠用了,我們需要多台:一台處理流量,另一台作為資料庫。將流量(Web Tier)與資料庫(Data Tier)分離,可以各自獨立擴展。

要用哪種資料庫?

關聯式資料庫(RDBMS) vs 非關聯式資料庫(NoSQL)

  • RDBMS的資料以「表格 + 列」方式儲存,你可以用 SQL 跨多表做 join

  • NoSQL 主要分四大類:Key-Value、Graph、Column、Document。一般不支援 join

    • 這幾乎意味著資料是非結構化,或沒有明確關聯。

SQL vs NoSQL Database

關聯式(SQL) vs 非關聯式(NoSQL)深度比較

維度關聯式 RDBMSNoSQL(概括)適合情境
資料模型嚴謹結構(Schema)彈性 / 半結構 / 無 Schema需求變動快用 NoSQL
查詢能力強大(JOIN, GROUP BY)依類型差異大:KV 最弱、Document 中等、Graph 強多維複雜查詢 → RDBMS
交易(ACID)原生支援多數弱化(但有改良,如 DynamoDB 的條件寫入)金融 / 訂單 → RDBMS
一致性強一致(可調整隔離層級)多採最終一致(Eventual Consistency)高可用 + 容忍延遲一致
可擴展性垂直為主(水平需要分片)多為天然水平擴展(尤其 KV / Column)快速擴容量 → NoSQL
延遲寫讀都穩定,但會因 JOIN 複雜度上升KV / Column 可極低延遲Ultra 低延遲 → NoSQL
生態成熟度工具、多數工程師熟悉生態片段化團隊熟悉度低 → 先選 RDBMS
成本/維運單體易維護,scale 後複雜分散式一致性/備援需心智成本人力不足 → 先簡單

NoSQL 四大類型何時選?

類型代表優勢典型場景
Key-Value StoreRedis, DynamoDB超低延遲、簡單伸縮Session、快取、購物車
Document StoreMongoDB, CouchDB半結構 JSON、欄位可變CMS、使用者設定、活動資料
Column Family StoreCassandra, HBase寫入量大、時間序列、寬表Log、IoT、時間序列聚合
Graph StoreNeo4j, JanusGraph圖遍歷、關係跳數社交圖、推薦、欺詐關聯

如果你需要:

  • 頻繁 schema 變化 → Document

  • 海量寫入(append-only style)→ Column

  • 超快單 key 存取 → Key-Value

  • 多跳關係(friend of friend)→ Graph

常見「誤解」與反例

誤解說明更好思路
高併發就一定要 NoSQL先看 Query Pattern + 快取命中加 Cache + 調索引可能足夠
NoSQL 不需要設計資料模型模型差仍導致查詢/儲存浪費瞭解訪問模式後再定文件結構
RDBMS 不可水平擴展可:分片 / 分區表 / 多租戶設計成本是操作複雜性
Join 不支援 = NoSQL 一定快可能需要在應用層多次 round-trip資料壓平(denormalize)需衡量更新成本

結構化與非結構化

什麼是「結構化(Structured)」資料?

  • 欄位(Column)固定、類型明確(INT、VARCHAR、DATETIME…)、行(Row)格式一致。

  • 易於使用標準化查詢(SQL)

  • 可建立索引、加約束(UNIQUE、FOREIGN KEY、CHECK)

  • 資料品質高,可做強一致交易(ACID)

  • 本質:Schema-on-write(寫入時即驗證格式)

半結構(Semi-Structured)資料

  • 有基本層次(Hierarchy)與標記(Markers / Tags),但欄位可變;每筆資料不必完全相同。

    • 資料本身以「樹狀 / 巢狀(Nested)」方式呈現,而不是像關聯式資料表那樣固定為二維(rows × columns)。

    • 標記(Markers / Tags)指的是附著在資料上的「自描述結構資訊(Self-describing Metadata)」,幫助系統或人來解析這份資料(白話文即欄位名稱)。

  • 例如︰JSON、XML、YAML、Parquet

  • 常見的有Document Store(MongoDB)、Data Lake(查詢引擎 Presto / Athena)、Columnar files(Parquet 供 OLAP)

  • Schema 演進成本低,因為欄位名稱與層次一起存放於記錄裡:

    • 新增欄位:新文件直接加;舊文件沒有也不報錯

    • 移除欄位:新文件不再寫;舊文件仍保留歷史值

    • 讀取的程式可以設計成「有則解析,無則給預設值」(能參考 Martin Fowler 的 Tolerant Reader

  • 更貼近業務事件自然結構

非結構化(Unstructured)資料

  • 缺乏可預測欄位結構,系統很難直接用行列表示語意。

  • 例子:影片、圖片、音訊、自然語言文件(純文字)、PDF。

  • 通常都放 AWS S3、或自己建立檔案系統(FTP)這類的。

  • 設計上會搭配「Metadata」用來建立索引(例如:影像寬高、上傳者、標籤)

  • 本質︰Schema-on-read(讀取 / 分析時才賦予結構)

為何區分這三者?

面向結構化半結構非結構化
可查詢性立即強需索引策略需先萃取特徵
驗證寫入即驗證可選(驗或放寬)幾乎無(靠外部流程)
模型演進嚴謹、成本高中等重度依賴額外處理層
典型操作OLTP混合(OLTP + Event)ETL / 分析 / AI 推論
索引策略B-Tree、Hash二級索引、多鍵Metadata / 向量索引
分析工具成熟度高(日益成熟)需前處理

不同資料型態 → 不同儲存與存取模式,而非一刀切

Join 的「表面意義」與「深層意義」

表面意義(使用層)

JOIN 是把分散在不同表格中的相關資料「在查詢時動態關聯」起來,例如:

  • users(使用者)

  • orders(訂單)

  •       SELECT u.name, o.amount FROM users u JOIN orders o ON u.id = o.user_id
    

深層意義(設計層)

JOIN 存在的根本原因:你「選擇將資料 Normalization」以:

  1. 減少冗餘(避免資料重複)

  2. 降低更新異常(Update / Delete / Insert anomalies)

  3. 維持一致性(例如使用者名稱更新只改一處)

JOIN 就是「查詢時把拆散的資訊重新拼起來」的計算成本。

「為什麼要拆?拆開後怎麼正確表達『包含』語意?」、怎麼辨別「擁有 (ownership)」 vs 「參考 (reference)」 vs 「成員 (membership)」。

層面說明關鍵洞察
表面(操作層)SQL 語法:動態關聯多表資料「拼接」分散的資訊片段
深層(設計層)正規化的代價:查詢時重組被拆散的業務實體「為什麼要拆?拆了如何表達完整語意?」

正規化 ↔ JOIN 的權衡循環


業務實體 → 正規化拆表 → 減少冗餘 → 需要 JOIN 重組 → 查詢成本 → 考慮反正規化
    ↑                                                                    ↓
    ←←←←←←←←←← 效能 vs 一致性的永恆拉鋸 ←←←←←←←←←←←←←←←←←←←←←←←

什麼是「包含」語意?

包含」(X contains Y) 指:X 代表一個聚合整體 (whole),Y 為其組成部分 (parts),Y 的存在或意義與 X 有緊密依附關係(生命週期、識別、權限、保存策略)。

對比:

  • 單純引用 (reference):Post 引用 User(作者),User 不屬於 Post。

  • 成員關係 (membership):Playlist 與 Song,Song 可同時在多個 Playlist。

  • 真正組成/擁有 (composition):Order 與 OrderItem;OrderItem 沒有 Order 就不應存在。

三層視角:語意層 → 結構層 → 實作層

層次問題表達工具
語意層 (Domain)這是擁有?聚合?共享資源?DDD Aggregate、語意語彙
結構層 (Logical / Relational)怎麼拆表與定鍵?主鍵/外鍵、唯一約束、級聯
實作層 (Physical)怎麼索引、分片、避免昂貴 JOIN?索引、分區、快取、反正規化

第一層:語意層(Domain Layer)

核心問題: 這個關係在現實世界中代表什麼?

  • 專注於 業務語意現實世界關係

  • 定義四種關係類型:擁有/組成、引用、成員、依賴

  • 決定資料的 生命週期耦合程度

關係類型特徵範例設計含義
擁有/組成 (Composition)強依附、共生命週期Order ← OrderItem應聚合設計、級聯操作
引用 (Reference)弱耦合、獨立存在Post → User (author)外鍵約束、但不級聯刪除(生命週期不同)
成員 (Membership)多對多、共享資源Playlist ↔ Song中介表、獨立生命週期
依賴 (Dependency)單向依賴、可替換Order → PaymentMethod可能需要歷史快照

第二層:結構層(Logical/Relational Layer)

核心問題: 如何用關聯式模型正確表達語意?

  • 將語意轉換為 資料庫結構

  • 設計主鍵、外鍵、約束和級聯規則

  • 確保資料的 完整性一致性

語意主鍵設計外鍵約束級聯規則唯一性
組成關係複合鍵 (parent_id, seq)NOT NULL + FKON DELETE CASCADE子項在父項內唯一
引用關係獨立主鍵FK (可 NULL)NO ACTION / RESTRICT全域唯一
成員關係中介表雙 FK雙向 FKSET NULL組合唯一

第三層:實作層(Physical Layer)

核心問題: 如何在效能與一致性間取得平衡?

  • 針對 效能需求 選擇實作策略

  • 在一致性與效能間做 權衡

  • 考慮索引、分片、快取等技術手段

策略適用場景優勢代價
標準 JOIN中等規模、高一致性需求即時、準確跨表查詢成本
反正規化讀多寫少、可接受最終一致單表查詢快更新複雜、儲存冗餘
物化視圖複雜聚合、定期刷新可接受查詢極快刷新延遲、儲存倍增
應用層 JOIN分片環境、微服務架構靈活路由網路往返、程式複雜
事件溯源高變更頻率、需完整歷史可重放、審計完整查詢需重建狀態

包含語意」判斷框架

快速評估表(7 個維度)

維度組成關係 (Composition)引用關係 (Reference)
獨立價值子項無獨立商業意義被引用方有獨立價值
生命週期與父項同生共死獨立的創建/銷毀
權限繼承繼承父項權限有自己的權限體系
數量特性可預期上限(1-100s)可能無上限或極大
共享性專屬於單一父項可被多方引用
查詢模式通常與父項一起查詢(跟第一點一樣意思)經常獨立查詢
一致性要求強一致性可接受最終一致

判斷結果 → 設計策略

評估結果推薦策略實作要點
5+ 個「組成」特徵聚合根設計複合主鍵、級聯刪除、事務邊界
3-4 個「組成」特徵強外鍵約束NOT NULL FK、部分級聯
2 個以下「組成」特徵引用設計獨立主鍵、軟約束、可 NULL

RDBMS 中的「包含」表達手法

常見實作:

  1. 父表 + 子表(FK NOT NULL + ON DELETE CASCADE)

  2. 子表複合主鍵 (parent_id, line_no) 強化「從屬」語意

  3. 有些小且穩定的子集合(例如 User 的設定集)可用 JSON/ARRAY 欄位(減少 JOIN)

  4. 視圖 / 物化視圖 將常用父子聚合預先展開(讀快、寫複雜)

NoSQL(以Document DB為例)對應模型

建模方式語意優點風險
嵌入 (Embed)強組成、一起讀單查取整體、少網路往返、單文件原子性子集合無上限會膨脹;部分更新成本高;熱點文件鎖
參考 (Reference)弱關聯 / 重用文件小、子項可獨立成長需要多次 round-trip 或應用層 Join;一致性自己管
預先展開 (Denormalize Copy)為讀優化查詢快重複數據同步難
預計算 / 物化文件聚合快取讀非常快延遲一致 / 重建流程必要

MongoDB 有 $lookup 但非傳統 cost-based optimizer;Cassandra 無 JOIN(需建反向查詢表)。

何時在 NoSQL「嵌入」 vs 「引用」

嵌入 (Embed) 適用:

  • 子項集合大小「小且有上限」(例如 <= 50 或數百內)

  • 讀操作幾乎總是「父 + 全部子」一起要

  • 子項更新頻率低或批次

  • 需要單文件原子性

  • 不會被多個父共享

  • 無需跨子項複雜查詢(例如不單獨以子項條件大範圍搜尋)

引用 (Reference) 適用:

  • 子集合「可能無上限」或高速成長(留言、日誌)

  • 子項要被多個父使用或交叉分析

  • 子項更新頻率高 / 局部更新為主

  • 需要基於子項條件分頁、排序、統計

  • 權限或壽命不完全跟父同步

  • 需要避免文件熱點(高併發寫同一父)

常見錯誤對應

誤解風險建議
看到「包含」就一律在 RDBMS 拆表過多 JOIN、過早複雜化子集合小且不查詢可考慮 JSON
NoSQL 一律嵌入文件爆炸、熱點寫鎖監控文件大小/寫頻率,必要拆
以為引用 + 應用層 join 等同 RDBMS JOIN 成本多 round-trip 延遲高用批量查詢 / pipeline / 預計算
任意反正規化未設同步策略資料漂移不一致有來源版本號 / 重建流程
Mongo $lookup 當成 RDBMS JOIN 大量使用性能不可預期控制資料量,適度寫入時展開

但除了正規化避免冗餘,還常因為它們屬於不同的業務主題(Domain / Subject)或擁有不同的更新頻率、保存期限、合規要求與存取模式;當某個使用場景需要把這些原本獨立演化的資料拼在一起時,就需要 JOIN。

為什麼「不同主題 / 生命週期」會驅動分表?

拆分驅動因素說明常見做法影響 JOIN 需求
主題邊界(Domain Boundary)User、Order、Payment、Inventory 各自業務語意按領域建獨立表(甚至獨立 schema)報表或聚合時跨域 JOIN
更新頻率(Update Velocity)Profile 更新少;交易流水高頻熱表與冷表分離查詢展示時再 JOIN
生命週期 / 保存期限(Retention)訂單主檔保留 7 年;暫存計算只要 30 天歷史表(history/archive)需要歷史 + 現況視圖時 JOIN 或 UNION
安全 / 權限(Security Classification)PII 與一般行為資料分離敏感表設更嚴 ACL需遮罩或脫敏後再 JOIN
存取模式(Access Pattern)OrderItems 高寫入、Product 靜態針對高寫入表獨立調優下單詳情頁面 JOIN
資料粒度(Granularity)Fact(交易事實) vs Dimension(維度)星狀/雪花模型OLAP 查詢多 JOIN
資料版本 / 緩變(Slowly Changing Dimension, SCD)需要保留歷史屬性維度表多版本(Type 2)回放歷史分析需依時間 JOIN
擴展 / 分片策略不同表按不同 key 分片垂直分片 + 水平分片跨分片 JOIN 成本升高

為什麼要「應用層 / 資料層」分離?

面向分離前(單機)分離後(雙層)好處
資源競爭App 與 DB 搶 CPU / RAM / I/O各自獨立效能穩定
擴展模式垂直擴充為主(易到頂)App 可水平加機,DB 可做複寫/調優成本更彈性
可靠性一崩全崩單層故障影響範圍縮小可用性提升
部署風險部署 App 可能拖累 DBApp 可頻繁部署,DB 較少改動DevOps 友善
監控精度難拆指標分 tiers 監控 (QPS, Slow Query, CPU)除錯快

這一步是系統走向 Layered Architecture 的第一個拆分,也是之後能加快取、CDN、讀寫分離、分片的基石。

垂直擴充(Vertical Scaling)與水平擴充(Horizontal Scaling)

垂直擴充(又稱 Scale Up)是指替現有伺服器增加更多資源(例如 CPU、記憶體等)。在流量較低的階段,垂直擴充是一個很好的選擇,其最大優點是簡單(不需修改架構、部署流程;常只是調整雲端實例規格。)。但它也存在嚴重的限制。

  1. 垂直擴充存在硬體上限——不可能無限制地為單一伺服器添加 CPU 與記憶體。

  2. 垂直擴充本身不提供容錯與冗餘能力;若唯一的伺服器故障,整個網站/應用會完全無法服務(會有Single point of failure問題)。

So…在業務有賺錢且請求數才 C10K 上下時,其實垂直擴充還是很不錯的解決方案的。

水平擴充(又稱 Scale Out)則是透過新增多台伺服器進入資源池來提升整體處理能力。在更大請求數的系統中,水平擴充往往是更可取的方案。又因為有多台伺服器所以水平擴充方案通常會導入負載平衡器(Load Balancer)是最合適的技術手段。

負載平衡器(Load Balancer)

負載平衡器會將進入的流量平均(或依特定演算法)分配到設定於「負載平衡伺服器組」中的多台 Web 伺服器。如下圖所示。

使用者直接連到負載平衡器的 Public IP。採用這種架構後,外部用戶無法再直接存取後端 Web 伺服器。為了提升安全性,伺服器之間的通訊方式改用 Private IP。所謂Private IP,是只能在同一網路(例如同一 VPC / 子網)內互相存取的IP address,無法透過公開網際網路被直接訪問。

Load Balnacer 常見的負載平均策略

服務方的服務發現(Server Side Discovery

根據地理位置導流量去不同 Region 的服務

資料庫複寫機制

剛剛提到水平擴展是請求量較大的系統場景時較好的技術方案。又因為大多數場景資料的讀寫比例差異也不小,幾乎都是讀取資料的次數跟筆數遠比寫入修改的高。

因此我們就想著能否將資料庫讀取資料的請求與寫入修改的請求分開,因此有了複寫機制這架構,在這架構中最主要的兩個角色分別是 Primary/Main(資料來源)以及 Replica(資料副本)。

資料庫複寫架構的優點:

  • 效能提升:在複寫模型中,寫入與更新集中於主節點,而讀取請求分散到各從節點,達到並行處理更多查詢的效果。

  • 資料可靠性:若某一資料庫伺服器遭受天災(如颱風、地震)損毀,資料仍保存在其他節點,不致遺失,因為複寫將資料散佈在多個位置(能想成 Git server 與自己local的 Git 副本)。

  • 高可用性:資料跨節點複寫後,即使其中一台資料庫離線,系統仍可向其他節點存取相同資料,服務不致中斷(目前此架構僅針對讀取場景具備高可用性)。

! 提出問題 !

  1. 若系統只有一台 Replica 資料庫而它離線了,此時 client 發出的讀取請求你會怎處理?

  2. 若主資料庫離線呢?

引入 Cache 機制

Cache 的目的將「計算代價高」或「被頻繁存取」的資料結果存放於記憶體,使後續請求能更快回應。每次網頁載入時,通常會觸發一次或多次資料庫查詢(例如取得遊戲排行榜、後台列表清單、取得企業用戶的昨日報表等),反覆打資料庫會顯著影響效能,而快取可減輕此問題。

Caching Strategies

被動式 vs 主動式

被動式︰都是在讀取階段才判斷。

主動示︰資料寫入修改階段跟著作動。

  1. Cache Aside(最常見)

How cache-aside pattern works?

  1. Read through

    令一個是 Write through,有 through 的都是 application 只認識一個端點,由該服務來執行跟資料庫中間的同步。

How read through caching pattern works?

  1. Write Around(小弟個人比較常搭配這個)

How write-around caching strategy works?

使用 Cache 的一些提醒事項

  • 適用於「讀多改少」的資料。

  • Cache 存於Memory,重啟即失,並不適合作為最終持久層。因此關鍵資料必須寫入永久性儲存(如資料庫、物件儲存)。

  • 應設定到期時間。沒有過期機制會導致記憶體永遠被占用。

    • TTL 太短 → 頻繁回源查 DB

    • TTL 太長 → 資料可能陳舊失真。需依一致性與負載取平衡。

  • 資料一致性問題,需維持主資料存放(DB)與快取同步。由於更新快取與更新資料庫通常非同一原子交易,可能產生不一致。多區域部署時(跨資料中心)一致性更難

  • 淘汰(Eviction)策略:當快取已滿,新加入資料會觸發舊資料被移除,稱為「快取淘汰」。最常見策略為 LRU(最近最少使用)。其他如 LFU(最少使用頻率)、FIFO(先進先出)等,可依使用情境選擇。

  • 故障緩解:單一快取伺服器可能成為單點故障(SPOF)。建議多台、甚至跨資料中心部署以避免全失。另可超額配置記憶體(預留成長緩衝),降低突增導致的淘汰壓力。

CDN

CDN(內容傳遞網路)是由地理上分散的伺服器所組成,用來加速傳送靜態內容。CDN 節點會快取圖片、影片、CSS、JavaScript 等靜態資源。

使用者造訪網站時,離他最近的 CDN 節點提供靜態資源。直覺上,使用者距離 CDN 節點越遠,載入越慢。例如節點在舊金山,洛杉磯使用者會比歐洲使用者獲得更快回應。

使用 CDN 的常見注意事項

Stateful vs Stateless

之前都是考慮資料層的分層、擴容。但 Web/API Server ,一些應用服務 呢?

如果我們想把 Web/API server 做水平擴充。要達成這點,必須把狀態(例如:使用者 Session 資料)移出 Web 層。好的做法是把 Session 資料(常見的狀態之一)存放在持久化或共享型儲存(例如:關聯式資料庫或 NoSQL)。叢集中的每一台 Web 伺服器都能從該資料庫存取狀態資料。這種設計稱為「stateless Web 層 (stateless web tier)」。

問題在於:同一個客戶端的每次請求都必須導向同一台伺服器。多數負載平衡器可以透過「 Session (sticky sessions)」達成,但這會帶來額外開銷。以這種方式擴充或縮減伺服器更困難,也較難處理伺服器故障。

討論︰為什麼stateful server 我們即使擴充或縮減伺服器,書上也是說困難?

下圖是採用無狀態 Web 層後的更新設計。在這種 stateless 架構中,使用者的 HTTP 請求可以被送往任意 Web 伺服器;這些伺服器會到共享資料儲存取回狀態。狀態資料放在共享儲存(該儲存可以是RDBMS、Memcached / Redis、或 NoSQL 等。)中,而不是留在 Web 伺服器。stateless 系統更簡單、更穩健且易於擴充(僅限於方便擴充 Web server)。

「狀態」是什麼?

在這裡的狀態指的是:
為了正確處理使用者接下來的請求,某台特定伺服器必須保存在自己本地(記憶體/本地檔案/連線)的資料或上下文
只要「下一個請求必須回到同一台伺服器才能正常運作」,那份資料就是造成有狀態的因素。

簡化判別句:
如果我把這台 Web 機器砍掉重建,使用者的體驗或流程會不會壞?會 → 那些就是『狀態』。

狀態常見分類

類型說明是否常見造成黏著
使用者身份 / 認證狀態Session、登入 token、權限上下文
使用者互動流程進度多步驟表單進度、購物車、結帳流程
使用者偏好 / UI 狀態語系、佈局模式、A/B variant若存在伺服器本地就會
暫存的業務資料尚未提交的草稿、臨時計算結果、購物車:若存放在某機器記憶體,請求換機器就消失、股票撮合服務 Orderbook 上掛的買賣單
Cache本機計算結果 / 熱資料一般屬「軟狀態」,但設計不當會依賴
檔案暫存上傳中碎片、影像處理中間檔案
連線層狀態WebSocket 連接對應的使用者、推播訂閱是(如綁在單機記憶體)
排程 / 工作佇列內記錄In-memory job queue / delayed tasks、背景批次 Job 進度
Rate limiting / 計數器以記憶體紀錄使用者請求數 Map<IP, counter>是(各機不一致導致錯誤限制)
防重 / Idempotency 狀態保存在單機的紀錄是否已處理某訂單/交易是(若僅存在本機)
Feature Flag 解析結果每個使用者被分派的組別是(若本機才知)
TLS / 加密交握 / session ticket傳輸層協商(通常由 LB 處理)通常不視為應用黏著
ML 推論上下文使用者個別暫存特徵向量可能

「軟狀態 vs 硬狀態」

類別定義失去後影響例子
硬狀態 (Authoritative)系統必須正確保存遺失 = 資料錯誤/使用者體驗壞Session、交易紀錄、購物車
軟狀態 (Reconstructable)可以重新計算或重抓遺失 = 暫時變慢 / 重新生成Cache、預算好的推薦結果

建議:

  • 硬狀態永遠不要只放在單機記憶體。

  • 軟狀態可放本機:但要接受可隨時清空 (cache invalidation OK)。

什麼情況「不必過度追求完全無狀態」?

條件可接受局部本地狀態
早期原型、單節點快速迭代比彈性更重要
本地緩存的這些資料不是系統的「authoritative source」,而是從別地方的資料加工、計算、查詢、組合後得到的暫存結果。清掉僅造成效能下降
臨時開發工具介面非生產流量

Message Queue

Message Queue 是一種支援非同步通訊的「具備持久性(durable)」元件,同時以記憶體為基礎進行暫存。它扮演 buffer 的角色(消峰),分散並傳遞非同步請求。

其基本架構相當簡潔:輸入端服務(稱為 Producer / Publisher)產生訊息並發佈到佇列;其他服務或伺服器(稱為 Consumer / Subscriber)連接到佇列,依訊息所定義的內容執行對應動作。

解耦(Decoupling)」特性使訊息佇列成為構建可擴展且可靠應用的常用架構。使用訊息佇列時,當消費者暫時無法處理請求,生產者仍然可以把訊息送進佇列;反之,即使生產者暫時離線,消費者仍能從佇列中取出既有訊息處理。Decoupling 白話的說,生產者不必管有沒有消費者,也不用管消費者是誰。消費者也是如此。

資料庫的擴展

最簡單的是垂直擴展(Vertical Scaling),前面講過了不再提。

而資料庫也有水平擴展(Horizontal Scaling),又稱「分片(Sharding)」,是透過新增更多伺服器來擴充。分片是把大型資料庫切成較小且更易管理的單元(稱為「分片」)。各分片共享相同 Schema,但存放的實際資料互不重複。

圖示範一個分片資料庫:使用者資料依使用者 ID 分配到不同伺服器。每次存取資料都會用雜湊函式尋找對應分片。本例採用 user_id%4 作為hash:結果為 0 則用 shard 0;為 1 則 shard 1;其餘類推。

實作分片策略時最重要的考量是「分片鍵」(Sharding Key,又稱 Partition Key)。分片鍵由一或多個欄位組成,決定資料如何分佈。良好的分片鍵可讓查詢正確路由到對應資料庫以有效存取。關鍵準則之一是能「均勻分散」資料(避免 hot spot)。

Sharding 是有效擴展資料庫的技術,但並非完美,會引入新的複雜度與挑戰:

  • 資料再分片(Resharding):在以下情況需要:1) 單一分片因成長過快已放不下。2) 資料分佈不均造成部分分片較快「耗盡」(容量或效能)。此時須更新分片函式並搬遷資料。第 5 章將提到的一致性雜湊(Consistent Hashing)是常用解法。

  • 名人問題(Celebrity Problem / Hotspot Key):過多請求集中於特定分片導致過載。若多位超高人氣帳號落在同一分片,其讀取壓力會壓垮伺服器。可能需為每位名人單獨配置分片,甚至再細分。

  • Join 與去正規化:跨分片進行 Join 困難,常見替代作法是「去正規化」,讓查詢可在單表完成。

Sharding 架構下的 JOIN 挑戰

挑戰分類

挑戰類型具體問題影響程度
跨分片 JOIN資料分散在不同物理節點高:需網路聚合
分片鍵不一致關聯表用不同分片策略極高:可能全掃描
聚合計算SUM、COUNT 需跨分片合併中:可並行處理
排序分頁ORDER BY + LIMIT 跨分片複雜高:需全域排序

解法對照

解法適用場景實作複雜度效能影響
應用層 JOIN簡單關聯、可控資料量多次網路往返
分片鍵對齊強關聯實體(如 User-Order)可能分佈不均
反正規化讀多寫少、可接受冗餘高(同步機制)讀快寫慢
CQRS 分離複雜查詢 vs 簡單寫入查詢極快
搜尋引擎全文檢索、複雜篩選近即時延遲
資料倉儲分析型查詢、歷史資料批次延遲

實務案例分析

案例 1:電商訂單系統

實體關係語意判斷設計決策理由
Order ← OrderItem組成關係複合主鍵 (order_id, line_no)OrderItem 無獨立意義
Order → User引用關係獨立 FK,不級聯User 有獨立生命週期
Order → Product引用關係FK + 快照欄位Product 可能變更,需歷史記錄

案例 2:社交媒體平台

實體關係語意判斷設計決策理由
Post ← Comment組成關係FK + 級聯刪除Comment 依附於 Post
Post → User引用關係FK,不級聯User 獨立存在
User ↔ User (Follow)成員關係中介表 follows多對多關係

案例 3:分片環境挑戰

場景問題解法權衡
跨用戶統計無法按 user_id 分片定期 ETL 到分析庫延遲 vs 效能
商品搜尋商品與訂單不同分片策略Elasticsearch 同步一致性 vs 查詢能力
實時推薦需要用戶行為 + 商品資訊Redis 快取 + 應用層合併記憶體成本 vs 延遲

監控與優化指標

層面關鍵指標告警閾值建議優化方向
JOIN 效能查詢執行時間、掃描行數P95 > 500ms索引優化、查詢重寫
跨分片成本網路往返次數、資料傳輸量單查詢 >3 次往返批次查詢、快取
一致性延遲反正規化同步延遲\>10 秒事件驅動、並行處理
快取命中率JOIN 結果快取效率<80%快取策略調整

總結

架構演進路線

場景特徵推薦方案關鍵考量
QPS < 1K, 資料 < 1GB單機簡單優先
QPS 1K-10K, 讀多寫少讀寫分離 + 快取成本效益
QPS > 10K, 多地域CDN + 分片延遲與一致性
// 使用 pg_database_size() 看資料庫的資料大小
SELECT 
    datname as database_name,
    pg_size_pretty(pg_database_size(datname)) as size
FROM pg_database
WHERE datname = 'your_database_name';

要怎知道讀多寫少? 或者啟用 Metrics 做監控。

-- 查看資料庫整體讀寫統計
SELECT 
    datname as database_name,
    tup_returned as total_reads,      -- 總讀取行數
    tup_fetched as fetched_reads,     -- 實際取得行數  
    tup_inserted as total_inserts,    -- 總插入行數
    tup_updated as total_updates,     -- 總更新行數
    tup_deleted as total_deletes,     -- 總刪除行數
    (tup_inserted + tup_updated + tup_deleted) as total_writes,
    CASE 
        WHEN (tup_inserted + tup_updated + tup_deleted) = 0 THEN 'READ_ONLY'
        ELSE ROUND(
            tup_returned::numeric / 
            (tup_inserted + tup_updated + tup_deleted)::numeric, 
            2
        )
    END as read_write_ratio
FROM pg_stat_database 
WHERE datname = current_database();

More from this blog

Get Your Hands Dirty on Clean Architecture CH5 Use Case

書本連結 前言 會挑這本書看的人,應該都是關心自己負責專案的軟體架構的人。不只希望開發的軟體能滿足客戶的明確需求,也希望能滿足可維護性(maintainability)的隱性需求,以及自己對結構與美觀習慣的要求。 要能滿足上述這些要求很難,因為專案通常不會按照計畫進行。可能變因有 deadline,最後的 API 與承諾的不同,又或者我們的設計無法很好的貼合需求的變化所需。。因此完美的架構只有在一

Jul 2, 20264 min read40
Get Your Hands Dirty on Clean Architecture CH5 Use Case

Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南

稟告主公:此乃司馬懿進呈之兵書,詳解如何以 OpenTelemetry 陣法,令臥龍神算之一舉一動盡在掌握,知糧草消耗、察兵器效能、辨戰報異常,使主公運籌帷幄於大帳之中。 為何需要斥候情報? 司馬懿稟告主公: 臥龍神算(Claude Code)乃當世利器,然若無斥候回報,主公便如蒙眼行軍——兵器耗損幾何、糧草消費幾許、哪路斥候出了差錯,一概不知。臣以為,此乃兵家大忌。 無情報之弊,有四: 軍

Feb 19, 202610 min read254
Claude Code 監控秘錄:OpenTelemetry(OTel/OTLP)實戰指南
M

MicroFIRE

74 posts