前一章談了 CDN「為什麼快」與基本概念。本章深入它的內部運作:請求是怎麼被分到某個邊緣節點的?節點之間如何分攤快取?某個節點掛了會發生什麼事?理解這些,有助於看懂 CDN 的命中率報表,並做出更合理的快取設定。
請求如何被導向最近的節點#
當瀏覽器要向 CDN 拿資源,背後通常經過兩段「找節點」的過程:
flowchart TD
U[使用者請求 asset.example.com] --> D[DNS / Anycast 導向最近的邊緣機房]
D --> LB[機房內負載均衡:依資源 key 選快取節點]
LB --> N1[節點 A]
LB --> N2[節點 B]
LB --> N3[節點 C]- 地理導向:透過 DNS 或 Anycast,把使用者導到網路上「最近」的邊緣機房。
- 機房內分配:同一機房內有多台快取伺服器,要決定「哪台負責哪些資源」。
第二步若隨便用「節點數取模」來分配(hash(key) % N),會有個致命問題:一旦增減節點,N 改變,幾乎所有資源的歸屬都會重算,導致快取大規模失效、瞬間全部回源。
一致性雜湊#
一致性雜湊(Consistent Hashing)就是為了解決上述問題:把節點與資源都映射到一個環上,資源歸屬於「順時針遇到的第一個節點」。
flowchart LR
subgraph 雜湊環
A[節點 A] --> B[節點 B]
B --> C[節點 C]
C --> A
end
K1[資源 k1] -. 順時針 .-> B
K2[資源 k2] -. 順時針 .-> C它的關鍵好處:增減一個節點,只影響環上相鄰的一小段資源,其餘歸屬不變。
取模分配:節點 4→5 台,約 80% 的 key 要重新搬移
一致性雜湊:節點 4→5 台,平均僅 1/5 的 key 受影響實務上還會為每個實體節點放多個「虛擬節點」,均勻散布在環上,避免資料分布不均(某個節點分到過多熱點)。這也是 Redis Cluster、分散式快取常用的同一套技巧。
多層回源:edge ➡️ regional ➡️ origin#
單一邊緣節點容量有限,CDN 通常設計成多層階梯,未命中時逐層往上問,最後才打到你的來源伺服器:
flowchart LR
Edge[邊緣節點<br/>離使用者最近] -->|miss| Regional[區域節點<br/>較大快取]
Regional -->|miss| Origin[(來源 Server)]
Origin -. 填充 .-> Regional -. 填充 .-> Edge這層級設計讓「來源伺服器」承受的請求量被大幅過濾——大多數請求在邊緣或區域層就被擋下並命中。讀 CDN 報表時,Edge Hit / Origin Hit 的比例就是在反映這套階梯的效率。
驚群效應:熱門資源同時過期#
當一個熱門資源在某節點過期,若不加防護,瞬間湧入的大量請求會同時穿透到來源(thundering herd / cache stampede),可能把來源打垮。CDN 對此的常見防護:
- 請求合併(request coalescing):同一節點對同一個過期 key,只放「一個」請求回源,其餘請求等它回來共用結果。
- 過期抖動:對存活時間加入隨機偏移,避免大量資源在同一秒一起過期(對應
快取雪崩)。
把一大批資源設定成「整點同時過期」是常見地雷。版本發布時若所有靜態資源的快取同時失效,回源流量會在那一刻飆升。用內容雜湊檔名(content hash)讓「沒變的資源」維持快取,是更穩的做法。
邊緣節點失效的代價#
CDN 邊緣快取分散在全球,使「主動清除(purge)某個資源」變成一個分散式問題:清除指令要傳達到所有節點,本身是非同步的、需要時間。
因此 CDN 的最佳實務不是「改了再去 purge」,而是讓 URL 隨內容改變:
app.a1b2c3.js。內容一變檔名就變,新舊各自有獨立快取,根本不需要清除——舊的自然沒人再請求。這與程式碼壓縮與快取的指紋(fingerprint)策略一致。
小結#
| 機制 | 解決的問題 |
|---|---|
| 地理 / Anycast 導向 | 把使用者導到最近的機房 |
| 一致性雜湊 | 增減節點時只搬移少量快取,避免大規模失效 |
| 多層回源 | 逐層過濾請求,保護來源伺服器 |
| 請求合併 | 熱門資源過期時避免驚群打垮來源 |
| 內容雜湊檔名 | 繞過「分散式清除」難題,新舊資源各自快取 |