【系統設計白話文】#04 快取、CDN 與流量管理 — 代理伺服器與負載均衡

測驗:快取、CDN 與流量管理 — 代理伺服器與負載均衡

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

1. 快取的核心概念是什麼?

  • A. 用時間換空間,壓縮資料以節省儲存
  • B. 用空間換時間,把常用資料放在更快的地方
  • C. 用多台伺服器同時處理同一個請求以加速
  • D. 把所有資料都存在記憶體中以取代資料庫

2. 正向代理(Forward Proxy)和反向代理(Reverse Proxy)最大的差異是什麼?

  • A. 正向代理用於加密,反向代理用於解密
  • B. 正向代理只能處理 HTTP,反向代理可以處理所有協定
  • C. 正向代理站在客戶端這邊保護客戶端,反向代理站在伺服器這邊保護伺服器
  • D. 正向代理速度較快,反向代理速度較慢但更安全

3. CDN 最適合用來放哪一類內容?

  • A. 使用者的個人設定和偏好資料
  • B. 即時股票交易價格
  • C. 需要驗證權限才能存取的機密文件
  • D. 圖片、CSS、JavaScript 等靜態資源

4. 如果你的後端有三台伺服器,硬體規格各不相同(一台 16 核、一台 8 核、一台 4 核),最適合使用哪種負載均衡演算法?

  • A. Round Robin — 輪流分配最公平
  • B. 加權分配(Weighted)— 能力強的多分一點
  • C. IP Hash — 讓同一使用者固定用同一台
  • D. Least Connections — 自動看哪台最閒

5. 「快取雪崩(Cache Stampede)」是什麼情況?以下哪個做法可以幫助避免?

  • A. 快取空間不足導致資料遺失,應增加快取容量
  • B. 快取寫入速度太慢導致延遲,應升級 Redis 版本
  • C. 大量快取同時過期導致請求湧向資料庫,應為 TTL 加上隨機偏移
  • D. 快取與資料庫資料不一致導致錯誤,應改用 Write-through 策略

你的應用跑起來了,資料庫也連上了。但隨著使用者變多,每個請求都去資料庫查一次,伺服器開始喘不過氣。這篇教你系統設計中最常見的四個加速元件:快取、CDN、代理伺服器、負載均衡器。

這篇文章要解決什麼問題?

如果你有以下困惑,這篇文章適合你:

  • 聽過 Redis、CDN 這些名詞,但不確定它們在系統裡扮演什麼角色
  • 分不清正向代理和反向代理到底差在哪裡
  • 知道負載均衡器可以「分散流量」,但不清楚它是怎麼決定把請求送去哪台伺服器的
  • 想跟 AI 討論架構時,能說出「我這邊應該加一層快取」而不只是「我的網站好慢」

前置知識

