跳到主要內容

系統設計

把資料結構放大到好幾台機器

主題 · CDN

CDN

CDN 是 Content Delivery Network(內容傳遞網路):把內容複製到離使用者近的邊緣節點,大部分請求不用跨海回到原始伺服器,延遲和原站負載一起下降。

程式碼路徑
1 min
1.0
200 (20%)
經過 CDN(平均)直接連原站
0 ms50 ms100 ms150 ms200 ms北美命中 67%北美(原站在此): 30 ms30 ms北美(原站在此), 直連: 40 ms40 ms歐洲命中 65%歐洲: 53 ms53 ms歐洲, 直連: 110 ms110 ms亞洲命中 65%亞洲: 85 ms85 ms亞洲, 直連: 200 ms200 ms
每 100 個請求
65 在邊緣答掉
35 回原站
命中率
65.5%
平均延遲(直連)
54 ms (109 ms)
原站每秒請求
13.8 / 40
舊版本回應
30.5%

TTL 設為 1 min 時,邊緣節點自己答掉 65% 的請求,原站每秒只收到 13.8 個請求,而不是 40 個。亞洲受惠最多:平均 85 ms,直連原站要 200 ms,因為每次命中都省掉離原站最遠的那一趟。TTL 長的代價:30.5% 的回應是原站已經換掉的舊版本。

假設的往返時間:使用者到自己地區的邊緣節點 20 ms;邊緣節點回原站,北美/歐洲/亞洲分別 30/95/185 ms;使用者直連原站 40/110/200 ms。1,000 個物件、熱度依 Zipf 分佈,每秒 40 個請求、原站每秒更新 1 個物件,共 10 分鐘;前 60 秒暖機不計。

亮起來的是這一步執行的程式碼
interface Origin {
fetch(key: string): string; // far away: 30-185 ms
}
class EdgeCache {
// A Map keeps insertion order: the first key is the least recently used.
private cache = new Map<string, { body: string; fetchedAt: number }>();
constructor(private capacity: number, private ttlSec: number, private origin: Origin) {}
get(key: string, now: number): { body: string; hit: boolean } {
const entry = this.cache.get(key);
if (entry && now - entry.fetchedAt < this.ttlSec) {
this.cache.delete(key);
this.cache.set(key, entry);
return { body: entry.body, hit: true };
}
if (entry) this.cache.delete(key);
const body = this.origin.fetch(key);
if (this.cache.size >= this.capacity) {
this.cache.delete(this.cache.keys().next().value!);
}
this.cache.set(key, { body, fetchedAt: now });
return { body, hit: false };
}
// Called on every edge when the origin changes an object.
purge(key: string): void {
this.cache.delete(key);
}
}

模型假設與範圍

  • 這是可重現的教學模型;延遲、容量、故障率與工作負載是設定或樣本,不能直接當作正式系統的效能承諾。
  • 三個區域的邊緣快取與單一來源,TTL/purge 控制版本新鮮度;往返延遲是假設,沒有真實路由或故障量測。

什麼時候用

  • 使用者遍布各地,而內容大家讀的都一樣:圖片、影片、JavaScript、CSS(Cascading Style Sheets)、下載檔、公開的 API 回應。
  • 原站撐不住尖峰流量:大部分請求在邊緣就被答掉,原站只看到沒命中的那一部分。
  • 內容可以容忍短時間的舊版本;不能容忍的(價格、庫存、個人資料)就設短 TTL、更新時清除,或乾脆不經過快取。

和其他主題的關係

由這些組成
快取
延伸閱讀
負載平衡

出現在這些架構裡

時間與空間複雜度(Big O)

操作平均最差
邊緣節點回應一個請求
沒命中時再加一趟回原站:時間花在網路,不在計算
O(1)O(1)
清除一個物件
E 是邊緣節點數;每一台都要通知到
O(E)O(E)

空間:O(E·C),每個邊緣節點各放 C 個物件,熱門的物件在每一台都有一份

和其他做法比

命中率原站每秒請求平均延遲舊版本回應
TTL 0(不快取)0%40.0116 ms0.0%
TTL 10 秒46%21.572 ms9.9%
TTL 1 分鐘65%13.854 ms30.5%
TTL 5 分鐘69%12.350 ms45.8%
TTL 5 分鐘+更新時清除66%13.653 ms0.0%

同一串請求(熱度 s = 1、每個邊緣放 200 個物件)只換 TTL 和清除策略。TTL 越長,命中率越高、原站越輕鬆,送出舊版本的比例也越高;更新時主動清除可以兩者兼得一部分,但需要原站知道每一個邊緣節點在哪裡。

真實世界裡的它

  • Cloudflare、Akamai、Fastly、Amazon CloudFront:全球幾百個節點,用 DNS 或 Anycast 把使用者導到最近的一個。
  • Netflix 的 Open Connect 直接把機器放進網路業者的機房,熱門影片在半夜先推過去。
  • HTTP 的 Cache-Control: max-age 和 s-maxage 就是在設定這裡的 TTL。

取捨與陷阱

  • TTL 是新鮮度和負載的交換:設長了命中率高,但使用者會看到舊內容;檔名加上版本(app.3f9a.js)就能讓靜態檔永遠不用清除。
  • 沒命中比直接連原站還慢:多了使用者到邊緣這一段。冷門內容、個人化內容放上 CDN 反而變慢。
  • 快取的鍵沒有包含會改變內容的東西(語言、登入狀態、裝置),就會把 A 的頁面送給 B。