測驗:系統設計思維入門 — 從需求到架構圖
共 5 題,點選答案後會立即顯示結果
1. 「寫程式」和「系統設計」的核心差異是什麼?
2. 下列哪一項屬於「非功能性需求」?
3. 在短網址服務的估算中,假設 DAU 為 1,000 萬、每人每天產生 0.1 個短網址、讀寫比 100:1,則高峰期的讀取 QPS 大約是多少?
4. 在架構圖中,Cache(快取)的主要作用是什麼?
5. 在短網址服務的 FastAPI 範例中,簡易版使用 Python dict 當資料庫。為什麼正式版需要換成 PostgreSQL?
一句話說明
系統設計就是決定「哪些元件、怎麼組合」,讓服務在大量使用者下依然穩定運作。
前言:寫程式跟系統設計有什麼不同?
很多人能寫出一個功能完整的短網址服務,但被問到「如果每天有一億人用呢?」就卡住了。
這就是系統設計要解決的問題。
| 寫程式 | 系統設計 |
|---|---|
| 解決「怎麼做」 | 解決「怎麼組合」 |
| 一台電腦、一個程式 | 多台機器、多個服務協作 |
| 關注正確性 | 關注規模與可靠性 |
| 輸入 → 輸出 | 需求 → 架構 → 元件 → 連接方式 |
翻譯:寫程式是蓋一間房間,系統設計是規劃一棟大樓 — 要考慮電梯、水管、逃生路線怎麼配置。
第一步:需求分析 — 搞清楚到底要做什麼
系統設計的起點永遠是需求。需求分兩種:
功能性需求(Functional Requirements)
「系統要能做什麼事」:
短網址服務的功能性需求:
1. 給一個長網址 → 產生一個短網址
2. 訪問短網址 → 重新導向到原始長網址
3. (可選)可以自訂短網址
4. (可選)短網址有過期時間
一句話:功能性需求就是「使用者能做什麼」。
非功能性需求(Non-Functional Requirements)
「系統要做到什麼程度」:
| 指標 | 意思 | 範例 |
|---|---|---|
| 延遲(Latency) | 回應速度 | 短網址轉址要在 100ms 內完成 |
| 吞吐量(Throughput) | 每秒處理量 | 每秒能處理 10,000 次轉址 |
| 可用性(Availability) | 不掛掉的時間比例 | 99.9% 時間可用(一年最多掛 8.7 小時) |
| 一致性(Consistency) | 資料正確性 | 建立的短網址馬上就能用 |
一句話:非功能性需求就是「系統要有多快、多穩、多大」。
Vibe Coder 檢查點
看到系統設計討論時確認:
- [ ] 功能性需求有列清楚嗎?(使用者能做什麼)
- [ ] 非功能性需求有量化嗎?(不是「要快」,而是「100ms 內」)
- [ ] 有沒有遺漏的邊界情況?(過期、重複、錯誤處理)
第二步:估算思維 — 數字會說話
面試或實戰中,拿到需求後第一件事就是「估算」。這叫 Back-of-the-envelope estimation(信封背面估算),不需要精確,但要有量級概念。
短網址服務的估算範例
假設條件:
- DAU(每日活躍使用者):1,000 萬
- 每個使用者每天產生 0.1 個短網址(大部分人只是讀取)
- 讀寫比:100:1(讀取遠多於寫入)
Code language: CSS (css)開始算:
寫入 QPS(每秒寫入量):
1,000 萬 x 0.1 / 86,400 秒 ≈ 12 QPS
讀取 QPS(每秒讀取量):
12 x 100 = 1,200 QPS
高峰期(尖峰約為平均的 2~5 倍):
寫入:60 QPS
讀取:6,000 QPS
儲存量:
每筆資料大約:
短網址(7 字元)+ 長網址(平均 200 字元)+ 建立時間 ≈ 250 bytes
每天新增:100 萬筆
每年新增:3.65 億筆
5 年儲存:3.65 億 x 5 x 250 bytes ≈ 456 GB
Code language: CSS (css)常用數字速查表
記住這些數字,估算時很好用:
| 項目 | 數值 |
|---|---|
| 一天有幾秒 | ~86,400(記成 ~10 萬) |
| 一年有幾秒 | ~3,100 萬(記成 ~3 x 10^7) |
| 1 KB | 1,000 bytes |
| 1 MB | 1,000 KB |
| 1 GB | 1,000 MB |
| 1 TB | 1,000 GB |
一句話:估算不需要精確到個位數,只要知道量級是「幾百 QPS」還是「幾萬 QPS」。
第三步:畫架構圖 — 系統設計的核心輸出
架構圖是系統設計最重要的產出。以下是最常見的元件:
基本架構元件
Client(使用者端)
↓
DNS(域名解析)
↓
Load Balancer(負載均衡器)
↓ ↓ ↓
Web Server Web Server Web Server(多台分散流量)
↓
Application Server(應用邏輯)
↓ ↓
Database Cache(快取)
每個元件翻譯:
| 元件 | 翻譯 | 一句話說明 |
|---|---|---|
| Client | 使用者端 | 瀏覽器或 App |
| DNS | 域名解析 | 把 bit.ly 轉成 IP 位址 |
| Load Balancer | 負載均衡器 | 把流量分散到多台伺服器 |
| Web Server | 網頁伺服器 | 接收 HTTP 請求 |
| Application Server | 應用伺服器 | 執行商業邏輯 |
| Database | 資料庫 | 永久儲存資料 |
| Cache | 快取 | 暫存常用資料,加速讀取 |
為什麼需要這些元件?
問題:一台伺服器撐不住
解法:Load Balancer + 多台伺服器
問題:每次都查資料庫太慢
解法:Cache 暫存熱門資料
問題:資料庫掛了全部完蛋
解法:資料庫做主從複製(Replica)
翻譯:架構圖就是畫出「請求從使用者出發,經過哪些站,最後拿到結果」的路線圖。
第四步:實戰範例 — 短網址服務的完整設計
現在用短網址服務走一遍完整流程。
4.1 API 設計
POST /api/shorten
請求:{ "long_url": "https://very-long-url.com/..." }
回應:{ "short_url": "https://short.ly/abc123" }
GET /{short_code}
行為:302 重新導向到原始長網址
Code language: JavaScript (javascript)翻譯:兩個 API — 一個「產生短網址」,一個「用短網址轉址」。
4.2 資料模型
urls 表格:
+----+-----------+----------------------------------+-----------+
| id | short_code| long_url | created_at|
+----+-----------+----------------------------------+-----------+
| 1 | abc123 | https://very-long-url.com/... | 2024-01-01|
+----+-----------+----------------------------------+-----------+
索引:short_code(唯一索引) ← 查詢時靠這個快速找到
Code language: JavaScript (javascript)4.3 短碼產生策略
最常見的方式是用 Base62 編碼:
import string
# Base62 字元集:a-z, A-Z, 0-9 共 62 個字元
CHARS = string.ascii_letters + string.digits # 'abcdefg...XYZ012...9'
def encode_base62(num: int) -> str:
"""把數字轉成 Base62 字串"""
if num == 0:
return CHARS[0]
result = []
while num > 0:
result.append(CHARS[num % 62]) # 取餘數對應字元
num //= 62 # 整數除法往下一位
return ''.join(reversed(result)) # 反轉得到最終結果
# 範例
print(encode_base62(1)) # → 'b'
print(encode_base62(12345)) # → 'dnh'
print(encode_base62(999999999)) # → 'bLY2Mn'
Code language: PHP (php)逐行翻譯:
CHARS = string.ascii_letters + string.digits
# ↑ 建立字元集:"abcdef...XYZ0123456789",共 62 個字元
def encode_base62(num: int) -> str:
# ↑ 接收一個數字,回傳一個字串
result.append(CHARS[num % 62])
# ↑ num 除以 62 的餘數,對應到字元集裡的某個字元
num //= 62
# ↑ 整數除法,處理下一位(就像十進位轉二進位的過程)
Code language: PHP (php)一句話:Base62 就是「把數字轉成用 62 個字元表示的短字串」,7 個字元就能表示 62^7 = 3.5 兆個網址。
4.4 最小可運行範例:FastAPI 短網址服務
以下是一個可以直接跑的最簡版本:
from fastapi import FastAPI, HTTPException
from fastapi.responses import RedirectResponse
from pydantic import BaseModel
import string
app = FastAPI()
# --- 資料模型 ---
class ShortenRequest(BaseModel):
long_url: str # 必須提供長網址
class ShortenResponse(BaseModel):
short_url: str # 回傳短網址
# --- 簡易版:用 dict 當資料庫 ---
url_db: dict[str, str] = {} # { "abc123": "https://long-url.com/..." }
counter = 0 # 自增 ID,用來產生短碼
# --- Base62 編碼 ---
CHARS = string.ascii_letters + string.digits
def encode_base62(num: int) -> str:
if num == 0:
return CHARS[0]
result = []
while num > 0:
result.append(CHARS[num % 62])
num //= 62
return ''.join(reversed(result))
# --- API 1:產生短網址 ---
@app.post("/api/shorten", response_model=ShortenResponse)
def shorten(req: ShortenRequest):
global counter
counter += 1
short_code = encode_base62(counter) # 用自增 ID 產生短碼
url_db[short_code] = req.long_url # 存進「資料庫」
return ShortenResponse(short_url=f"http://localhost:8000/{short_code}")
# --- API 2:短網址轉址 ---
@app.get("/{short_code}")
def redirect(short_code: str):
long_url = url_db.get(short_code) # 從「資料庫」查找
if not long_url:
raise HTTPException(status_code=404, detail="Short URL not found")
return RedirectResponse(url=long_url) # 302 重新導向
逐行翻譯重點部分:
url_db: dict[str, str] = {}
# ↑ 用 Python 字典模擬資料庫,key 是短碼,value 是長網址
counter = 0
# ↑ 簡單的自增 ID,每次建立短網址就 +1
@app.post("/api/shorten", response_model=ShortenResponse)
# ↑ POST 請求到 /api/shorten,回傳格式是 ShortenResponse
def shorten(req: ShortenRequest):
# ↑ FastAPI 自動把 request body 解析成 ShortenRequest 物件
short_code = encode_base62(counter)
# ↑ 把自增 ID 轉成 Base62 短碼
return RedirectResponse(url=long_url)
# ↑ 回傳 302 重新導向,瀏覽器會自動跳轉到長網址
Code language: PHP (php)4.5 從程式碼對應到架構圖
這個最簡版只用了架構圖裡的一小部分:
架構圖元件 → 程式碼對應
─────────────────────────────────────────
Client → 瀏覽器發出 HTTP 請求
Web Server → FastAPI(app = FastAPI())
Application Server → shorten() 和 redirect() 函式
Database → url_db 字典(正式版換成 PostgreSQL)
Cache → (這個簡易版沒有,正式版會加 Redis)
Load Balancer → (這個簡易版沒有,正式版會加 Nginx)
翻譯:最簡版是「一台機器跑所有東西」。正式版要把每個元件獨立出來,各司其職。
第五步:從簡易版到正式版 — 需要考慮什麼?
| 簡易版 | 正式版 | 為什麼 |
|---|---|---|
dict 當資料庫 |
PostgreSQL 或 DynamoDB | 重啟不會遺失資料 |
counter 自增 ID |
分散式 ID 產生器(如 Snowflake) | 多台伺服器不會產生重複 ID |
| 沒有快取 | Redis 快取熱門短網址 | 減少資料庫查詢,加速讀取 |
| 單一伺服器 | Load Balancer + 多台伺服器 | 分散流量,避免單點故障 |
| 沒有監控 | 加入日誌、指標、告警 | 出問題時能快速定位 |
正式版架構圖
使用者
↓
DNS(解析 short.ly)
↓
Load Balancer(Nginx)
↓ ↓ ↓
App Server 1 App Server 2 App Server 3
↓ ↓ ↓
↓ ← Redis Cache → ↓
↓ ↓
└────→ PostgreSQL (主) ←────────┘
↓
PostgreSQL (從) ← 讀取流量分散到從資料庫
Code language: CSS (css)翻譯:
- 使用者的請求先到 Load Balancer,分配到某台 App Server
- App Server 先查 Redis Cache,有就直接回
- Cache 沒有的話,查 PostgreSQL 資料庫
- 資料庫有主從架構,寫入走主庫,讀取可以走從庫
必看懂 vs 知道就好
必看懂(系統設計面試必考)
- [ ] 功能性需求 vs 非功能性需求的區別
- [ ] 基本估算:DAU、QPS、儲存量怎麼算
- [ ] 架構圖基本元件:Client、Load Balancer、Server、Database、Cache
- [ ] 為什麼需要 Cache 和 Load Balancer
知道就好(遇到再深入)
- CAP 定理:一致性、可用性、分區容錯性不可能同時滿足
- 一致性雜湊(Consistent Hashing):分散資料到多台伺服器的策略
- 訊息佇列(Message Queue):非同步處理任務的元件
- CDN:靜態內容分散到全球節點加速
- 資料庫分片(Sharding):把一個大資料庫拆成多個小資料庫
Vibe Coder 檢查點
看到系統設計相關的程式碼或架構圖時:
- [ ] 能說出每個元件的作用嗎?(Load Balancer 幹嘛的?Cache 幹嘛的?)
- [ ] 資料從使用者出發,經過哪些站,最後到哪裡?
- [ ] 有沒有單點故障?(如果某台機器掛了,系統還能動嗎?)
- [ ] 讀寫比是多少?讀多還是寫多?(決定要不要加 Cache)
- [ ] 預估的 QPS 和儲存量是多少量級?
小結
系統設計的思考流程:
需求分析 → 估算量級 → 畫架構圖 → API 設計 → 資料模型 → 討論取捨
這篇用短網址服務走了一遍完整流程。下一篇我們會深入「資料庫選型」– 什麼時候該用 SQL,什麼時候該用 NoSQL,以及如何設計 Schema。
記住:系統設計沒有標準答案,重要的是展示你的思考過程和取捨判斷。
進階測驗:系統設計思維入門 — 從需求到架構圖
共 5 題,包含情境題與錯誤診斷題。