擴容之前,先確認瓶頸不是你自己造成的
大部分號稱「撐不住流量」的系統,問題不在機器太少,而在每一層的職責沒切乾淨。加機器只是把問題往後延,遲早會在資料庫或某個共享狀態上爆開。
高併發架構的核心其實只有一句話:讓越多請求越早離開昂貴的資源。CDN 比應用伺服器便宜,記憶體快取比資料庫便宜,讀比寫便宜。所有設計選擇都圍繞這個排序展開。
以下幾個是常見、但容易被低估的取捨:
1. CDN 不只是放圖片
很多 API 回應其實是可以邊緣快取的,例如商品列表、分類頁、公開的內容資料。只要願意接受幾秒到幾分鐘的延遲一致性,就能把大量讀請求擋在源站之外。重點是設計合適的 Cache-Control 與快取鍵,而不是無腦放行。
2. 快取策略要先想失效,再想命中
寫 cache 很容易,難的是 cache 失效。Cache-aside 是預設選項,但要小心三個經典問題:快取穿透(查不到的 key 一直打 DB)、快取擊穿(熱點 key 過期瞬間流量灌入)、快取雪崩(大量 key 同時失效)。對應的工具是空值快取、互斥鎖或 single-flight、加上隨機過期時間。
# 示意:single-flight 概念,實際請用對應語言的成熟函式庫
def get_product(pid):
val = cache.get(pid)
if val is not None:
return val
with lock(f"lock:{pid}"): # 同一 key 同時只有一個請求查 DB
val = cache.get(pid)
if val is not None:
return val
val = db.query(pid)
cache.set(pid, val, ttl=random_ttl())
return val
3. 資料庫分片是最後手段
Sharding 一旦做下去,跨片 join、跨片交易、後續搬資料都是長期負擔。在動手分片之前,先確認讀寫分離、索引、查詢計畫、connection pool 都已經調過。很多時候只是一條沒走到索引的 SQL 在拖垮整個系統。
真的要分,先想清楚 shard key:它必須能均勻分散熱點,又必須在大部分查詢路徑上自然出現。選錯 shard key 比不分片還痛苦。
訊息佇列、無狀態、可觀測性
4. 訊息佇列是流量的避震器
面對突發流量,同步處理是脆弱的。把可以非同步的工作(寄信、通知、報表、後處理)丟進 Kafka、RabbitMQ 或雲端的佇列服務,前端就能用穩定的回應時間吸收尖峰。代價是要面對 at-least-once 投遞、訊息順序、消費者冪等這些議題——這些不是 bug,是這個模型的本質。
5. 服務無狀態,session 放外面
要能水平擴展,應用伺服器就不能在記憶體裡留任何「只有這台機器知道的事」。Session、暫存檔、上傳中的檔案,全部放到外部儲存(Redis、物件儲存、共享檔案系統)。這條原則違反一次,後面每次擴容都會痛。
6. 沒有可觀測性,等於閉著眼睛開車
大流量系統一定會出狀況,問題是你多快知道、多快定位。Metrics 看趨勢、Logs 看細節、Traces 看請求在分散式系統裡的路徑——三者缺一不可。OpenTelemetry 已經是事實上的標準,越早接入越省事。
舉個例子,假設一個電商首頁突然變慢,沒有 trace 的團隊只能猜:是 DB?是某個外部 API?是 GC?有 trace 的團隊能在幾分鐘內指到那個拖慢一切的下游呼叫。
我們的觀察
我們在替客戶評估架構時,通常會先問兩個問題:流量的形狀長什麼樣(穩定、尖峰、活動型)、以及哪些資料可以接受最終一致。這兩個答案會直接決定要不要 CDN、要不要佇列、要不要分片。技術選型沒有標準答案,但有標準的判斷順序——先看清楚瓶頸,再決定花錢的方向,永遠比一開始就堆架構划算。
