跳到主要內容

系統設計

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

主題 · 垂直擴充與水平擴充

垂直擴充與水平擴充

換一台更大的機器,還是加更多台機器?垂直擴充最簡單但有上限、價格越來越陡;水平擴充幾乎沒有上限,但服務必須無狀態,資料也要能切開。

5,000
程式碼

垂直擴充:換一台更大的

規格
32 核
每小時
$1.60
忙碌度
63%
壞一台後的容量
0

水平擴充:多台 4 核小機器

台數(含備用)
5
每小時
$1.03
忙碌度
55%
壞一台後的容量
80%
$0.1$1$101k10k100k每秒請求數(對數);每小時成本(對數)最大台機器的上限每秒 400 — 換大台:$0.10,加台數:$0.43每秒 424 — 換大台:$0.10,加台數:$0.43每秒 449 — 換大台:$0.10,加台數:$0.43每秒 476 — 換大台:$0.10,加台數:$0.43每秒 505 — 換大台:$0.10,加台數:$0.43每秒 535 — 換大台:$0.10,加台數:$0.43每秒 567 — 換大台:$0.10,加台數:$0.43每秒 601 — 換大台:$0.10,加台數:$0.43每秒 638 — 換大台:$0.10,加台數:$0.43每秒 676 — 換大台:$0.10,加台數:$0.43每秒 716 — 換大台:$0.20,加台數:$0.43每秒 759 — 換大台:$0.20,加台數:$0.43每秒 805 — 換大台:$0.20,加台數:$0.43每秒 853 — 換大台:$0.20,加台數:$0.43每秒 904 — 換大台:$0.20,加台數:$0.43每秒 959 — 換大台:$0.20,加台數:$0.43每秒 1,016 — 換大台:$0.20,加台數:$0.43每秒 1,077 — 換大台:$0.20,加台數:$0.43每秒 1,142 — 換大台:$0.20,加台數:$0.43每秒 1,210 — 換大台:$0.20,加台數:$0.43每秒 1,283 — 換大台:$0.40,加台數:$0.63每秒 1,360 — 換大台:$0.40,加台數:$0.63每秒 1,441 — 換大台:$0.40,加台數:$0.63每秒 1,528 — 換大台:$0.40,加台數:$0.63每秒 1,620 — 換大台:$0.40,加台數:$0.63每秒 1,717 — 換大台:$0.40,加台數:$0.63每秒 1,820 — 換大台:$0.40,加台數:$0.63每秒 1,929 — 換大台:$0.40,加台數:$0.63每秒 2,045 — 換大台:$0.40,加台數:$0.63每秒 2,167 — 換大台:$0.40,加台數:$0.63每秒 2,297 — 換大台:$0.40,加台數:$0.63每秒 2,435 — 換大台:$0.80,加台數:$0.63每秒 2,581 — 換大台:$0.80,加台數:$0.83每秒 2,736 — 換大台:$0.80,加台數:$0.83每秒 2,900 — 換大台:$0.80,加台數:$0.83每秒 3,074 — 換大台:$0.80,加台數:$0.83每秒 3,259 — 換大台:$0.80,加台數:$0.83每秒 3,454 — 換大台:$0.80,加台數:$0.83每秒 3,662 — 換大台:$0.80,加台數:$0.83每秒 3,881 — 換大台:$1.60,加台數:$1.03每秒 4,114 — 換大台:$1.60,加台數:$1.03每秒 4,361 — 換大台:$1.60,加台數:$1.03每秒 4,623 — 換大台:$1.60,加台數:$1.03每秒 4,900 — 換大台:$1.60,加台數:$1.03每秒 5,194 — 換大台:$1.60,加台數:$1.23每秒 5,506 — 換大台:$1.60,加台數:$1.23每秒 5,836 — 換大台:$3.20,加台數:$1.23每秒 6,186 — 換大台:$3.20,加台數:$1.23每秒 6,558 — 換大台:$3.20,加台數:$1.43每秒 6,951 — 換大台:$6.40,加台數:$1.43每秒 7,368 — 換大台:沒有夠大的機器,加台數:$1.43每秒 7,810 — 換大台:沒有夠大的機器,加台數:$1.63每秒 8,279 — 換大台:沒有夠大的機器,加台數:$1.63每秒 8,775 — 換大台:沒有夠大的機器,加台數:$1.63每秒 9,302 — 換大台:沒有夠大的機器,加台數:$1.83每秒 9,860 — 換大台:沒有夠大的機器,加台數:$1.83每秒 10,452 — 換大台:沒有夠大的機器,加台數:$2.03每秒 11,079 — 換大台:沒有夠大的機器,加台數:$2.03每秒 11,744 — 換大台:沒有夠大的機器,加台數:$2.23每秒 12,448 — 換大台:沒有夠大的機器,加台數:$2.23每秒 13,195 — 換大台:沒有夠大的機器,加台數:$2.43每秒 13,987 — 換大台:沒有夠大的機器,加台數:$2.43每秒 14,826 — 換大台:沒有夠大的機器,加台數:$2.63每秒 15,716 — 換大台:沒有夠大的機器,加台數:$2.83每秒 16,658 — 換大台:沒有夠大的機器,加台數:$2.83每秒 17,658 — 換大台:沒有夠大的機器,加台數:$3.03每秒 18,717 — 換大台:沒有夠大的機器,加台數:$3.23每秒 19,841 — 換大台:沒有夠大的機器,加台數:$3.43每秒 21,031 — 換大台:沒有夠大的機器,加台數:$3.63每秒 22,293 — 換大台:沒有夠大的機器,加台數:$3.83每秒 23,630 — 換大台:沒有夠大的機器,加台數:$4.03每秒 25,048 — 換大台:沒有夠大的機器,加台數:$4.23每秒 26,551 — 換大台:沒有夠大的機器,加台數:$4.43每秒 28,144 — 換大台:沒有夠大的機器,加台數:$4.63每秒 29,833 — 換大台:沒有夠大的機器,加台數:$5.03每秒 31,623 — 換大台:沒有夠大的機器,加台數:$5.23每秒 33,520 — 換大台:沒有夠大的機器,加台數:$5.63每秒 35,531 — 換大台:沒有夠大的機器,加台數:$5.83每秒 37,663 — 換大台:沒有夠大的機器,加台數:$6.23每秒 39,923 — 換大台:沒有夠大的機器,加台數:$6.63每秒 42,318 — 換大台:沒有夠大的機器,加台數:$6.83每秒 44,858 — 換大台:沒有夠大的機器,加台數:$7.23每秒 47,549 — 換大台:沒有夠大的機器,加台數:$7.83每秒 50,402 — 換大台:沒有夠大的機器,加台數:$8.23每秒 53,426 — 換大台:沒有夠大的機器,加台數:$8.63每秒 56,632 — 換大台:沒有夠大的機器,加台數:$9.23每秒 60,029 — 換大台:沒有夠大的機器,加台數:$9.63每秒 63,631 — 換大台:沒有夠大的機器,加台數:$10.23每秒 67,449 — 換大台:沒有夠大的機器,加台數:$10.83每秒 71,496 — 換大台:沒有夠大的機器,加台數:$11.43每秒 75,786 — 換大台:沒有夠大的機器,加台數:$12.23每秒 80,333 — 換大台:沒有夠大的機器,加台數:$12.83每秒 85,153 — 換大台:沒有夠大的機器,加台數:$13.63每秒 90,262 — 換大台:沒有夠大的機器,加台數:$14.43每秒 95,678 — 換大台:沒有夠大的機器,加台數:$15.23每秒 101,419 — 換大台:沒有夠大的機器,加台數:$16.23每秒 107,504 — 換大台:沒有夠大的機器,加台數:$17.03
垂直擴充:一台更大的機器水平擴充:更多台小機器+負載平衡

