負載平衡
把請求分給好幾台伺服器。輪流分最簡單,但只要有一台比較慢,它就會成為整體延遲的瓶頸。
策略
20/s
4
p50 延遲
168 ms
p95 延遲
31 s
p99 延遲
36 s
| 策略 | p50 | p99 | 最忙的一台 |
|---|---|---|---|
| ▸ 輪流 | 168 ms | 36 s | ⚠ 過載 |
| 隨機 | 209 ms | 36 s | ⚠ 過載 |
| 最少連線 | 115 ms | 1.3 s | 80% |
模擬 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 存在記憶體裡)時,改用依用戶雜湊——這時就會用到一致性雜湊。
和其他主題的關係
- 被這些用到
- 案例:從一台機器到百萬用戶API 閘道
出現在這些架構裡
時間與空間複雜度(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 O | n = 10 | n = 100 | n = 1,000 | n = 10,000 | 成長倍數:實測(理論) | |
|---|---|---|---|---|---|---|
| 輪流 | O(1) | 1 | 1 | 1 | 1 | ×1.0 (×1.0) |
| 最少連線 | O(n) | 10 | 100 | 1,000 | 10,000 | ×1,000 (×1,000) |
輪流不管有幾台,永遠只看下一台;最少連線每次都要把所有伺服器看過一遍,台數變一千倍,工作就變一千倍。
和其他做法比
| p99(四台一樣快) | p99(A 慢三倍) | A 的忙碌程度 | 負載平衡器要知道什麼 | |
|---|---|---|---|---|
| 輪流 | 594 ms | 36 s | 過載(160%) | 上一個給了誰 |
| 隨機 | 821 ms | 36 s | 過載(161%) | 什麼都不用 |
| 最少連線 | 501 ms | 1.3 s | 80% | 每台目前有幾個連線 |
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%,排隊時間會翻好幾倍。容量要留餘裕。
- 負載平衡器自己也是單點故障,正式環境通常會有兩台互為備援。