本篇假設你已經了解:

  • 客戶端與伺服器之間如何透過 HTTP 溝通(見【系統設計白話文】#03)
  • API 是什麼、請求和回應的基本概念

第一章:快取 — 空間換時間的藝術

一句話說明

快取就是「把常用的東西放在更近、更快的地方」。

你去圖書館查資料,每次都要走到書架、找書、翻頁。如果你把最常查的那幾頁影印放在桌上,下次直接看桌上的影本,就快多了。這就是快取。

快取的核心邏輯

第一次請求(快取未命中 Cache Miss):
客戶端 → 伺服器 → 資料庫(慢)→ 存一份到快取 → 回傳給客戶端

第二次請求(快取命中 Cache Hit):
客戶端 → 快取(快!)→ 直接回傳

翻譯:第一次還是得去原始來源拿資料,但拿完之後存一份副本。下次有人問同樣的問題,直接給副本,不用再跑一趟。

快取的四個層級

系統中不只一個地方可以放快取,從離使用者最近到最遠:

層級 在哪裡 放什麼 白話解釋
瀏覽器快取 使用者的電腦 圖片、CSS、JS 檔案 「這張圖昨天下載過了,不用再下載」
CDN 快取 全球各地的節點 靜態資源、熱門頁面 「台灣使用者從台灣節點拿,不用跑去美國」
應用層快取 伺服器的記憶體(Redis) API 回應、計算結果 「這個排行榜 10 秒前才算過,直接給上次的結果」
資料庫查詢快取 資料庫本身 常見查詢的結果 「這個 SQL 剛跑過,結果還在記憶體裡」

最小範例:Redis 快取怎麼用

import redis

cache = redis.Redis()

def get_user_profile(user_id):
    # 先查快取
    cached = cache.get(f"user:{user_id}")
    if cached:
        return cached  # 快取命中,直接回傳

    # 快取沒有,去資料庫查
    profile = db.query(f"SELECT * FROM users WHERE id = {user_id}")

    # 查到後存入快取,設定 300 秒後過期
    cache.set(f"user:{user_id}", profile, ex=300)

    return profile
Code language: PHP (php)

逐行翻譯

  • cache.get(...) — 去 Redis 問「你有沒有這筆資料?」
  • if cached: return cached — 有的話直接回傳,省掉資料庫查詢
  • db.query(...) — 沒有的話,去資料庫拿
  • cache.set(..., ex=300) — 拿到後存進 Redis,300 秒後自動刪除

快取失效策略:資料過期了怎麼辦?

快取最大的挑戰不是「怎麼存」,而是「什麼時候該丟掉」。資料庫裡的資料更新了,快取裡還是舊的,使用者就會看到過期資訊。

策略 白話解釋 適合場景
TTL(Time To Live) 設定保存期限,時間到自動刪除 排行榜、熱門文章(幾分鐘更新一次沒關係)
LRU(Least Recently Used) 空間滿了,先丟掉最久沒人用的 記憶體有限時的通用策略
Write-through 寫資料庫的同時也更新快取 資料一致性很重要(如帳戶餘額)
Write-back 先寫快取,之後再批次寫回資料庫 寫入頻繁但可以容忍短暫不一致(如按讚數)

快速決策

Q1: 資料多久不更新,使用者可以接受?
├─ 幾秒到幾分鐘 → TTL(設個合理的過期時間)
└─ 一秒都不行 → 繼續 Q2

Q2: 寫入頻率高嗎?
├─ 高(每秒幾百次以上)→ Write-back(先寫快取,批次同步)
└─ 不高 → Write-through(同時寫兩邊,保證一致)

第二章:CDN — 全球部署的快取

一句話說明

CDN 就是在全球各地放副本,讓使用者從最近的節點取資料。

CDN 解決什麼問題?

假設你的伺服器在美國。一個台灣使用者要看你網站上的圖片:

沒有 CDN:
台灣使用者 →→→→→(太平洋)→→→→→ 美國伺服器 →→→→→(太平洋)→→→→→ 台灣使用者
延遲:200ms+

有 CDN:
台灣使用者 →→ 台灣 CDN 節點(已經有副本)→→ 台灣使用者
延遲:20ms

效果:延遲從 200ms 降到 20ms,而且美國伺服器的負擔也變小了。

CDN 的運作方式

1. 第一個台灣使用者請求圖片
   → 台灣 CDN 節點沒有(Miss)
   → 去美國原始伺服器拿
   → 存一份在台灣節點
   → 回傳給使用者

2. 第二個台灣使用者請求同一張圖片
   → 台灣 CDN 節點有了(Hit)
   → 直接回傳,不用去美國

CDN 適合放什麼?

適合 不適合
圖片、影片、CSS、JavaScript 使用者個人資料(每人不同)
不常變動的頁面 即時交易資料(必須最新)
公開的靜態內容 需要驗證權限的內容

重點:CDN 本質上就是「離使用者很近的快取層」,適合放「很多人都會要、而且不常變」的內容。


第三章:代理伺服器 — 請求的中間人

一句話說明

代理伺服器就是「幫你轉交請求的中間人」。

代理分兩種方向:正向代理(站在客戶端這邊)和反向代理(站在伺服器這邊)。名字很像,但做的事情完全不同。

正向代理(Forward Proxy)

                    正向代理
客戶端 A ──┐        ┌──→ 伺服器 X
客戶端 B ──┼──→ [Proxy] ──→ 伺服器 Y
客戶端 C ──┘        └──→ 伺服器 Z
Code language: JavaScript (javascript)

白話翻譯:正向代理站在「客戶端這邊」,代表客戶端去跟伺服器溝通。伺服器不知道真正的客戶端是誰,只看到代理。

用途

功能 白話解釋 實際例子
匿名存取 伺服器看不到你的真實 IP VPN 服務
內容過濾 公司封鎖員工存取某些網站 企業防火牆
快取加速 多人存取同一網站,代理快取一份 學校網路代理

反向代理(Reverse Proxy)

                              反向代理
客戶端 A ──┐        ┌──→ 伺服器 1
客戶端 B ──┼──→ [Reverse Proxy] ──→ 伺服器 2
客戶端 C ──┘        └──→ 伺服器 3
Code language: JavaScript (javascript)

白話翻譯:反向代理站在「伺服器這邊」,代表伺服器來接客戶端的請求。客戶端不知道背後有幾台伺服器,只看到代理。

用途

功能 白話解釋 實際例子
負載均衡 把請求分散給多台伺服器 Nginx 分流
SSL 終止 統一處理 HTTPS 加密 後面的伺服器只需處理 HTTP
安全防護 隱藏真實伺服器的 IP 和數量 防 DDoS 攻擊
快取 快取常見回應 靜態資源加速

正向 vs 反向:一張表搞懂

正向代理 反向代理
站在誰那邊 客戶端 伺服器
誰不知道對方 伺服器不知道真正的客戶端 客戶端不知道真正的伺服器
誰設定的 客戶端(使用者)設定 伺服器管理員設定
核心目的 保護客戶端的隱私 保護伺服器的安全與效能
常見例子 VPN、企業防火牆 Nginx、Cloudflare

記憶口訣

  • 正向代理 = 代理「正」在發請求的人(客戶端)
  • 反向代理 = 代理請求要去的「反」方向(伺服器)

第四章:負載均衡器 — 流量的交通警察

一句話說明

負載均衡器就是「決定每個請求要送去哪台伺服器」的交通指揮。

當你的應用有多台伺服器時,需要有人來分配流量。這個「分配者」就是負載均衡器(Load Balancer)。

為什麼需要負載均衡?

沒有負載均衡器:
所有請求 →→→ 只有一台伺服器(撐不住就掛了)

有負載均衡器:
所有請求 →→→ [負載均衡器] ──→ 伺服器 1(處理 33%)
                            ──→ 伺服器 2(處理 33%)
                            ──→ 伺服器 3(處理 33%)
Code language: CSS (css)

常見的負載均衡演算法

負載均衡器用什麼方式決定「這個請求送去哪台」?以下是四種最常見的演算法:

1. Round Robin(輪流分配)

請求 1 → 伺服器 A
請求 2 → 伺服器 B
請求 3 → 伺服器 C
請求 4 → 伺服器 A(輪回來了)
請求 5 → 伺服器 B
...

白話:排隊輪流,像發撲克牌一樣一人一張。

優點:簡單,不需要追蹤任何狀態。 缺點:不管伺服器忙不忙,照樣發。可能有一台已經很忙了還在收到新請求。

2. Least Connections(最少連線數)

伺服器 A:目前 5 個連線
伺服器 B:目前 2 個連線  ← 新請求送這裡
伺服器 C:目前 8 個連線

白話:看哪台最閒就送去哪台。

優點:能自動適應,忙的伺服器會少分到。 缺點:需要持續追蹤每台伺服器的連線數,稍微增加複雜度。

3. IP Hash(根據來源 IP 分配)

使用者 IP 192.168.1.1hash → 固定送到伺服器 A
使用者 IP 192.168.1.2hash → 固定送到伺服器 C
使用者 IP 192.168.1.3hash → 固定送到伺服器 B
Code language: CSS (css)

白話:同一個使用者永遠送去同一台伺服器。

優點:同一使用者的 session 資料不用在伺服器之間同步。 缺點:某些 IP 請求特別多時會造成分配不均。

4. 加權分配(Weighted)

伺服器 A(16 核心):權重 5 → 分到 50% 流量
伺服器 B(8 核心):權重 3 → 分到 30% 流量
伺服器 C(4 核心):權重 2 → 分到 20% 流量

白話:能力強的多分一點,能力弱的少分一點。

優點:適合伺服器硬體規格不同的情況。 缺點:權重需要手動設定和調整。

演算法速查表

演算法 適合場景 不適合場景
Round Robin 所有伺服器規格相同、請求處理時間差不多 請求有輕有重、伺服器規格不同
Least Connections 請求處理時間差異大(如有些查詢很慢) 需要 session 固定在同一台
IP Hash 需要 session 粘性(sticky session) 使用者分布很不均勻
加權分配 伺服器硬體規格不同 所有伺服器規格一樣(沒必要加權)

第五章:實戰組合 — Nginx 同時當反向代理和負載均衡器

在真實世界中,這些元件經常組合使用。最經典的例子就是 Nginx:它同時扮演反向代理和負載均衡器。

最小範例:Nginx 設定

# nginx.conf
http {
    # 定義後端伺服器群組
    upstream backend_servers {
        server 192.168.1.10:8000 weight=3;  # 高規格伺服器,多分一點
        server 192.168.1.11:8000 weight=2;
        server 192.168.1.12:8000 weight=1;  # 低規格伺服器,少分一點
    }

    server {
        listen 80;

        # 靜態檔案 — Nginx 自己處理(不用麻煩後端)
        location /static/ {
            root /var/www;
            expires 7d;  # 瀏覽器快取 7 天
        }

        # API 請求 — 轉發給後端伺服器
        location /api/ {
            proxy_pass http://backend_servers;  # 負載均衡到後端群組
        }
    }
}
Code language: PHP (php)

逐行翻譯

  • upstream backend_servers — 定義一群後端伺服器,Nginx 會把請求分散給它們
  • weight=3 — 這台伺服器的權重是 3,會收到較多請求
  • listen 80 — Nginx 在 port 80 接收所有請求
  • location /static/ — 如果請求的是靜態檔案,Nginx 自己回傳(當快取用)
  • expires 7d — 告訴瀏覽器「這個檔案 7 天內不用重新下載」
  • location /api/ — 如果請求的是 API,轉發給後端伺服器群組
  • proxy_pass — 這就是「反向代理」的動作:把請求代為轉發

完整的請求路徑

把本篇所有概念串起來,一個真實世界的請求可能經過這些層:

使用者(台灣)
  │
  ▼
瀏覽器快取 → 有快取?→ 直接顯示(不發請求)
  │ 沒有
  ▼
CDN 節點(台灣)→ 有快取?→ 直接回傳
  │ 沒有
  ▼
反向代理 / 負載均衡器(Nginx)
  │
  ├─ 靜態資源 → Nginx 直接回傳
  │
  └─ API 請求 → 選一台後端伺服器
                    │
                    ▼
               應用層快取(Redis)→ 有快取?→ 直接回傳
                    │ 沒有
                    ▼
               資料庫查詢 → 拿到結果 → 存入 Redis → 回傳

重點:每一層都在試圖「攔截」請求,越早攔截到,回應越快、後面的元件壓力越小。


常見坑與解法

坑 1:快取了不該快取的東西

症狀:使用者 A 看到使用者 B 的個人資料。

為什麼會發生: 把包含個人化內容的 API 回應放進了公共快取。快取的 key 只用了 URL,沒有區分不同使用者。

怎麼避免

  • 個人化內容不要放入公共快取(CDN、共享 Redis)
  • 如果要快取,key 裡面要包含使用者 ID
  • HTTP 回應標頭用 Cache-Control: private 告訴 CDN 不要快取

坑 2:快取雪崩(Cache Stampede)

症狀:快取過期的瞬間,系統突然變得很慢或掛掉。

為什麼會發生: 大量快取同時過期,所有請求同時湧向資料庫。

怎麼避免

  • 不要讓所有快取的 TTL 都設一樣的時間,加上隨機偏移
  • 設定「快取預熱」:在快取即將過期前就主動更新
  • 使用互斥鎖:同一時間只允許一個請求去更新快取,其他請求等待

坑 3:搞混正向代理和反向代理

症狀:討論架構時用詞混淆,導致團隊溝通不順。

怎麼記

  • 你(客戶端)找人幫你跑腿 = 正向代理(VPN)
  • 商店(伺服器)找人幫它接客 = 反向代理(Nginx)

Vibe Coder 檢查點

當你在跟 AI 討論系統架構時,確認這些事:

  • [ ] 有沒有快取層? — 如果同樣的查詢會被反覆執行,問 AI:「這個 API 需不需要加 Redis 快取?」
  • [ ] 靜態資源有沒有走 CDN? — 圖片、CSS、JS 檔案應該透過 CDN 分發,而不是每次都從你的伺服器取
  • [ ] 有沒有反向代理? — 如果有多台伺服器,前面應該有 Nginx 或類似工具來分配流量
  • [ ] 負載均衡演算法合理嗎? — 如果伺服器規格不同,用加權分配;如果需要 session 粘性,用 IP Hash
  • [ ] 快取失效策略? — 確認有設定 TTL,並且不是所有快取都在同一時間過期

跟 AI 討論架構時怎麼說

入門版

「我的網站很慢,怎麼辦?」

進階版(讀完本篇後)

「我的 API 回應時間有 500ms,其中 400ms 花在資料庫查詢。這個查詢結果 5 分鐘內不會變。我想加一層 Redis 快取,TTL 設 5 分鐘,用 write-through 策略。你覺得合理嗎?」

「我們有三台 API 伺服器,一台 16 核、兩台 8 核。現在用 Nginx 做負載均衡,想改成加權 Round Robin。幫我寫 Nginx 設定,16 核的權重設 2,8 核的設 1。」


小結

本篇介紹了四個讓系統更快、更穩的核心元件:

元件 一句話 核心思路
快取 常用的資料放在更快的地方 空間換時間
CDN 在全球各地放副本 把資料搬到使用者旁邊
代理伺服器 請求的中間人 正向保護客戶端,反向保護伺服器
負載均衡器 流量的交通警察 把請求分散給多台伺服器

記住這個原則:系統設計的優化就是在請求路徑上層層攔截。每一層快取、每一個代理,都在試圖讓請求「盡早得到回應」,不要一路跑到最後面的資料庫。

下一篇我們會進入資料庫的世界,討論 SQL vs NoSQL、分片、複製等讓資料層也能擴展的技術。

進階測驗:快取、CDN 與流量管理 — 代理伺服器與負載均衡

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

1. 你的電商網站有一個「熱門商品排行榜」API,每次呼叫都要從資料庫統計銷售數據,耗時約 800ms。排行榜大約每 10 分鐘才會有明顯變化。你打算加一層 Redis 快取來加速,應該選哪種快取失效策略? 情境題

  • A. Write-through — 每次有新訂單就同時更新快取,保證排行榜即時正確
  • B. TTL — 設定 5-10 分鐘的過期時間,時間到自動重新計算
  • C. Write-back — 先把排行榜寫入快取,之後再批次寫回資料庫
  • D. LRU — 讓最久沒人查的排行榜自動被淘汰

2. 你的團隊有三台 API 伺服器。使用者登入後,session 資料會存在伺服器的記憶體裡(沒有用共享 session store)。目前使用者反映偶爾會被登出,你懷疑是負載均衡導致的。應該怎麼處理? 情境題

  • A. 改用 Least Connections 演算法,讓最閒的伺服器接新請求
  • B. 改用加權分配,給效能好的伺服器分更多流量
  • C. 改用 IP Hash 演算法,讓同一使用者固定送到同一台伺服器
  • D. 增加更多伺服器,降低每台的負載就不會掉 session

3. 你的公司想對外部 API 請求做統一的存取控制:封鎖員工存取特定網站,並且隱藏公司內部網路的真實 IP。你應該部署哪種代理? 情境題

  • A. 正向代理 — 站在客戶端這邊,可以過濾請求並隱藏內部 IP
  • B. 反向代理 — 站在伺服器這邊,可以做 SSL 終止和安全防護
  • C. CDN — 在全球各地放副本,讓員工從最近節點存取
  • D. 負載均衡器 — 把員工的請求分散到多個外部伺服器

4. 你的網站上線後,客服收到投訴:「使用者 A 登入後看到的是使用者 B 的個人資料頁面」。團隊檢查後發現 API 回應有被快取。以下是問題程式碼片段,最可能的根本原因是什麼? 錯誤診斷

def get_profile(user_id): cached = cache.get(“user_profile”) # 快取 key if cached: return cached profile = db.query(f”SELECT * FROM users WHERE id = {user_id}”) cache.set(“user_profile”, profile, ex=300) return profile
  • A. TTL 設定 300 秒太長,應該縮短為 30 秒
  • B. 快取 key 只用了 "user_profile",沒有包含 user_id,導致所有使用者共用同一份快取
  • C. 應該用 Write-through 策略而不是 TTL
  • D. Redis 的記憶體不足,導致快取資料被覆蓋出錯

5. 你的網站在凌晨 3:00 每天會執行排程任務,清空並重建所有快取。但每天凌晨 3:00 之後的幾分鐘,監控系統都會顯示資料庫 CPU 飆到 100%,網站回應極慢。這個問題最可能是什麼? 錯誤診斷

  • A. 排程任務本身佔用太多 CPU,應該改到非尖峰時段執行
  • B. Redis 重啟導致連線中斷,應該設定自動重連
  • C. 快取雪崩 — 所有快取同時被清空,大量請求同時湧向資料庫,應改為漸進式更新或為 TTL 加上隨機偏移
  • D. 資料庫索引在凌晨被重建,應該把索引重建排在不同時段

發佈留言

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