每秒 5,000 個請求:一台 32 核、63% 忙碌,每小時 $1.60;或 5 台 4 核小機器加負載平衡,每小時 $1.03。單台換規格每次要停機 5 分鐘,加一台小機器則不用。

前提:水平擴充的伺服器必須無狀態

登入狀態存在
已登入使用者
1,000
壞一台時被登出
205 (21%)
每個請求多查幾次
0

使用者被黏在存著自己 session 的那台(sticky session)。那台一壞,黏在它上面的 205 位使用者就在使用途中被登出。

亮起來的是這一步執行的程式碼
type Instance = { cores: number; qps: number; pricePerHour: number };
// Scale up: the smallest machine that carries the load below the target.
function pickInstance(qps: number, sizes: Instance[], target = 0.7): Instance | null {
for (const size of sizes) {
if (size.qps * target >= qps) return size;
}
// Past the biggest machine there is nothing to buy.
return null;
}
// Scale out: enough small servers for the load, plus one spare.
function serversNeeded(qps: number, perServerQps: number, target = 0.7): number {
return Math.ceil(qps / (perServerQps * target)) + 1;
}
// Stateless: the session is looked up in a shared store on every request,
// so any server can answer and losing one signs nobody out.
class Handler {
constructor(private sessions: Map<string, string>) {}
handle(sessionId: string): { status: number; user?: string } {
const user = this.sessions.get(sessionId);
if (user === undefined) return { status: 401 };
return { status: 200, user };
}
}

