【工程師的系統設計】#01 系統設計思維入門:從需求到架構圖

測驗:系統設計思維入門 — 從需求到架構圖

共 5 題,點選答案後會立即顯示結果

1. 「寫程式」和「系統設計」的核心差異是什麼?

  • A. 寫程式關注效能,系統設計關注正確性
  • B. 寫程式解決「怎麼做」,系統設計解決「怎麼組合」
  • C. 寫程式用一種語言,系統設計用多種語言
  • D. 寫程式不需要架構,系統設計不需要程式碼

2. 下列哪一項屬於「非功能性需求」?

  • A. 使用者可以輸入長網址,產生短網址
  • B. 訪問短網址時要重新導向到原始網址
  • C. 系統 99.9% 的時間必須可用,一年最多掛 8.7 小時
  • D. 短網址可以設定過期時間

3. 在短網址服務的估算中,假設 DAU 為 1,000 萬、每人每天產生 0.1 個短網址、讀寫比 100:1,則高峰期的讀取 QPS 大約是多少?

  • A. 1,200 QPS
  • B. 6,000 QPS
  • C. 12,000 QPS
  • D. 60 QPS

4. 在架構圖中,Cache(快取)的主要作用是什麼?

  • A. 永久儲存所有資料,取代資料庫
  • B. 暫存常用資料,減少資料庫查詢以加速讀取
  • C. 把流量分散到多台伺服器
  • D. 把域名轉成 IP 位址

5. 在短網址服務的 FastAPI 範例中,簡易版使用 Python dict 當資料庫。為什麼正式版需要換成 PostgreSQL?

url_db: dict[str, str] = {} # { “abc123”: “https://long-url.com/…” }
  • A. dict 的查詢速度太慢,PostgreSQL 比較快
  • B. dict 無法儲存字串類型的 value
  • C. dict 存在記憶體中,重啟後資料會遺失
  • D. dict 不支援 Base62 編碼的 key

一句話說明

系統設計就是決定「哪些元件、怎麼組合」,讓服務在大量使用者下依然穩定運作。


前言:寫程式跟系統設計有什麼不同?

很多人能寫出一個功能完整的短網址服務,但被問到「如果每天有一億人用呢?」就卡住了。

這就是系統設計要解決的問題。

寫程式 系統設計
解決「怎麼做」 解決「怎麼組合」
一台電腦、一個程式 多台機器、多個服務協作
關注正確性 關注規模與可靠性
輸入 → 輸出 需求 → 架構 → 元件 → 連接方式

翻譯:寫程式是蓋一間房間,系統設計是規劃一棟大樓 — 要考慮電梯、水管、逃生路線怎麼配置。


第一步:需求分析 — 搞清楚到底要做什麼

系統設計的起點永遠是需求。需求分兩種:

功能性需求(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.65x 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 BalancerNginx)
  ↓  ↓  ↓
App Server 1  App Server 2  App Server 3
  ↓                ↓              ↓
  ↓      ←    Redis Cache    →   ↓
  ↓                                ↓
  └────→ PostgreSQL (主) ←────────┘
              ↓
         PostgreSQL (從)  ← 讀取流量分散到從資料庫
Code language: CSS (css)

翻譯

  1. 使用者的請求先到 Load Balancer,分配到某台 App Server
  2. App Server 先查 Redis Cache,有就直接回
  3. Cache 沒有的話,查 PostgreSQL 資料庫
  4. 資料庫有主從架構,寫入走主庫,讀取可以走從庫

必看懂 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 題,包含情境題與錯誤診斷題。

1. 你的團隊正在設計一個圖片分享平台。PM 跟你說:「使用者要能上傳圖片、瀏覽其他人的圖片,而且系統不能太慢。」在開始畫架構圖之前,你應該先做什麼? 情境題

  • A. 立刻選擇使用 AWS S3 儲存圖片,因為這是業界標準
  • B. 把「不能太慢」量化成具體指標,例如圖片載入延遲在 200ms 內,並確認 DAU、QPS 等數字
  • C. 先決定資料庫用 SQL 還是 NoSQL
  • D. 先研究競品的技術架構,直接複製他們的設計

2. 你在設計一個線上投票系統,預估 DAU 為 500 萬,每人每天投 2 票,讀寫比為 50:1。你需要估算高峰期的讀取 QPS 來決定需要幾台伺服器。以下哪個估算最接近? 情境題

提示:一天約 86,400 秒,高峰期約為平均的 5 倍
  • A. 約 580 QPS
  • B. 約 5,800 QPS
  • C. 約 29,000 QPS
  • D. 約 290,000 QPS

3. 你負責的短網址服務上線後,發現讀取 QPS 高達 8,000,但其中 80% 的請求都集中在最近 24 小時建立的短網址。為了降低資料庫壓力,你應該優先採取哪個措施? 情境題

  • A. 增加更多資料庫從庫(Read Replica),分散讀取流量
  • B. 在 App Server 和 Database 之間加入 Redis Cache,快取熱門短網址
  • C. 把資料庫從 PostgreSQL 換成 DynamoDB
  • D. 增加 Load Balancer 的數量

4. 小明用文章中的 FastAPI 短網址範例部署到生產環境,跑了一段時間後重啟伺服器,結果所有短網址都失效了。以下是他的核心程式碼,問題出在哪裡? 錯誤診斷

url_db: dict[str, str] = {} # 儲存短網址對應 counter = 0 # 自增 ID @app.post(“/api/shorten”, response_model=ShortenResponse) def shorten(req: ShortenRequest): global counter counter += 1 short_code = encode_base62(counter) url_db[short_code] = req.long_url return ShortenResponse(short_url=f”http://short.ly/{short_code}”)
  • A. encode_base62 函式產生了重複的短碼
  • B. url_db 是記憶體中的 dict,重啟後資料全部遺失,應使用持久化資料庫
  • C. 沒有使用 async def 導致伺服器阻塞崩潰
  • D. global counter 在多執行緒環境下會產生競爭條件

5. 團隊部署了三台 App Server 處理短網址服務,但發現部分使用者建立短網址後,訪問短網址卻收到 404 錯誤。架構如下,問題最可能出在哪裡? 錯誤診斷

使用者 → Load Balancer → App Server 1 (counter=100, url_db={…}) → App Server 2 (counter=100, url_db={…}) → App Server 3 (counter=100, url_db={…}) 每台 App Server 各自維護自己的 url_db (dict) 和 counter
  • A. Load Balancer 的分流演算法有問題,應改用固定 IP 分配
  • B. Base62 編碼在不同機器上產生不同結果
  • C. 每台伺服器用各自的記憶體存資料,建立短網址的伺服器和查詢的伺服器不同,導致找不到資料;應改用共享的外部資料庫
  • D. DNS 解析延遲導致請求逾時

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *