【系統設計 20 概念】#03 擴展與效能 — Scaling、Load Balancer、CDN、Caching

測驗:擴展與效能 — Scaling、Load Balancer、CDN、Caching

共 5 題,點選答案後會立即顯示結果

1. Vertical Scaling(垂直擴展)的主要缺點是什麼?

  • A. 需要修改程式碼才能運作
  • B. 費用一定比水平擴展高
  • C. 有硬體上限且存在單點故障風險
  • D. 需要搭配 Load Balancer 才能使用

2. CDN 最適合用來快取哪一類資源?

  • A. 使用者的購物車內容
  • B. 圖片、CSS、JavaScript 等靜態檔案
  • C. 即時聊天訊息
  • D. 使用者個人資料頁面

3. Load Balancer 使用 Least Connections 策略時,新的請求會被分配到哪裡?

  • A. 最近一次處理完請求的伺服器
  • B. 依照固定順序輪流分配
  • C. 根據使用者 IP 決定的固定伺服器
  • D. 目前正在處理最少請求的伺服器

4. 在下面的 Redis 快取程式碼中,ex=3600 的作用是什麼?

cache.set(f”product:{product_id}”, json.dumps(product), ex=3600)
  • A. 設定快取的最大容量為 3600 筆
  • B. 設定快取在 3600 秒(1 小時)後自動過期
  • C. 設定快取最多被存取 3600 次
  • D. 設定快取的優先級為 3600

5. 在電商網站的完整架構中,CDN、Load Balancer 和 Cache 三者協作後,資料庫實際需要處理的流量大約是原本的多少?

  • A. 約 50%,因為 CDN 攔截了一半的請求
  • B. 約 20%,因為 Load Balancer 分散了大部分流量
  • C. 約 2%,因為 CDN 攔截靜態請求、Cache 攔截大部分資料庫查詢
  • D. 約 10%,因為快取攔截了 90% 的查詢

本篇是「系統設計 20 概念」系列第 3 篇,共 5 篇。
難度:L2-進階 | 前置知識:第 1 篇的網路基礎(IP、HTTP)

一句話說明

當使用者暴增、伺服器撐不住時,我們有四種武器:加大機器、增加機器、把內容放近一點、把結果記起來。


問題場景:雙 11 你的網站掛了

想像你經營一個電商網站,平常每秒 100 個請求,一切正常。

雙 11 來了,每秒 10,000 個請求湧入。你的單台伺服器:

  • CPU 飆到 100%
  • 記憶體用光
  • 資料庫連線數爆滿
  • 使用者看到 502 Bad Gateway

怎麼辦?

這篇要講的四個概念,就是解決這個問題的四把鑰匙。先看一下它們在架構中的位置:

使用者(台北)  使用者(高雄)  使用者(東京)
     |              |              |
     v              v              v
  [ CDN 邊緣節點 — 靜態資源直接回應 ]     ← (3) CDN
     |
     v
  [ Load Balancer — 分配流量 ]            ← (2) Load Balancer
     |         |         |
     v         v         v
  Server A  Server B  Server C           ← (1) Horizontal Scaling
     |         |         |
     v         v         v
  [    Cache 層(Redis)   ]              ← (4) Caching
     |
     v
  [ Database ]

接下來逐一拆解。


概念 1:Scaling(擴展)

一句話翻譯

伺服器扛不住了,要嘛換更大台,要嘛多叫幾台來幫忙。

兩種擴展方式

Vertical Scaling(垂直擴展)= 升級單台機器

升級前:          升級後:
┌──────────┐     ┌──────────────┐
│ 4 CPU    │ →→→ │ 32 CPU       │
│ 8GB RAM  │     │ 128GB RAM    │
│ 500GB SSD│     │ 2TB NVMe SSD │
└──────────┘     └──────────────┘
   舊機器            新猛獸

白話翻譯:把你的筆電換成工作站。CPU 加大、記憶體加多、硬碟換快的。

優點

  • 最簡單,不用改程式碼
  • 不用處理多台機器的協調問題

缺點

  • 有天花板(一台機器最多就那麼大)
  • 單點故障 — 這台掛了,全部掛

Horizontal Scaling(水平擴展)= 多叫幾台來

