面試陷阱題 (Exam Traps)
目的
收集系統設計面試中常見的陷阱、易錯點和容易混淆的概念。
Client-Server Architecture
Trap:聽到「低延遲通訊」就跳進 P2P
- WebRTC / P2P 極難做對,有複雜的 NAT traversal 問題
- 大多數「即時」需求(聊天、協作編輯、通知)用 Client-Server 就足夠
- 正確做法:只有明確涉及音視訊通話才考慮 WebRTC
- 主從架構
Trap:忽略 Client 類型對 API 設計的影響
- Thin Client(SSR)需要一次取大塊資料;Thick Client(SPA)需要細粒度 REST API
- 正確做法:確認 Client 類型,再決定 API 設計方向
- 主從架構
傳輸層協定
Trap:預設選 UDP 追求效能
- 低延遲不等於需要 UDP,大多數場景 TCP 才是正確選擇
- UDP 不保證送達和順序,應用層需要自己處理
- 正確做法:預設 TCP,只有同時滿足「低延遲至關重要 + 可容忍丟包 + 不需瀏覽器支援」才考慮 UDP
- 傳輸層協定 TCP vs UDP
Trap:忽略瀏覽器對 UDP 的限制
- 瀏覽器只能透過 WebRTC 使用 UDP,無法直接用原生 UDP
- 設計需要 UDP 但支援瀏覽器的方案時,必須提供替代路徑
- 傳輸層協定 TCP vs UDP
HTTP 與 API 設計
Trap:以為 HTTPS 代表請求內容可信
- HTTPS 加密傳輸通道,不保證 request body 是合法客戶端發出的
- 錯誤:從 body 讀 user_id 做查詢
- 正確:從驗證過的 session/token 取得用戶身份
- HTTP 與 HTTPS
Trap:用操作命名 REST API
POST /updateUser、GET /createOrder不是 RESTful- 正確:
PUT /users/{id}、POST /orders(資源 + HTTP 動詞) - API 範式比較
Trap:面試中過早跳到 gRPC
- 面試官想看你思考問題,不是優化協定
- 正確做法:先 REST,確認有效能瓶頸且是內部服務時才提 gRPC
- API 範式比較、RPC 與 gRPC
API 設計決策框架
Trap:把決策樹兩個提問的順序搞反
- 正確順序:先問 External/Internal,再問 Over/Under-fetching
- Internal 直接走 RPC,根本不需要問 fetching 問題
- 順序錯了會把內部服務推向 GraphQL,偏離業界慣例
- API 設計決策框架
Trap:對外 API 一開始就選 gRPC
- gRPC 對外缺點多:瀏覽器需 gRPC-Web 代理、debug 困難、不易讓第三方測試
- 正確做法:對外 API 預設 REST,只有內部服務間才用 gRPC
- API 設計決策框架、RPC 與 gRPC
REST
Trap:把冪等談成「回應內容相同」
- 冪等講的是 伺服器狀態,不是回應內容
- 例:DELETE 第一次回 204、第二次回 404,仍是冪等(伺服器狀態「已不存在」不變)
- REST 架構風格
Trap:以為 PATCH 一定冪等
- PATCH 依設計而定:
PATCH {age: 30}冪等;PATCH {age: age+1}非冪等 - 實務 一般不保證 → 需要冪等時用 Idempotency-Key header
- REST 架構風格
Trap:重試非冪等 API 沒用 Idempotency-Key
Trap:在 GET 帶 body
- CDN / Proxy / 瀏覽器多以 URL 為 cache key,不看 body
- 許多 HTTP client library 會丟棄 GET body
- 正確做法:用 Query Parameter;需要複雜 body 就改
POST /search(犧牲快取) - REST 架構風格
Trap:混淆 401 與 403
GraphQL
Trap:忽略 N+1 問題
- Field-based resolver 對巢狀查詢會跑 1 + N 次 DB query
- 正確做法:用 DataLoader batch 合成
IN (...)或 resolver 層 JOIN - GraphQL 查詢語言
Trap:以為 GraphQL 的 HTTP cache 和 REST 一樣
- GraphQL 所有查詢都 POST 到
/graphql→ 無法靠 URL 當 CDN cache key - 正確做法:
- Persisted queries:用 query hash 當 key,可走 GET + CDN
- 客戶端 normalized cache(Apollo / Relay)
- GraphQL 查詢語言
Trap:開放 client 任意深度巢狀查詢
- 惡意使用者可送
users → orders → users → orders ...拖垮伺服器 - 正確做法:Query depth / complexity limit + rate limiting + persisted queries(白名單)
- GraphQL 查詢語言、API 安全
RPC / gRPC
Trap:把 gRPC 往對外 API 推
- 瀏覽器原生不支援 gRPC(需要 gRPC-Web 代理)
- 第三方開發者 debug / 測試成本高
- 正確做法:對外 REST / GraphQL,對內才 gRPC
- RPC 與 gRPC
Trap:直接改 Protobuf 欄位的 tag number 或型別
- 會破壞向後相容,舊 client 解不開
- 正確做法:刪欄位用
reserved、新欄位用新 tag、型別變更另開新欄位 - RPC 與 gRPC
API Security
Trap:把 OAuth 2.0 當作 Authentication
- OAuth 2.0 做的是 delegated authorization(「代 X 存取某資源」)
- 要認證「就是 X 本人」應該用 OpenID Connect(OIDC)
- API 安全
Trap:把敏感資料放進 JWT payload
- JWT payload 只是 base64 編碼,不是加密 → 任何人都能解開
- 正確做法:payload 只放 user_id、roles、expire;敏感資料查 DB
- API 安全
Trap:JWT 沒處理撤銷
- JWT 是 stateless → 伺服器無法主動使它失效
- 正確做法:Access token 短期(幾分鐘)+ Refresh token + 必要時黑名單機制
- 警惕
alg: none漏洞 → 伺服器固定允許的 algorithm - API 安全
Trap:內部微服務用 API Key 代替 mTLS
- API Key 單向驗證、粒度粗、洩漏風險大
- 正確做法:內部服務用 mTLS(雙向憑證 + 自動輪替,通常靠 Service Mesh)
- API 安全
Trap:RBAC 適用所有情境
- 權限細粒度需求多時,RBAC 會 角色爆炸(Admin-v1、Admin-HR、Admin-HR-readonly...)
- 正確做法:主架構用 RBAC,細節用 ABAC(屬性)補;第三方授權用 Scope
- API 安全
即時通訊
Trap:沒說明原因就直接用 WebSocket
- WebSocket 需要持久有狀態連線,基礎設施成本高
- 客戶端到伺服器每個元件(防火牆、代理、LB)都需要支援
- 正確做法:先考慮 SSE(單向推送夠嗎?),雙向高頻才用 WebSocket
- 即時通訊協定
Trap:用 WebRTC 設計非音視訊系統
- WebRTC 極難做對,連最好的實作都有連線問題
- 正確做法:WebRTC 只用於音視訊通話/會議場景
- 即時通訊協定
負載平衡
Trap:WebSocket 使用 L7 負載平衡器
Trap:持久連線服務用 Round Robin
- SSE/WebSocket 用 Round Robin,新伺服器不會分到已建立的連線,舊伺服器逐漸積累所有連線
- 正確做法:持久連線服務用 Least Connections
- 負載平衡
故障處理
Trap:重試時沒有退避和抖動
- 立即重試 + 大量客戶端同步 = thundering herd,讓已掙扎的服務更不堪負荷
- 正確做法:指數退避 + 隨機抖動
- 魔法短語:「retry with exponential backoff and jitter」
- 故障處理模式
Trap:重試非冪等 API(如支付)
- 重試 POST /payments 可能導致重複扣款
- 正確做法:使用冪等鍵(idempotency key)確保同一請求只處理一次
- 故障處理模式
Trap:忽略級聯故障
- 下游服務崩潰時,持續重試只會讓問題更嚴重(thundering herd)
- 正確做法:使用熔斷器(Circuit Breaker)自動保護下游
- 故障處理模式
CAP Theorem
Trap:以為可以同時滿足 C、A、P 三者
- CAP 定理明確指出三者不可兼得,網路分區時只能二選一
- 正確做法:確認業務能否接受暫時不一致 → AP;不能 → CP
- CAP 定理
Trap:說「我們選 CA 系統」
- CA 系統假設網路永遠不斷線,在真實分散式環境中不可能存在
- 正確做法:P 是必選,只在 C 和 A 之間取捨;CA 說法會讓面試官扣分
- CAP 定理
Trap:預設選 CP(強一致性)
- 大多數系統能接受最終一致性,過度強調 CP 會犧牲可用性
- 正確做法:預設 AP;只有在「暫時不一致會造成嚴重後果」(銀行、庫存、訂票)時才選 CP
- CAP 定理
Scalability
Trap:一開始就設計水平擴展
- 水平擴展設計複雜(LB、Session 狀態、資料一致性),過早引入增加不必要的複雜度
- 正確做法:先垂直擴展(快速簡單),流量持續增加或需要高可用性時再水平擴展
- 可擴展性
Trap:水平擴展後忘記 Session 狀態問題
- 多台 Server 時,請求可能打到不同機器,Stateful Server 會導致 Session 遺失
- 正確做法:Server 設計為無狀態(Stateless),Session 存入集中式 Store(Redis)
- 可擴展性
Trap:只擴展 Server,忘記 DB 也是瓶頸
- Server 水平擴展後,DB 往往成為新瓶頸
- 正確做法:同時考慮 Read Replica、Caching(Redis)、DB Sharding
- 可擴展性
Consistent Hashing
Trap:用
hash(key) % N 做分散式分片
- N 變動(擴縮容)→ 幾乎所有 key 重新分配 → 快取全 miss → DB 瞬間被打爆(cache stampede)
- 9 個 key、N 從 3 → 4 時有 7 個要搬
- 正確做法:用 Consistent Hashing,搬遷量降到 O(K/N)
- 一致性雜湊
Trap:省略 Virtual Nodes
- 節點少時資料分佈不均,熱點嚴重
- 節點下線時所有流量集中壓到下一節點 → 負載翻倍
- 強機弱機都只拿一個區間 → 無法按硬體能力分配
- 正確做法:每實體節點配 100~200 個 vNode
- 一致性雜湊
索引基礎
Trap:所有欄位都加索引追求速度
- 寫入
INSERT/UPDATE/DELETE要維護所有索引 → 寫入效能崩跌 - 索引佔儲存空間(每個索引獨立結構)
- 查詢優化器要在眾多索引間挑選,分析成本增加
- 正確做法:只對高頻查詢欄位 + 選擇性高的欄位建索引
- 資料庫索引概論
B-Tree Index
Trap:以為 InnoDB 用 B-Tree
- InnoDB 實際用的是 B+Tree:只有葉節點存資料,葉節點間有 linked list
- 這個差異對範圍查詢效能影響很大(沿葉節點線性走 vs 反覆回上層)
- 正確做法:說 B+Tree,並解釋為何範圍查詢更快
- B-Tree 索引
Trap:用 B-Tree 索引加速
LIKE '%abc'LSM Tree
Trap:忽略 Compaction 對前台的影響
- Compaction 需大量讀寫 SSTable → 佔用磁碟 I/O 與 CPU
- 和前台查詢共用資源時會拖慢線上請求
- 正確做法:低峰時段排程 compaction、或用 rate limit 控制
- LSM Tree
Trap:對 read-heavy 場景直接選 LSM
Trap:大量 DELETE 後查詢變慢
- LSM 的 DELETE 不立即清理,而是寫 tombstone
- 大量 tombstone 累積 → 讀取要掃過所有 tombstone → 讀放大
- 正確做法:定期 compaction 清理 tombstone;或避免把 LSM 當 delete-heavy 表
- LSM Tree
Hash Index
Trap:用 Hash Index 做範圍查詢
WHERE age > 25無法利用 hash(hash 打亂 key 順序)→ 退化到全掃 bucket- 正確做法:範圍查詢一律用 B-Tree;Hash Index 只用在純等值查詢
- 雜湊索引
Trap:忽略 Hash DoS 攻擊
- 攻擊者刻意產生大量同 hash 的 key → 全擠在同一 bucket → 查詢退化 O(n)
- 正確做法:用 salted hash / 密碼學安全 hash;對外服務不直接暴露 hash 結構
- 雜湊索引
Geospatial Index
Trap:用兩個獨立的 1D B-Tree 做地理鄰近搜尋
- 只用緯度索引 → 橫跨全球的長條帶,經度索引派不上用場
- 兩個索引交集 → 形成矩形區域(比圓形大得多)→ 還要過濾距離
- 正確做法:用 Geohash / R-Tree / S2 / H3 等專門空間索引
- 地理空間索引
Trap:忽略 Geohash 邊界效應
- 相鄰但跨越 Geohash 格子的點可能有完全不同前綴(例:馬路兩側)
- 正確做法:查詢時同時檢查中心 cell + 8 個鄰居 cell
- 地理空間索引
Trap:Quadtree 用在生產環境
- Quadtree 需專門樹結構,不能用既有 B-Tree
- 固定切 4 等份不適應資料密度,密集區樹很深
- 正確做法:現代生產環境用 R-Tree(PostGIS、SQLite);Quadtree 留給遊戲碰撞檢測
- 地理空間索引
Inverted Index
Trap:用 Inverted Index 做結構化欄位查詢
WHERE age = 30這種精確欄位比對,B-Tree 更直接- Inverted Index 擅長「文字包含某 term」,不是精確欄位匹配
- 正確做法:結構化用 B-Tree,全文檢索用 Inverted Index
- 倒排索引
Trap:忽略 Phrase Query 需要 positions
- 只存 doc ID 的 Inverted Index 無法做
"apple pie"短語查詢 - 必須存 positions 才能判斷 term 是否相鄰
- 正確做法:生產系統(Elasticsearch/Lucene)預設就存 positions,但會增加索引體積
- 倒排索引
Database Transactions
Trap:把 ACID 的 C 和 CAP 的 C 混為一談
- ACID-C:商業邏輯正確性(餘額不能為負、constraint 必須成立)
- CAP-C:分散式副本之間的同步(所有節點同一時間看到相同資料)
- 完全不同概念,面試最常被混淆的點
- 資料庫交易
Trap:用 Serializable 防超賣
- 庫存超賣本質是 Lost Update(不是 Phantom Read)
- 正確做法:原子
UPDATE qty = qty - 1 WHERE qty > 0一條 SQL 即可 - 提升到 Serializable 大幅犧牲並行性,是過度設計
- 如果扣減前需要複雜判斷,再用
SELECT FOR UPDATE鎖該行就好 - 資料庫交易
Trap:微服務跨服務 transaction 用 2PC
- 2PC 在 coordinator 崩潰時節點會卡在「prepared 未 commit」,生產環境通常避免
- 正確做法:用 Saga —— 本地交易 + 補償操作 + 接受最終一致
- 現代微服務的主流選擇
- 資料庫交易
Trap:Pessimistic Locking 不考慮 Deadlock
SELECT FOR UPDATE在不同 transaction 以不同順序鎖資源時會 deadlock- 正確做法:所有 transaction 用一致鎖序(永遠先鎖 ID 小的)
- DB 的 deadlock detection 會自動 rollback 一方,application 層要處理重試
- 資料庫交易
Sharding
Trap:用低基數欄位當 Shard Key
is_premium(布林)最多只能分 2 個 shardcountry在 90% 用戶集中於美國時嚴重不均- 正確做法:選高基數 + 均勻分佈 + 契合查詢的欄位(通常
user_id、order_id) - 資料分片
Trap:用
created_at 分片
- 所有新寫入都打到最新 shard → 寫入熱點
- 舊 shard 幾乎只在讀歷史時才有流量
- 正確做法:用
user_id/order_id等隨時間均勻分佈的欄位 - 資料分片
Trap:過早 Sharding
Trap:頻繁跨 Shard 查詢沒當回事
- 跨 shard 查詢頻繁是「shard key 選錯」的訊號
- 正確做法:重新評估 shard key / 反正規化 / 快取 / 背景預計算
- 面試官期待你「最小化跨 shard 查詢」,不是「理所當然全 shard 聚合」
- 資料分片
Replication
Trap:用牆上時鐘解 Read-After-Write
- 時鐘有 clock skew,不同節點時間不一致
- 正確做法:用 LSN(PostgreSQL)/ binlog position(MySQL) 等 replication log 邏輯位置
- Client 帶上 LSN,replica 必須追上才能服務
- 資料複寫
Trap:Failover 沒處理 Split Brain
- 偵測 leader 失效不可靠(網路 partition / 暫時延遲)→ 兩節點都以為自己是 leader
- 兩邊都接受寫入 → 資料損毀
- 正確做法:Fencing Token(新 leader 帶遞增 token,舊 leader 寫入被儲存層拒絕)/ STONITH / Raft 共識
- 資料複寫
Trap:Sloppy Quorum 還以為有強一致
- Sloppy quorum 寫到「非家節點」,w/r 可能完全沒重疊
- 即使
w + r > n,也無法保證讀到最新值 - 正確認知:Sloppy quorum 是 AP 配置,犧牲一致性換可用性
- 嚴格一致需求要嚴格 quorum(連不到足夠節點就返回錯誤)→ CP
- 資料複寫
Trap:以為所有 follower 同步就最安全
- 全同步 replication 下,任何一台 follower 失效就讓整個系統寫入卡住
- 正確做法:半同步(Semi-synchronous) —— 一台 follower 同步、其他非同步
- 至少保證 2 份節點有最新資料,又不會被單一 follower 卡死
- 資料複寫
Caching
Trap:Cache Stampede 用 Cache Warming 解所有情況
- Cache warming(過期前主動刷新)只在 TTL 過期模式有效
- 如果是「寫入時 invalidate」模式,無法預測何時失效,warming 沒用
- 真正有效解:Request Coalescing(Single Flight) —— 只讓一個請求重建快取
- 快取機制
Trap:複製 Hot Key 但 TTL 相同
- 多個副本同時過期 → 所有副本同時 miss → 更大規模的 Stampede
- 正確做法:各副本 TTL 錯開(加隨機 jitter)
- 或用機率性提前過期(probabilistic early expiration)
- 快取機制
Trap:Cache 故障時 DB 被打垮
- Redis 掛了 → 所有讀取突然打 DB(流量 5×)→ cascading failure
- 正確做法:Circuit Breaker 限制 DB 流量 + In-Process Cache 當最後防線
- 心法:「Cache 是性能優化,不是業務必需。故障時系統應該變慢但不該崩潰」
- 快取機制
Trap:把 FIFO 當預設淘汰策略
- FIFO 完全忽略使用模式,可能踢掉正在被頻繁存取的項目
- 正確做法:LRU 是預設(90% 工作負載適合)
- 長期熱門資料用 LFU,新鮮度敏感配 TTL
- 快取機制
Numbers to Know
Trap:以為記憶體大小是現代瓶頸
- 跟幾年前完全相反 —— 單機 RAM 最高 2TB,通常先碰到 CPU
- 真正瓶頸通常是 ops/sec(每秒操作) 或 網路頻寬
- 正確認知:先算 ops/sec 和頻寬,再談記憶體
- 系統設計關鍵數字
Trap:800GB 資料就喊 Sharding
- 一個調校良好的單一 PostgreSQL 可以撐數百萬到上千萬用戶
- 觸發 sharding 的真實量級:50TiB / 10k TPS 寫 / uncached < 5ms / 跨區域
- 正確順序:index → cache → read replica → 升級硬體 → 最後才 sharding
- 系統設計關鍵數字
Trap:在面試中精準背數字
- 你不需要記得每個數字的精確值
- 要有「量級直覺」:Latency 順序 memory ≪ disk ≪ network
- 面試話術用「大概量級 + 推理」:「假設一台機器有幾十 GB memory…」
- 重點是避免 over-engineering,不是炫耀數字
- 系統設計關鍵數字