測驗:擴展性設計 — 從單機到分散式
共 5 題,點選答案後會立即顯示結果
1. 垂直擴展(Scale Up)和水平擴展(Scale Out)的主要差異是什麼?
2. 在 Nginx 負載平衡設定中,least_conn 演算法的分配邏輯是什麼?
3. 為什麼水平擴展時,不應該將 Session 存在伺服器的記憶體裡?
4. 以下 Celery 程式碼中,.delay() 的作用是什麼?
5. 一個 5 人團隊正在開發一個新的電商平台,目前還沒上線,AI 建議將系統拆成 8 個微服務。根據文章的建議,這個做法合理嗎?
一句話說明
當一台伺服器撐不住流量時,你需要知道怎麼「加機器」而不是「加班除錯」。
前言:為什麼這篇跟你有關
你用 AI 產生了一個很棒的應用程式,本地跑得好好的。有一天你把它部署上線,突然湧入大量用戶 — 然後伺服器就掛了。
這篇文章不是要教你從頭設計分散式系統,而是讓你看懂那些擴展性相關的程式碼和架構圖在說什麼。當 AI 幫你產出 Nginx 設定、Celery 任務、或是資料庫分片邏輯時,你能判斷它寫得對不對。
前置知識
- 第 1-3 篇的所有內容(HTTP、API 設計、快取與資料庫基礎)
- 基本的命令列操作經驗
- 了解 Docker 基本概念(有幫助但非必要)
1. 為什麼需要擴展
先看一個典型場景。這是第 1 篇的短網址服務,跑在一台伺服器上:
用戶 ──→ [單一伺服器] ──→ [單一資料庫]
│
CPU: 95%
RAM: 7.8 / 8 GB
回應時間: 3200ms
翻譯:一台機器同時處理所有請求、跑所有程式、連同一個資料庫。當用戶變多,CPU 和記憶體爆掉,回應時間從 50ms 飆到 3200ms。
這就是需要「擴展」的時刻。擴展的核心問題只有一個:
怎麼讓系統在用戶增加時,依然維持可接受的回應速度?
2. 垂直擴展 vs 水平擴展
面對「撐不住」,有兩種思路:
垂直擴展(Scale Up):把機器變大
之前:2 CPU / 4GB RAM / 50GB SSD
之後:16 CPU / 64GB RAM / 1TB NVMe
翻譯:就是「換一台更強的電腦」。
水平擴展(Scale Out):加更多機器
之前:[伺服器 A]
之後:[伺服器 A] + [伺服器 B] + [伺服器 C]
↑
[負載平衡器] ←── 用戶
Code language: CSS (css)翻譯:不換電腦,而是「多買幾台一起扛」。
對照比較
| 比較項目 | 垂直擴展(Scale Up) | 水平擴展(Scale Out) |
|---|---|---|
| 做法 | 升級 CPU、記憶體、硬碟 | 增加伺服器數量 |
| 天花板 | 有(硬體有極限) | 理論上無限 |
| 成本曲線 | 指數成長(愈高階愈貴) | 線性成長(每台一樣價) |
| 複雜度 | 低(不用改程式) | 高(需要負載平衡、狀態管理) |
| 停機時間 | 通常需要停機升級 | 可以逐台加入,不中斷 |
| 適合場景 | 初期、資料庫、快速應急 | 長期、Web 伺服器、大規模 |
Vibe Coder 檢查點
看到擴展方案時確認:
- [ ] 目前的瓶頸是 CPU、記憶體、還是網路 I/O?
- [ ] 垂直擴展還能往上加嗎?(雲端通常有上限)
- [ ] 如果選水平擴展,程式碼有沒有「狀態」綁在單一機器上?
3. 負載平衡(Load Balancing)
水平擴展的第一步:你有多台伺服器了,誰來決定「這個請求送去哪台」?
答案是負載平衡器(Load Balancer)。
用戶請求 ──→ [負載平衡器] ──→ [伺服器 A]
├──→ [伺服器 B]
└──→ [伺服器 C]
Code language: CSS (css)L4 vs L7 負載平衡
| 類型 | 運作層級 | 看得到什麼 | 適合場景 |
|---|---|---|---|
| L4 | 傳輸層(TCP/UDP) | IP 位址、Port | 快速轉發、資料庫連線 |
| L7 | 應用層(HTTP) | URL 路徑、Header、Cookie | Web 應用、API 路由 |
翻譯:L4 只看「這個封包要去哪」,像快遞分揀員;L7 會拆開封包看內容,像能讀信的郵差。
常見演算法
Round Robin(輪流分配)
請求 1 → 伺服器 A
請求 2 → 伺服器 B
請求 3 → 伺服器 C
請求 4 → 伺服器 A ← 輪回來
翻譯:「排隊輪流,一人一個」。最簡單,但不考慮伺服器忙不忙。
Least Connections(最少連線)
伺服器 A: 15 個連線中
伺服器 B: 3 個連線中 ← 新請求送這裡
伺服器 C: 12 個連線中
翻譯:「誰最閒就給誰」。適合請求處理時間差異大的場景。
IP Hash(IP 雜湊)
用戶 IP: 192.168.1.100
hash(192.168.1.100) % 3 = 1 → 永遠送到伺服器 B
翻譯:「同一個用戶永遠去同一台」。解決 Session 問題(但不是最佳解)。
Nginx 負載平衡設定範例
這是你最常在專案裡看到的 Nginx 設定:
# /etc/nginx/conf.d/load-balancer.conf
upstream backend_servers { # 定義一組後端伺服器
least_conn; # 使用「最少連線」演算法
server 10.0.0.1:8000 weight=3; # 伺服器 A,權重 3(分到 3 倍流量)
server 10.0.0.2:8000 weight=1; # 伺服器 B,權重 1
server 10.0.0.3:8000 backup; # 伺服器 C,備用(其他都掛了才用)
}
server {
listen 80; # 監聽 80 埠
location / { # 所有請求
proxy_pass http://backend_servers; # 轉發到上面定義的伺服器群
proxy_set_header Host $host; # 保留原始的 Host 標頭
proxy_set_header X-Real-IP $remote_addr; # 傳遞用戶真實 IP
}
}
Code language: PHP (php)逐行翻譯:
| 設定 | 翻譯 |
|---|---|
upstream backend_servers |
「定義一群叫 backend_servers 的伺服器」 |
least_conn |
「用最少連線數的方式分配」 |
weight=3 |
「這台分到 3 倍的請求量」 |
backup |
「平常不用,其他都掛了才啟動」 |
proxy_pass |
「把請求轉送到那群伺服器」 |
健康檢查
負載平衡器怎麼知道某台伺服器掛了?靠健康檢查:
upstream backend_servers {
server 10.0.0.1:8000 max_fails=3 fail_timeout=30s;
# ↑ 連續失敗 3 次後,標記為不可用,30 秒後再試
}
Code language: PHP (php)翻譯:「每隔一段時間戳一下伺服器,連續三次沒回應就跳過它,30 秒後再看看有沒有活過來」。
Vibe Coder 檢查點
看到 Nginx/負載平衡設定時確認:
- [ ] 用的是哪種演算法?(
least_conn、ip_hash、還是預設的 round robin) - [ ] 有沒有設定健康檢查?(
max_fails、fail_timeout) - [ ] 有沒有備用伺服器?(
backup)
4. 無狀態設計原則
水平擴展有個大前提:伺服器不能把狀態存在自己身上。
有狀態的問題
請求 1 → 伺服器 A → 登入成功,Session 存在 A 的記憶體裡
請求 2 → 伺服器 B → 找不到 Session,要求重新登入!
翻譯:用戶登入後,下一個請求被分配到不同伺服器,結果又被踢回登入頁。
解決方案 1:JWT(無狀態 Token)
import jwt
from datetime import datetime, timedelta
# 登入時:產生 Token 給用戶
def create_token(user_id: int) -> str:
payload = {
"user_id": user_id, # 用戶 ID 直接寫在 Token 裡
"exp": datetime.utcnow() + timedelta(hours=24), # 24 小時後過期
}
return jwt.encode(payload, SECRET_KEY, algorithm="HS256")
# ↑ 用密鑰簽名,防止竄改
# 驗證時:任何伺服器都能驗,不需要查資料庫
def verify_token(token: str) -> dict:
return jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
# ↑ 只要有 SECRET_KEY,任何伺服器都能解開
Code language: PHP (php)翻譯:不在伺服器存 Session,而是把「你是誰」這個資訊加密後放在 Token 裡,用戶每次請求都帶著它。任何伺服器收到都能驗證,不需要「記住」用戶。
解決方案 2:Redis Session Store(集中存放)
import redis
r = redis.Redis(host="redis-server", port=6379) # 所有伺服器都連同一個 Redis
# 登入時:Session 存到 Redis
def save_session(session_id: str, user_data: dict):
r.setex(session_id, 3600, json.dumps(user_data))
# ↑ 存到 Redis,3600 秒(1 小時)後自動過期
# 驗證時:從 Redis 查
def get_session(session_id: str) -> dict:
data = r.get(session_id) # 從 Redis 讀取
return json.loads(data) if data else None
Code language: PHP (php)翻譯:Session 不存在任何一台伺服器上,而是存在「大家都能存取的共用 Redis」裡。不管請求被分到哪台伺服器,都能找到同一份 Session。
兩種方案對照
| 比較 | JWT | Redis Session |
|---|---|---|
| 狀態存放 | 用戶端(Token) | 集中式(Redis) |
| 伺服器負擔 | 低(只需驗證簽名) | 中(需查 Redis) |
| 可即時撤銷 | 難(Token 發出去就收不回) | 容易(刪掉 Redis 的紀錄) |
| 適合場景 | API、微服務 | 需要即時登出的場景 |
Vibe Coder 檢查點
看到認證相關程式碼時確認:
- [ ] 是用 JWT 還是 Session?
- [ ] 如果是 Session,存在哪裡?(記憶體?Redis?資料庫?)
- [ ] 記憶體裡的 Session 會讓水平擴展出問題嗎?
5. 資料庫擴展
伺服器加機器了,但資料庫還是只有一台 — 這就變成新的瓶頸。
讀寫分離(Primary-Replica)
寫入請求 ──→ [Primary 主庫]
│
│ 同步複製
├──→ [Replica 1] ←── 讀取請求
└──→ [Replica 2] ←── 讀取請求
Code language: CSS (css)翻譯:一台負責寫,多台負責讀。大多數應用程式「讀多寫少」(例如社群媒體 90% 是在刷貼文),所以這招很有效。
程式碼中的讀寫分離長這樣:
from sqlalchemy import create_engine
# 寫入用主庫
primary_engine = create_engine("postgresql://primary-db:5432/mydb")
# 讀取用副本
replica_engine = create_engine("postgresql://replica-db:5432/mydb")
def get_user(user_id: int):
with replica_engine.connect() as conn: # 讀取 → 用 replica
return conn.execute(
text("SELECT * FROM users WHERE id = :id"),
{"id": user_id}
).fetchone()
def create_user(name: str):
with primary_engine.connect() as conn: # 寫入 → 用 primary
conn.execute(
text("INSERT INTO users (name) VALUES (:name)"),
{"name": name}
)
conn.commit()
Code language: PHP (php)翻譯:程式裡建兩個資料庫連線,讀的時候用 replica_engine,寫的時候用 primary_engine。
分庫分表(Sharding)
當單一資料庫連讀寫分離都撐不住時,把資料「切開」放到不同資料庫:
用戶 ID 0-999999 → [資料庫 Shard 1]
用戶 ID 1000000-1999999 → [資料庫 Shard 2]
用戶 ID 2000000-2999999 → [資料庫 Shard 3]
Code language: CSS (css)程式碼裡會看到這種邏輯:
def get_shard(user_id: int) -> str:
shard_number = user_id % 3 # 用 ID 取餘數決定去哪個分片
return f"postgresql://shard-{shard_number}:5432/mydb"
# 翻譯:user_id 除以 3 的餘數決定資料在哪台資料庫
# user_id=100 → 100 % 3 = 1 → shard-1
# user_id=101 → 101 % 3 = 2 → shard-2
# user_id=102 → 102 % 3 = 0 → shard-0
Code language: PHP (php)翻譯:資料不再放在同一台,而是根據某個規則(通常是 ID)分散到多台資料庫。
重要提醒:Sharding 的複雜度非常高(跨分片查詢、資料遷移都是大問題)。除非用戶量達到千萬等級,否則先用讀寫分離 + 快取就夠了。
Vibe Coder 檢查點
看到資料庫相關架構時確認:
- [ ] 有沒有讀寫分離?寫入和讀取用的是不是不同連線?
- [ ] 如果有 Sharding,分片的 key 是什麼?(user_id?region?)
- [ ] Replica 的資料同步有沒有延遲?(會影響「剛寫入就讀取」的場景)
6. 非同步處理與訊息佇列
有些操作不需要(也不該)讓用戶等著。
同步 vs 非同步
同步(用戶等著):
用戶 → 註冊 → 寫入資料庫 → 寄歡迎信 → 產生頭像 → 回傳「註冊成功」
↑ 用戶等了 8 秒
非同步(用戶不等):
用戶 → 註冊 → 寫入資料庫 → 回傳「註冊成功」 → 用戶等了 0.5 秒
└→ 背景:寄歡迎信
└→ 背景:產生頭像
Code language: CSS (css)翻譯:「需要立刻回覆用戶的」同步做,「可以晚點做的」丟到背景。
訊息佇列(Message Queue)
訊息佇列是實現非同步的核心元件:
生產者 ──→ [訊息佇列] ──→ 消費者
(API) (Redis/RabbitMQ) (背景 Worker)
範例:
用戶註冊 ──→ 佇列放入「寄信」任務 ──→ 寄信 Worker 取出並處理
翻譯:就像餐廳的「出菜口」。廚師(生產者)把做好的菜放上去,服務生(消費者)自己去取。廚師不用等服務生,服務生不用等廚師。
Python + Celery + Redis 實作
這是你在 Python 專案中最常看到的非同步任務實作:
# tasks.py -- 定義背景任務
from celery import Celery
app = Celery("tasks", broker="redis://localhost:6379/0")
# ↑ 建立 Celery 應用,用 Redis 當訊息佇列
@app.task # 這個裝飾器讓函式變成「可以背景執行的任務」
def send_welcome_email(user_email: str):
# 寄信的邏輯(可能要 2-3 秒)
print(f"寄送歡迎信到 {user_email}")
@app.task
def generate_thumbnail(image_path: str):
# 圖片處理的邏輯(可能要 5-10 秒)
print(f"處理圖片 {image_path}")
Code language: PHP (php)# api.py -- API 端呼叫背景任務
from tasks import send_welcome_email, generate_thumbnail
@app.post("/register")
async def register(user: UserCreate):
# 1. 同步:寫入資料庫(必須馬上完成)
db_user = create_user_in_db(user)
# 2. 非同步:丟到佇列,不等結果
send_welcome_email.delay(user.email)
# ↑ .delay() 表示「丟到佇列背景執行,不要等」
generate_thumbnail.delay(user.avatar_path)
# 3. 立刻回覆用戶
return {"message": "註冊成功", "user_id": db_user.id}
Code language: PHP (php)逐行翻譯:
| 代碼 | 翻譯 |
|---|---|
Celery("tasks", broker="redis://...") |
「用 Redis 當任務佇列」 |
@app.task |
「這個函式可以被放到背景執行」 |
send_welcome_email.delay(email) |
「把寄信任務丟到佇列,不等它完成」 |
啟動 Worker 的指令:
celery -A tasks worker --loglevel=info
# ↑ 啟動一個 Worker,它會不斷從佇列取出任務並執行
# -A tasks 表示「去 tasks.py 找任務定義」
Code language: PHP (php)訊息佇列解決的兩大問題
問題 1:耦合 — 服務之間不直接呼叫,透過佇列溝通
緊耦合(直接呼叫):
註冊服務 → 直接呼叫寄信服務 → 寄信服務掛了,註冊也失敗
鬆耦合(透過佇列):
註冊服務 → 丟任務到佇列 → 寄信服務掛了?任務留在佇列,等它恢復再處理
問題 2:峰值 — 用佇列「削峰填谷」
沒有佇列:
湧入 10000 請求 → 伺服器同時處理 → 爆炸
有佇列:
湧入 10000 請求 → 全部放進佇列 → Worker 每秒處理 100 個 → 穩穩消化
Vibe Coder 檢查點
看到非同步任務程式碼時確認:
- [ ] 哪些操作是同步的(用戶在等)?哪些是非同步的(背景執行)?
- [ ]
.delay()的任務失敗了怎麼辦?有沒有重試機制? - [ ] 佇列的 broker 是什麼?(Redis、RabbitMQ、SQS?)
7. 微服務 vs 單體架構
「要不要把系統拆成微服務?」是最常見的架構決策之一。
單體架構(Monolith)
[一個應用程式]
├── 用戶模組
├── 訂單模組
├── 支付模組
└── 通知模組
↓
[一個資料庫]
Code language: CSS (css)翻譯:所有功能在同一個程式裡,部署時整包一起部署。
微服務架構(Microservices)
[用戶服務] ──→ [用戶 DB]
[訂單服務] ──→ [訂單 DB]
[支付服務] ──→ [支付 DB]
[通知服務] ──→ [通知 DB]
↕
[訊息佇列 / API Gateway]
Code language: CSS (css)翻譯:每個功能是獨立的小程式,有自己的資料庫,透過 API 或訊息佇列溝通。
什麼時候該拆、什麼時候不該拆
| 情境 | 建議 |
|---|---|
| 團隊 < 10 人 | 單體。拆了反而增加溝通成本 |
| 部署頻率不同 | 可以拆。支付模組每月更新一次,用戶模組每天更新 |
| 技術棧不同 | 可以拆。推薦引擎用 Python,API 用 Go |
| 擴展需求不同 | 可以拆。搜尋服務需要 20 台,用戶服務只要 2 台 |
| 剛起步的新專案 | 單體。先跑起來再說 |
**Conway’s Law**:「組織的系統設計會反映組織的溝通結構。」
白話翻譯:如果你的團隊是一個人,硬拆微服務只是在折磨自己。三個團隊各管一塊功能?那自然就會長成三個微服務。
Vibe Coder 檢查點
看到架構討論時確認:
- [ ] 現在是單體還是微服務?
- [ ] 如果 AI 建議拆微服務,團隊規模撐得起來嗎?
- [ ] 服務之間怎麼通訊?(HTTP API?gRPC?訊息佇列?)
8. 監控與可觀測性
系統從 1 台變成 10 台後,出了問題你要怎麼找到是哪台、哪個環節出錯?
可觀測性三大支柱
[Metrics] [Logs] [Traces]
數字指標 文字紀錄 請求追蹤
「多少」 「發生什麼」 「經過哪裡」
Code language: CSS (css)Metrics(指標):系統的健康數字
# 用 prometheus_client 暴露指標
from prometheus_client import Counter, Histogram
request_count = Counter(
"http_requests_total", # 指標名稱
"Total HTTP requests", # 說明
["method", "endpoint", "status"] # 標籤(可以分類統計)
)
request_duration = Histogram(
"http_request_duration_seconds", # 請求耗時
"HTTP request duration",
)
@app.middleware("http")
async def metrics_middleware(request, call_next):
start = time.time()
response = await call_next(request)
duration = time.time() - start
request_count.labels(
method=request.method,
endpoint=request.url.path,
status=response.status_code
).inc() # 計數 +1
request_duration.observe(duration) # 記錄耗時
return response
Code language: PHP (php)翻譯:每個請求進來時自動記錄「總數」和「花了多久」,讓監控系統(如 Grafana)可以畫成圖表。
Logs(日誌):發生了什麼事
import logging
import json
logger = logging.getLogger(__name__)
# 結構化日誌(方便搜尋和分析)
logger.info(json.dumps({
"event": "user_registered",
"user_id": 12345,
"email": "alice@example.com",
"duration_ms": 234,
"server": "web-03" # 標記是哪台伺服器
}))
Code language: PHP (php)翻譯:不是隨便 print(),而是用結構化的格式記錄事件,讓你可以在多台伺服器的日誌裡搜尋「所有來自 web-03 且耗時超過 1000ms 的註冊事件」。
Traces(追蹤):一個請求的完整旅程
[請求 abc-123]
├── API Gateway (2ms)
├── 用戶服務 (15ms)
│ ├── Redis 查快取 (1ms) - MISS
│ └── PostgreSQL 查詢 (12ms)
├── 訂單服務 (45ms) ← 瓶頸在這
│ └── MongoDB 查詢 (40ms)
└── 回應用戶 (total: 62ms)
Code language: CSS (css)翻譯:追蹤一個請求「經過了哪些服務、每一站花了多久」。當系統變慢時,你一眼就能看到瓶頸在哪。
為什麼擴展後監控更重要
| 單機時代 | 分散式時代 |
|---|---|
tail -f app.log 就能看日誌 |
10 台伺服器,日誌散落各處 |
| CPU 高就是你的程式有問題 | 可能是 A 服務打爆了 B 服務 |
| 重啟就好 | 重啟哪台?會不會影響其他服務? |
Vibe Coder 檢查點
看到監控相關程式碼時確認:
- [ ] 有沒有 Metrics?(能看到系統的即時數字)
- [ ] 日誌是不是結構化的?(JSON 格式比純文字好搜尋)
- [ ] 有沒有 Tracing?(分散式系統幾乎必備)
- [ ] 監控資料存在哪裡?(Prometheus、ELK、Datadog?)
9. 綜合範例:短網址服務的擴展之路
讓我們把第 1 篇的短網址服務,從「一台伺服器」擴展為「支撐百萬用戶」的架構。
階段 1:單機版(0-1000 用戶)
用戶 ──→ [FastAPI 伺服器] ──→ [PostgreSQL]
Code language: CSS (css)# 最簡單的短網址服務
@app.post("/shorten")
async def shorten(url: str):
short_code = generate_code()
db.execute("INSERT INTO urls (code, original) VALUES (?, ?)",
(short_code, url))
return {"short_url": f"https://sho.rt/{short_code}"}
@app.get("/{code}")
async def redirect(code: str):
result = db.execute("SELECT original FROM urls WHERE code = ?", (code,))
return RedirectResponse(result["original"])
Code language: PHP (php)階段 2:加快取 + 讀寫分離(1000-10 萬用戶)
用戶 ──→ [FastAPI] ──→ [Redis 快取]
│ ↓ miss
│ [Replica DB] (讀取)
└──→ [Primary DB] (寫入)
Code language: CSS (css)import redis
cache = redis.Redis(host="redis-server")
@app.get("/{code}")
async def redirect(code: str):
# 1. 先查快取
cached = cache.get(f"url:{code}")
if cached:
return RedirectResponse(cached.decode())
# 2. 快取沒有,查 Replica 資料庫
result = replica_db.execute(
"SELECT original FROM urls WHERE code = ?", (code,)
)
# 3. 寫入快取,下次就不用查資料庫了
cache.setex(f"url:{code}", 3600, result["original"])
return RedirectResponse(result["original"])
Code language: PHP (php)階段 3:水平擴展 + 負載平衡(10 萬-100 萬用戶)
用戶 ──→ [Nginx 負載平衡器]
├──→ [FastAPI 伺服器 1]
├──→ [FastAPI 伺服器 2] ──→ [Redis 快取叢集]
└──→ [FastAPI 伺服器 3] ↓ miss
[DB Primary]
[DB Replica 1]
[DB Replica 2]
Code language: CSS (css)Nginx 設定:
upstream shorturl_api {
least_conn;
server 10.0.0.1:8000;
server 10.0.0.2:8000;
server 10.0.0.3:8000;
}
server {
listen 80;
location / {
proxy_pass http://shorturl_api;
proxy_set_header X-Real-IP $remote_addr;
}
}
Code language: PHP (php)階段 4:非同步處理 + 監控(100 萬+ 用戶)
用戶 ──→ [CDN / Nginx]
├──→ [API 伺服器群]
│ ├──→ [Redis 快取叢集]
│ ├──→ [DB Primary + Replicas]
│ └──→ [訊息佇列 (Redis)]
│ ↓
│ [Celery Workers]
│ ├── 統計分析
│ └── 通知服務
│
└──→ [Prometheus + Grafana] (監控)
Code language: CSS (css)# 點擊統計改為非同步
@app.get("/{code}")
async def redirect(code: str):
url = cache.get(f"url:{code}")
if not url:
url = await get_from_db(code)
cache.setex(f"url:{code}", 3600, url)
# 統計點擊次數 -- 非同步處理,不讓用戶等
track_click.delay(code, request.client.host)
return RedirectResponse(url)
@celery_app.task
def track_click(code: str, ip: str):
# 這個在背景慢慢做,不影響用戶體驗
db.execute(
"INSERT INTO clicks (code, ip, clicked_at) VALUES (?, ?, NOW())",
(code, ip)
)
Code language: PHP (php)擴展路線圖總結
| 階段 | 用戶量 | 關鍵技術 | 複雜度 |
|---|---|---|---|
| 1 | 0-1K | 單機、單資料庫 | 低 |
| 2 | 1K-100K | Redis 快取、讀寫分離 | 中 |
| 3 | 100K-1M | 負載平衡、水平擴展、無狀態 | 中高 |
| 4 | 1M+ | 非同步處理、監控、CDN | 高 |
重要原則:**不要過早優化**。先用最簡單的架構上線,遇到瓶頸再解決。90% 的應用在階段 2 就夠用了。
10. 必看懂 vs 知道就好
必看懂(會一直出現)
- 垂直擴展 vs 水平擴展的差異
- Nginx 負載平衡基本設定(
upstream、proxy_pass) - 無狀態設計原則(JWT、Redis Session)
- 非同步任務的
.delay()呼叫 - 讀寫分離的程式碼模式(兩個 engine)
知道就好(遇到再查)
- 一致性雜湊(Consistent Hashing):更智慧的分片方式,加減節點時不用大搬家
- 服務網格(Service Mesh):微服務之間通訊的基礎設施,如 Istio
- CQRS:Command Query Responsibility Segregation,讀寫用不同的資料模型
- Saga Pattern:微服務之間的分散式交易管理
- CAP 定理:分散式系統的一致性、可用性、分區容忍性三選二
本篇總結
| 概念 | 一句話翻譯 |
|---|---|
| 垂直擴展 | 換更大的機器 |
| 水平擴展 | 加更多的機器 |
| 負載平衡 | 決定請求送去哪台伺服器 |
| 無狀態設計 | 別把資料存在單一伺服器的記憶體裡 |
| 讀寫分離 | 一台寫、多台讀 |
| Sharding | 資料依規則切開,分散到不同資料庫 |
| 訊息佇列 | 任務排隊,背景慢慢做 |
| 微服務 | 功能拆成獨立的小程式 |
| 監控三支柱 | Metrics(數字)、Logs(紀錄)、Traces(追蹤) |
系列總回顧
這是【工程師的系統設計】系列的最後一篇。四篇文章帶你走過了:
- #01 基礎概念:HTTP、API、Client-Server 架構
- #02 API 設計:RESTful、狀態碼、版本管理
- #03 快取與資料庫:Redis、SQL vs NoSQL、快取策略
- #04 擴展性設計:負載平衡、非同步處理、微服務(本篇)
作為 Vibe Coder,你不需要從頭設計這些系統,但你需要看懂 AI 產生的架構和程式碼。當你讀到 Nginx 設定、Celery 任務、或資料庫分片邏輯時,希望你不再覺得陌生,而是能自信地判斷:「這寫得合理」或「這裡有問題」。
系統設計沒有完美的解答,只有在當下情境中最合適的取捨。記住那個最重要的原則:先讓它跑起來,再讓它跑得快。
進階測驗:擴展性設計 — 從單機到分散式
共 5 題,包含情境題與錯誤診斷題。