擴展前:          擴展後:
┌──────────┐     ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Server   │ →→→ │ Server A │ │ Server B │ │ Server C │
└──────────┘     └──────────┘ └──────────┘ └──────────┘
   一台扛            三台分著扛

白話翻譯:搬家太重?不是找一個大力士,而是多叫幾個人一起搬。

優點

  • 理論上無上限(不夠就再加)
  • 一台壞了還有其他台頂著

缺點

  • 程式要能跑在多台機器上(不能把資料存在本地)
  • 需要有人「分配工作」— 這就是下面的 Load Balancer

必看懂 vs 知道就好

概念 重要性
Vertical = 升級單台 必看懂
Horizontal = 加機器 必看懂
Vertical 有天花板 必看懂
Horizontal 需要 stateless 設計 知道就好,遇到再查

面試怎麼問

「你的系統使用者從 1 萬成長到 100 萬,你會怎麼做?」

標準答案起手式:先 Vertical Scaling 到合理上限,然後轉 Horizontal Scaling。


概念 2:Load Balancer(負載均衡器)

一句話翻譯

站在門口的交通指揮,把客人平均分配到不同櫃台。

工作原理

所有請求
    |
    v
┌─────────────────┐
│  Load Balancer   │   ← 「你去 A,你去 B,你去 C」
└─────────────────┘
    |      |      |
    v      v      v
 Server  Server  Server
   A       B       C

沒有 Load Balancer 的時候,所有流量打到同一台 Server。有了它,流量被分散,每台伺服器只要扛 1/3。

常見的分配策略

策略 1:Round Robin(輪流)

請求 1 → Server A
請求 2 → Server B
請求 3 → Server C
請求 4 → Server A   ← 回到第一台,繼續輪
請求 5 → Server B
...

白話翻譯:排隊叫號,一人一次,輪流來。

最簡單的策略。問題是:如果 Server A 已經很忙、Server B 很閒,它不會管,照樣輪。

策略 2:Least Connections(最少連線)

Server A: 正在處理 15 個請求
Server B: 正在處理 3 個請求   ← 新請求送這裡
Server C: 正在處理 10 個請求

白話翻譯:看誰最閒就派給誰。

比 Round Robin 聰明,會看目前各伺服器的負載狀況。

策略 3:IP Hash(依 IP 分配)

使用者 IP: 1.2.3.4hash → 結果: A → 永遠去 Server A
使用者 IP: 5.6.7.8hash → 結果: B → 永遠去 Server B
Code language: CSS (css)

白話翻譯:同一個客人永遠去同一個櫃台。

適用於需要「黏性 Session」的場景(例如使用者登入狀態存在特定伺服器上)。

在程式碼中長什麼樣

你如果用 AI 協助開發,可能會在 Nginx 設定檔裡看到這樣的內容:

upstream backend {
    # 列出所有後端伺服器
    server 10.0.0.1:8080;    # Server A
    server 10.0.0.2:8080;    # Server B
    server 10.0.0.3:8080;    # Server C
}

server {
    listen 80;                           # 對外只開一個入口
    location / {
        proxy_pass http://backend;       # 把流量轉給上面那組伺服器
    }
}
Code language: PHP (php)

逐行翻譯

  • upstream backend — 定義一組後端伺服器,取名叫 backend
  • server 10.0.0.1:8080 — 第一台伺服器的 IP 和 port
  • proxy_pass http://backend — 把進來的請求轉發給那組伺服器(Nginx 預設用 Round Robin)

雲端服務中的 Load Balancer

在實際工作中,你不一定會自己設定 Nginx。雲端平台都有現成的:

雲端平台 服務名稱
AWS Elastic Load Balancing (ELB)
GCP Cloud Load Balancing
Azure Azure Load Balancer

它們做的事情一樣 — 分配流量。差別在有更多進階功能(健康檢查、自動移除壞掉的伺服器等)。


概念 3:CDN(Content Delivery Network)

一句話翻譯

把你的圖片、CSS、JS 複製到全世界各地的伺服器,讓使用者從最近的地方拿。

為什麼需要 CDN

沒有 CDN:
東京使用者 → (海底電纜) → 美國主機 → 回傳圖片
延遲: 200ms+

有 CDN:
東京使用者 → 東京邊緣節點 → 直接回傳圖片
延遲: 20ms

物理距離決定速度。光速是有限的,從東京到美國再回來就是慢。CDN 的解法很直覺:把東西放到離使用者近的地方。

CDN 的運作方式

第一次存取(Cache Miss):
使用者 → CDN 邊緣節點 → 「我沒有這張圖」→ 回源站拿 → 存一份 → 回傳給使用者

第二次存取(Cache Hit):
使用者 → CDN 邊緣節點 → 「我有!」→ 直接回傳
  1. 使用者請求一張圖片
  2. CDN 邊緣節點檢查自己有沒有
  3. 沒有(Miss)→ 去原始伺服器拿,存下來,回傳
  4. 有(Hit)→ 直接回傳,不用問原始伺服器

CDN 適合放什麼

適合(靜態、不常變):        不適合(動態、常變):
✅ 圖片 (.jpg, .png, .webp)  ❌ 使用者個人資料
✅ CSS 樣式檔                 ❌ 購物車內容
✅ JavaScript 檔案            ❌ 即時聊天訊息
✅ 影片串流                   ❌ 股票報價
✅ 字型檔案
Code language: CSS (css)

判斷原則:內容是「給所有人看都一樣」的,就適合放 CDN。

在程式碼中長什麼樣

你在前端專案裡可能會看到 CDN 的引用方式:

<!-- 從 CDN 載入 jQuery(不是從自己的伺服器) -->
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>

<!-- 從 CDN 載入字型 -->
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC" rel="stylesheet">
Code language: HTML, XML (xml)

翻譯:這些檔案不是放在你的伺服器上,而是從 CDN 全球節點載入。使用者會自動連到離他最近的那個節點。

常見 CDN 服務

服務 特色
Cloudflare 免費方案夠用,最多人用
AWS CloudFront 和 AWS 生態整合
Akamai 老牌,企業級
Fastly 開發者友善,邊緣運算

概念 4:Caching(快取)

一句話翻譯

把之前算過的結果記起來,下次直接用,不要重算。

為什麼需要快取

沒有快取:
請求 → 伺服器 → 查資料庫(50ms)→ 計算結果(20ms)→ 回傳
每次都要 70ms

有快取:
請求 → 伺服器 → 查快取(1ms)→ 有!→ 回傳
只要 1ms

同樣的查詢,資料庫要 50ms,快取只要 1ms。差 50 倍。

當你的網站每秒 10,000 個請求,其中 80% 在看同樣的商品頁面時,快取就是救命稻草。

快取的不同層級

快取不只一種,它出現在整個架構的每一層:

使用者的瀏覽器
    ↓ [瀏覽器快取] ← 第 1 層:最快,直接不發請求

CDN 邊緣節點
    ↓ [CDN 快取]   ← 第 2 層:靜態資源

Load Balancer
    ↓
應用伺服器
    ↓ [應用層快取]  ← 第 3 層:Redis / Memcached

資料庫
      [查詢快取]    ← 第 4 層:資料庫自己的快取

第 1 層:瀏覽器快取

HTTP/1.1 200 OK
Cache-Control: max-age=86400    ← 告訴瀏覽器「這個資源 24 小時內不用再問我」
Content-Type: image/jpeg
Code language: HTTP (http)

翻譯:伺服器在回應裡加一個 header,告訴瀏覽器「這個東西 86400 秒(24 小時)內不會變,你存起來直接用」。

第 2 層:CDN 快取

就是上面 CDN 章節講的那個機制。靜態資源由邊緣節點快取。

第 3 層:應用層快取(Redis / Memcached)

這是最常見的後端快取方案。如果 AI 幫你寫後端,你很可能會看到這樣的程式碼:

import redis

cache = redis.Redis(host='localhost', port=6379)  # 連線到 Redis

def get_product(product_id):
    # 第 1 步:先查快取
    cached = cache.get(f"product:{product_id}")    # 用 key 去查
    if cached:
        return json.loads(cached)                   # 有!直接回傳

    # 第 2 步:快取沒有,查資料庫
    product = db.query(f"SELECT * FROM products WHERE id = {product_id}")

    # 第 3 步:存進快取,下次就有了
    cache.set(f"product:{product_id}", json.dumps(product), ex=3600)  # ex=3600 表示 1 小時後過期

    return product
