【從一台 Server 到分散式架構】第 31 篇:從面試題反推架構——用本系列思維拆解系統設計考題
走完這三十篇,小明回頭看了一眼自己一開始問的問題:
「什麼時候該用 Redis?什麼時候要加 Kafka?為什麼要有 API Gateway?」
他發現,現在他的問題不一樣了:
「這個系統的瓶頸在哪裡?讀多還是寫多?要強一致還是最終一致?要即時還是可以非同步?」
這個思維方式,也是系統設計面試考的核心。
系統設計面試考的是什麼
面試官問「設計一個 XXX 系統」,考的不是:
- ❌ 你背了多少架構圖
- ❌ 你知不知道 Zookeeper 是什麼
- ❌ 你能不能畫出完整的架構
考的是:
- ✅ 你會不會從需求推導架構
- ✅ 你知不知道每個設計決策的取捨
- ✅ 你能不能清楚溝通你的思考過程
好的面試表現,是讓面試官跟著你的思路走,而不是讓他看你默寫答案。
可重複使用的面試框架:五個步驟
步驟一:釐清需求(5 分鐘)
問題比答案更重要。
不要拿到題目就開始畫架構。先問清楚:
功能需求(Functional Requirements):
- 核心功能是什麼?(這個系統要做什麼)
- 有哪些使用者角色?
- 有沒有特殊功能?(例如:訊息系統要支援群組嗎?要有已讀回條嗎?)
非功能需求(Non-functional Requirements):
- 預期規模?(多少用戶、多少 QPS)
- 讀多還是寫多?
- 對延遲的要求?(需要即時嗎?)
- 對一致性的要求?(可以接受短暫不一致嗎?)
- 可用性要求?(99.9% 還是 99.99%)
釐清需求,是避免「設計了一個沒人要的系統」的第一步。
步驟二:估算規模(3 分鐘)
拿到規模需求後,做一些 back-of-envelope 估算:
- DAU(每日活躍用戶)× 每人每天的操作次數 → 每日請求總量
- 每日請求 / 86400 秒 → 平均 QPS
- 峰值 QPS ≈ 平均 QPS × 3(保守估計峰值倍數)
- 儲存量:每條記錄多大 × 每天幾條 × 幾年
這個估算不需要精確,目的是感受「這個系統大概處於什麼量級」,決定需不需要分散式、需不需要快取、需不需要 Sharding。
步驟三:定義 API(3 分鐘)
把核心功能轉成 API 定義:
POST /messages 建立訊息
GET /messages?room_id 讀取訊息
API 定義幫你鎖定「這個系統真正需要支援什麼操作」,避免設計過頭。
步驟四:高層架構設計(10 分鐘)
從最簡單的架構開始,逐步加入必要的元件。
先畫最小可行架構:
[Client] → [API Server] → [Database]
然後問自己:「這樣夠嗎?哪裡會先出問題?」
根據需求和規模估算,逐步加入:
- 讀多?→ 加 Cache(第 05 篇)
- 寫多?→ 讀寫分離(第 06 篇)
- 需要即時?→ WebSocket + Pub/Sub(第 17 篇)
- 有耗時任務?→ 非同步 + MQ(第 07-08 篇)
- 流量很大?→ Rate Limiting + 降級(第 13 篇)
- 資料量很大?→ Sharding(第 15 篇)
- 有搜尋需求?→ Elasticsearch(第 16 篇)
- 需要全球低延遲?→ CDN(第 26 篇)
步驟五:深挖關鍵設計(15 分鐘)
面試官通常會選一、兩個最有趣的地方深問。
常見的深入方向:
- 資料庫選型:為什麼用關聯式?為什麼不用 NoSQL?
- 快取策略:快取的 key 怎麼設計?怎麼失效?
- 一致性 vs 可用性:在 CAP 裡你傾向哪邊?為什麼?
- 瓶頸在哪:這個架構的最脆弱點是什麼?怎麼處理?
- 擴展策略:如果流量 10 倍,哪裡要改?
每個設計決策,都要說清楚「為什麼這樣選」和「代價是什麼」。
示範題一:設計 URL 短網址服務(TinyURL)
需求釐清
- 輸入長網址,回傳短網址
- 點擊短網址,跳轉到原始長網址
- 短網址要永久有效(或設定過期時間)
- 預期:每天 1 億次短網址點擊、100 萬次新建短網址
規模估算
- 讀寫比 ≈ 100:1(讀遠多於寫)
- 平均 QPS:100M / 86400 ≈ 1200 QPS
- 峰值 QPS:約 4000 QPS
- 儲存:1M 條 / 天 × 365 天 × 5 年 = 18 億條;每條 ~100 bytes → 180 GB
核心設計
短網址生成:
把長網址映射到一個 6 位的短 ID。
做法:用自增 ID 轉 Base62(26 小寫 + 26 大寫 + 10 數字 = 62)。6 位 Base62 = 62^6 ≈ 568 億種組合,夠用。
讀取(重定向):
GET /{short_id}
→ 先查 Redis 快取
→ 沒有 → 查 DB
→ 回傳 301 / 302 Redirect
301(永久重定向)vs 302(臨時重定向):
- 301:瀏覽器會快取,之後不再打到我們的伺服器——節省流量,但沒辦法追蹤點擊次數
- 302:每次都打到伺服器——可以追蹤點擊,但流量多
取捨:要分析點擊行為 → 用 302;只要重定向功能 → 用 301。
取捨說明:
| 決策 | 理由 |
|---|---|
| Redis 快取 | 讀多寫少,熱門短網址反覆被點,快取命中率高 |
| Base62 而非 MD5 | MD5 碰撞需要解決;自增 ID 簡單、唯一 |
| 302 而非 301 | 大多數商業服務需要點擊追蹤 |
示範題二:設計即時訊息系統(Slack / Line)
需求釐清
- 用戶之間可以一對一傳訊息
- 有群組聊天室
- 訊息要即時顯示
- 用戶離線時,重新上線後要能看到歷史訊息
- 預期:5000 萬 DAU,每人每天送 20 則訊息 → 10 億條訊息 / 天
規模估算
- 平均 QPS:10 億 / 86400 ≈ 11,500(寫)
- 加上讀取,峰值 QPS 約 5 萬
- 儲存:每條訊息 ~1KB,10 億 / 天 → 1 TB / 天
核心設計
訊息傳送流程:
A 送訊息
→ API 服務寫入 DB(訊息 ID 用 Snowflake,保證時序)
→ 寫入 Message Queue(通知要推送)
→ Worker 消費佇列
→ 查出 B 的 WebSocket 連線在哪台 WS Server
→ 透過 Pub/Sub 通知那台 WS Server
→ WS Server 推送給 B
離線訊息:
B 不在線時,訊息存在 DB 裡。B 上線後:
- 建立 WebSocket 連線
- 查詢「上次上線後的所有未讀訊息」
- 前端顯示未讀
訊息儲存的選型:
訊息的讀寫特性:
- 寫入量大(高頻)
- 讀取模式:主要是「某個聊天室最近的訊息,按時間排序」
- 不需要複雜的關聯查詢
適合:Cassandra 或 HBase(寫多、按時間排序查詢、水平擴展容易),而不是關聯式 DB(複雜 Join 在這裡沒用到,而且寫入能力有限)。
取捨說明:
| 決策 | 理由 |
|---|---|
| Cassandra 而非 PostgreSQL | 寫入量大,Cassandra 寫入效能好;查詢模式簡單(按 room_id + time 查) |
| Snowflake ID | 分散式環境下唯一且有序,訊息排序靠 ID 就夠 |
| WebSocket + Pub/Sub | 即時通知(第 17 篇),多台 WS Server 用 Pub/Sub 路由 |
面試時常犯的幾個錯誤
| 錯誤 | 更好的做法 |
|---|---|
| 拿到題目不問就開始畫 | 先花 5 分鐘問清楚需求和規模 |
| 一開始就設計超複雜架構 | 從最小可行架構開始,按需求逐步加複雜度 |
| 只說「要用 Kafka」,不說為什麼 | 說清楚用 Kafka 解決什麼問題,代價是什麼 |
| 面試官問問題時死守原設計 | 系統設計沒有唯一答案,面試官想看你怎麼回應新資訊 |
| 沉默自己想,不說出來 | 把思考過程說出來,讓面試官跟著你走 |
這個系列學到的核心思維
走完這三十一篇,小明的思維方式已經不一樣了。他學會問:
當系統遇到問題時:
- 瓶頸在哪裡?CPU?記憶體?網路?DB?
- 是讀的問題還是寫的問題?
- 需要強一致還是最終一致就夠?
當要引入新工具時:
- 這個工具解決了什麼具體的問題?
- 現有工具能不能解決?
- 引入的代價(學習、維運、複雜度)值得嗎?
當面對高流量時:
- 可以快取嗎?(把重複計算省掉)
- 可以非同步嗎?(把耗時任務移出主路徑)
- 可以降級嗎?(非核心功能犧牲,保住核心)
- 可以限流嗎?(不讓超出承受能力的量進來)
這套思維,不是用來背答案的,而是用來面對你沒見過的問題時,知道從哪裡開始想。
系列終章
從第 01 篇的一前一後一 DB,到第 25 篇的完整分散式架構,小明的系統走過了:
負載均衡 → 快取 → 讀寫分離 → 非同步佇列 → 微服務 → 限流降級 → DB 分片 → 多樣化儲存 → 即時通知 → 分散式協調 → 監控追蹤 → 容器化 → Kubernetes → AI 助理
每一步都是因為有真實的問題,才引入真實的解法。
架構不是設計出來的,是長出來的。
當你下次遇到一個系統設計問題,不管是工作中的真實需求,還是面試中的假設情境,希望你能想起小明從第一篇走到這裡的每一步:
從最簡單的起點出發,遇到問題才加複雜度,永遠知道每個決策的代價。
這就是系統設計。
【從一台 Server 到分散式架構】系列完。