跳到主要內容

系統設計

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

主題 · 負載平衡

負載平衡

把請求分給好幾台伺服器。輪流分最簡單,但只要有一台比較慢,它就會成為整體延遲的瓶頸。

策略
20/s
4
忙碌程度(工作時間比例)負載平衡器A ×3⚠ 過載分到 25% 請求 · 平均 22 sB51%分到 25% 請求 · 平均 162 msC53%分到 25% 請求 · 平均 140 msD52%分到 25% 請求 · 平均 132 ms
p50 延遲
168 ms
p95 延遲
31 s
p99 延遲
36 s
同一份流量,三種策略
策略p50p99最忙的一台
▸ 輪流168 ms36 s⚠ 過載
隨機209 ms36 s⚠ 過載
最少連線115 ms1.3 s80%

模擬 60 秒、共 1,263 個請求;每台伺服器一次處理一個,平均每個 100 ms(指數分佈)。

輪流不看伺服器有多忙,所以慢的 A 還是分到大約 1/4 的流量。它收到的工作量是處理能力的 160%,佇列只會越排越長,排在它後面的請求都得等:p99(第 99 百分位)是 36 s。

亮起來的是這一步執行的程式碼
type Strategy = "round-robin" | "random" | "least-connections";
class LoadBalancer {
private active: number[]; // requests in flight, per server
private live: number[]; // servers passing health checks
private turn = 0;
constructor(servers: number) {
this.active = new Array(servers).fill(0);
this.live = [...this.active.keys()];
}
markDown(server: number): void {
this.live = this.live.filter((s) => s !== server);
}
pick(strategy: Strategy): number {
const live = this.live;
let chosen: number;
if (strategy === "round-robin") {
chosen = live[this.turn % live.length];
} else if (strategy === "random") {
chosen = live[Math.floor(Math.random() * live.length)];
} else {
chosen = live[this.turn % live.length];
for (let k = 1; k < live.length; k++) {
const s = live[(this.turn + k) % live.length];
if (this.active[s] < this.active[chosen]) chosen = s;
}
}
this.turn += 1;
this.active[chosen] += 1;
return chosen;
}
finish(server: number): void {
this.active[server] -= 1;
}
}

模型假設與範圍

  • 這是可重現的教學模型;延遲、容量、故障率與工作負載是設定或樣本,不能直接當作正式系統的效能承諾。
  • 相同的單工作者伺服器、Poisson 到達、指數服務時間;同種子共用相同流量。未模擬 TLS、健康檢查延遲與伺服器異質性。

什麼時候用

  • 一台伺服器扛不住,或者不能接受「那台掛了服務就停」的時候:在前面放一個負載平衡器,後面放好幾台無狀態的伺服器。
  • 伺服器能力不一、請求處理時間長短不一時,用最少連線;請求都很短很平均時,輪流就夠了。
  • 需要同一個用戶一直打到同一台(例如 session 存在記憶體裡)時,改用依用戶雜湊——這時就會用到一致性雜湊。

和其他主題的關係

出現在這些架構裡

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

操作平均最差
輪流:選一台O(1)O(1)
隨機:選一台O(1)O(1)
最少連線:選一台
這裡的寫法是每台都看一次;用堆積維護連線數可以降到 O(log N),但台數通常只有幾十台,掃一遍反而最快
O(N)O(N)
請求結束O(1)O(1)
健康檢查移除一台O(N)O(N)

空間:O(N),每台伺服器一個連線計數

Big O 實測:n 變大時步數怎麼長

數的是:每選一台要看幾台伺服器(n 台伺服器,各選 1,000 次取平均)

Big On = 10n = 100n = 1,000n = 10,000成長倍數:實測(理論)
輪流O(1)1111×1.0 (×1.0)
最少連線O(n)101001,00010,000×1,000 (×1,000)

輪流不管有幾台,永遠只看下一台;最少連線每次都要把所有伺服器看過一遍,台數變一千倍,工作就變一千倍。

和其他做法比

p99(四台一樣快)p99(A 慢三倍)A 的忙碌程度負載平衡器要知道什麼
輪流594 ms36 s過載(160%)上一個給了誰
隨機821 ms36 s過載(161%)什麼都不用
最少連線501 ms1.3 s80%每台目前有幾個連線

4 台伺服器、每秒 20 個請求(四台加起來最多約每秒 40 個),模擬 60 秒,同一份流量。慢三倍的 A 自己最多每秒處理 3.3 個,所以平均分給它 5 個的策略會讓它的佇列一直變長。

真實世界裡的它

  • nginx、HAProxy、Envoy:設定檔裡的 round_robin、least_conn 就是這裡的兩種策略。
  • 雲端的負載平衡服務(AWS ELB、Google Cloud Load Balancing)做的也是同一件事,再加上健康檢查與自動擴縮。
  • DNS 輪詢:同一個網域回傳好幾個 IP,是最便宜也最粗糙的負載平衡。

取捨與陷阱

  • 平均延遲會騙人:慢的那台只拖累排在它後面的請求,平均值看起來還好,p99 已經爆了。要看尾端延遲。
  • 接近滿載時,延遲不是線性上升而是急遽變長:使用率從 80% 到 95%,排隊時間會翻好幾倍。容量要留餘裕。
  • 負載平衡器自己也是單點故障,正式環境通常會有兩台互為備援。

LeetCode 練習