Code language: PHP (php)

逐行翻譯

  • cache.get(...) — 去 Redis 查有沒有快取過的資料
  • if cached: return — 有就直接回傳(超快,1ms 以內)
  • db.query(...) — 沒有才去查資料庫(慢,50ms+)
  • cache.set(..., ex=3600) — 查完存進 Redis,設定 1 小時後過期

這個「先查快取 → 沒有就查資料庫 → 存回快取」的套路,叫做 Cache-Aside Pattern。你在 90% 的後端程式碼中都會看到這個模式。

第 4 層:資料庫查詢快取

資料庫自己也會快取常見的查詢結果。這部分通常不需要你處理,知道存在就好。

快取的致命問題:資料不一致

快取最大的問題:資料過期了怎麼辦?

時間線:
t=0   商品價格 = $100,快取也存了 $100
t=5   管理員把價格改成 $80(資料庫更新了)
t=10  使用者看到的還是 $100(因為快取沒更新)← 問題!

常見的解決策略:

策略 做法 適合場景
TTL(存活時間) 設定過期時間,過期自動刪除 大部分場景
Write-Through 寫資料庫的同時也寫快取 讀多寫少
Cache Invalidation 資料更新時主動刪除快取 對一致性要求高

最常見的做法是 TTL — 設定一個合理的過期時間(例如 5 分鐘),過期後自動重新查資料庫。簡單粗暴但有效。


四個概念如何協作

回到雙 11 場景,完整的解決方案:

使用者想買一雙鞋
    |
    v
[CDN] → 商品圖片、CSS、JS 直接從邊緣節點回傳     → 省 80% 靜態請求
    |
    v
[Load Balancer] → 把動態請求分散到 5 台伺服器      → 每台只扛 1/5
    |
    v
[Server A~E] → 查商品資訊
    |
    v
[Redis Cache] → 快取裡有!直接回傳                  → 省 90% 資料庫查詢
    |(只有快取沒有的才到這步)
    v
[Database] → 只處理真正需要的查詢

效果

  • CDN 攔截了 80% 的靜態資源請求
  • Load Balancer 把剩下的請求分散到多台伺服器
  • 快取攔截了 90% 的資料庫查詢
  • 資料庫實際只需要處理原本 2% 的流量

這就是為什麼蝦皮、PChome 這些電商平台能撐住每年的雙 11 — 不是因為有一台超級電腦,而是靠這套組合拳。


Vibe Coder 檢查點

當你在專案中看到這些概念時,確認以下事項:

看到 Scaling 相關設定時

  • [ ] 目前是 Vertical 還是 Horizontal?有沒有遇到瓶頸的風險?
  • [ ] 如果是 Horizontal,應用程式是不是 stateless 的?(不把使用者資料存在單台伺服器記憶體裡)

看到 Load Balancer 設定時

  • [ ] 用的是什麼分配策略?Round Robin 對你的場景夠用嗎?
  • [ ] 有沒有設定健康檢查?(某台伺服器掛了要自動跳過)

看到 CDN 設定時

  • [ ] 靜態資源有沒有走 CDN?如果全都從自己的伺服器送,效能會很差
  • [ ] CDN 的快取時間設多久?太長會導致更新後使用者看到舊版本

看到 Cache 相關程式碼時

  • [ ] 有設定 TTL(過期時間)嗎?沒有的話快取永遠不會更新
  • [ ] 快取的 key 命名有沒有意義?(例如 product:123cache_1 好)
  • [ ] 有處理 Cache Miss 的情況嗎?(快取沒有時要能正常從資料庫查)

本篇重點回顧

概念 一句話 什麼時候用
Vertical Scaling 升級單台機器 初期最簡單的做法
Horizontal Scaling 加更多機器 流量大到單台扛不住
Load Balancer 把流量分散到多台 用了 Horizontal Scaling 就需要
CDN 靜態資源放到全球各地 使用者分散在不同地區
Caching 把算過的結果記起來 同樣的資料被反覆請求

**面試小提示**:系統設計面試中,「你的系統怎麼 Scale?」幾乎是必問題。回答套路:先 Vertical Scaling,遇到瓶頸後 Horizontal Scaling + Load Balancer,加上 CDN 處理靜態資源、Cache 減少資料庫壓力。這四個概念是組合技,不是單選題。