模型假設與範圍

  • 這是可重現的教學模型;延遲、容量、故障率與工作負載是設定或樣本,不能直接當作正式系統的效能承諾。
  • 容量曲線由序列化、協調與每核心處理量的設定值計算;沒有硬體 benchmark、autoscaler 或部署成本實測。

什麼時候用

  • 先垂直:流量還小、資料庫難切的時候,換大台最省事,不用改任何程式。
  • 再水平:接近單機上限、需要撐過單台故障,或流量起伏大想隨時加減機器的時候。
  • 應用伺服器容易水平擴充(只要無狀態);資料庫比較難,要靠讀取副本和分片。

和其他主題的關係

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

操作平均最差
水平擴充的成本對負載
n 是負載;每台小機器扛固定的量,再加一台備用
O(n)O(n)
垂直擴充的成本對負載
價格跟核心數成正比,吞吐量卻越來越不跟:128 核比 64 核貴一倍,只多 2% 的吞吐;超過最大台的上限就買不到了(∞)
≥ O(n)∞
壞一台後剩下的容量:垂直00
壞一台後剩下的容量:水平(N−1)/N(N−1)/N
無狀態伺服器每個請求多查 session
多一次網路往返,換來任何一台都能接請求
O(1)O(1)

空間:O(n),台數隨負載線性增加

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

數的是:n 是每秒請求數:水平擴充需要的台數與每小時成本(都超過任何單台的上限)

Big On = 10,000n = 100,000n = 1,000,000成長倍數:實測(理論)
台數(含一台備用)O(n)979781×87 (×100)
每小時成本(美元)O(n)1.815.8156×85 (×100)

垂直擴充沒辦法放進這張表:這三種負載都超過最大台機器能扛的量。

和其他做法比

垂直:規格・每小時垂直:每 1k QPS 每小時水平:台數・每小時水平:每 1k QPS 每小時壞一台後剩下的容量
每秒 1,0004 核 · $0.20$0.202 · $0.43$0.430 對 50%
每秒 5,00032 核 · $1.60$0.325 · $1.03$0.210 對 80%
每秒 10,000沒有夠大的—9 · $1.83$0.180 對 89%
每秒 50,000沒有夠大的—40 · $8.03$0.160 對 98%

表中的 QPS 是 Queries Per Second(每秒請求數)。假設:每核心每秒 500 個請求,依 USL(Universal Scalability Law)模型隨核心數遞減(序列化比例 0.03、一致性成本 0.0001);每核心每小時 $0.05;機器最多跑到 70% 忙;水平擴充多一台備用,負載平衡器每小時 $0.03。小負載時單台比較便宜,因為水平擴充要付備用機和負載平衡器的錢;負載一大,大機器每單位效率變差,水平擴充反而便宜,而且單台一壞就全停。

真實世界裡的它

  • 雲端的自動擴展群組(AWS Auto Scaling、Kubernetes 的 HPA〔Horizontal Pod Autoscaler〕)依 CPU 或請求數自動加減台數。
  • Stack Overflow 長年用少數幾台很大的 SQL Server 撐住整站,是垂直擴充走得很遠的例子。
  • 把登入狀態放進 Redis 或用簽章的 token(JWT,JSON Web Token),是讓網站伺服器能水平擴充的標準做法。

取捨與陷阱

  • 單台就是單點故障:機器一壞服務全停,換規格也要停機。
  • 有狀態的伺服器不能隨便加減:session、上傳到一半的檔案、本機快取都會隨機器消失。
  • 水平擴充把瓶頸推到下一層:應用伺服器加到一百台,資料庫還是只有一台。