延伸:知道就好

這些進階主題在面試中偶爾出現,先知道名字就好:

  • Auto Scaling:根據流量自動增減伺服器數量(雲端平台都有支援)
  • Reverse Proxy:和 Load Balancer 類似,但還能做安全過濾、SSL 終止等
  • Cache Stampede:快取過期的瞬間大量請求同時打到資料庫,造成雪崩
  • Consistent Hashing:Horizontal Scaling 時更聰明的分配演算法,減少重新分配
  • Write-Behind Cache:先寫快取再非同步寫資料庫,提升寫入效能但有資料遺失風險

進階測驗:擴展與效能 — Scaling、Load Balancer、CDN、Caching

測驗目標:驗證你是否能在實際情境中應用所學。
共 5 題,包含情境題與錯誤診斷題。

1. 你的電商網站初期只有一台伺服器,最近使用者成長到每秒 5,000 個請求,伺服器 CPU 經常飆到 95%。你的團隊只有 2 個人,程式碼中有很多地方把使用者 session 存在伺服器記憶體裡。你應該先採取哪個策略? 情境題

  • A. 直接水平擴展,加 3 台伺服器搭配 Load Balancer
  • B. 先垂直擴展(升級 CPU 和 RAM),爭取時間重構 session 管理
  • C. 導入 CDN 來分流所有請求
  • D. 加入 Redis 快取,把所有資料庫查詢都快取起來

2. 你的網站有 3 台後端伺服器,使用 Load Balancer 搭配 Round Robin 策略。最近發現 Server A 的回應時間明顯比 Server B 和 C 慢(因為 Server A 在處理較多耗時的報表查詢),但 Load Balancer 還是繼續把 1/3 的流量送給 Server A。你應該怎麼調整? 情境題

  • A. 把 Server A 從 Load Balancer 中移除
  • B. 使用 IP Hash 策略,讓報表使用者固定去 Server A
  • C. 改用 Least Connections 策略,讓較閒的伺服器接更多請求
  • D. 對 Server A 做垂直擴展,升級成更大的機器

3. 你的電商網站使用者分佈在台灣、日本和東南亞。使用者反映商品圖片載入很慢(平均 3 秒),但後端 API 回應速度正常(100ms 內)。以下哪個方案最能直接解決這個問題? 情境題

  • A. 導入 CDN,把圖片等靜態資源分散到各地的邊緣節點
  • B. 增加後端伺服器數量,搭配 Load Balancer 分散壓力
  • C. 加入 Redis 快取,把圖片的 URL 快取起來
  • D. 把伺服器升級到更高頻寬的機器

4. 開發者用 AI 生成了以下 Redis 快取程式碼,但上線後發現商品價格更新後,使用者看到的始終是舊價格,即使等了好幾天都不會改變。問題出在哪裡? 錯誤診斷

def get_product(product_id): cached = cache.get(f”product:{product_id}”) if cached: return json.loads(cached) product = db.query(f”SELECT * FROM products WHERE id = {product_id}”) cache.set(f”product:{product_id}”, json.dumps(product)) return product
  • A. cache.get() 的 key 格式不正確,應該用底線而非冒號
  • B. cache.set() 沒有設定 ex 參數(TTL),快取永遠不會過期
  • C. 應該用 json.dumps() 取代 json.loads() 來讀取快取
  • D. Redis 預設不支援字串類型的 key,需要改用數字 ID

5. 團隊在 Nginx 設定了 Load Balancer,但測試時發現所有請求都只打到 Server A,Server B 和 C 完全沒收到流量。以下設定檔哪裡有問題? 錯誤診斷

upstream backend { server 10.0.0.1:8080; server 10.0.0.2:8080; server 10.0.0.3:8080; } server { listen 80; location / { proxy_pass http://10.0.0.1:8080; } }
  • A. upstream 區塊的名稱 backend 不能使用,應該改成 servers
  • B. 三台伺服器不能使用相同的 port 8080
  • C. proxy_pass 直接指向了 10.0.0.1:8080,應該改成 http://backend 才會走 upstream 分配
  • D. 缺少 load_balancing round_robin; 指令來啟用負載均衡

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *