大流量系統最常見的失敗,不是撐不住尖峰,而是設計者以為自己在做「擴充性」,其實是在做「更貴的單體」。加機器、加快取、加佇列,如果沒有事先想清楚故障邊界,只是把同一個問題放大到更難除錯的規模。
真正的高併發設計,第一步不是選技術,而是回答一個問題:當某個環節壞掉,我要它壞成什麼樣子? 是回舊資料、拒絕請求、還是丟訊息重試?這個答案會反過來決定你需要哪些元件。
快取、分片、佇列各自解什麼問題
這三個詞常被一起提起,但它們處理的其實是不同瓶頸。
快取解的是「重複讀」的成本。它的代價是資料新鮮度與一致性。做快取前先問:這份資料容忍多久的舊?如果答案是「一秒都不行」,那你要的可能不是快取,而是讀寫分離或更好的索引。快取穿透、雪崩、擊穿這三個經典問題,本質上都是「當快取失效那一瞬間,你的資料庫還撐得住嗎」——如果撐不住,加快取只是把炸彈延後引爆。
資料庫分片解的是「單一節點寫入上限」。但分片一旦上線,跨片查詢、跨片交易、rebalance 都會變成長期負擔。經驗法則是:能靠讀寫分離、垂直拆表、archive 舊資料撐過去,就不要急著水平分片。分片是單向門,回頭成本很高。
訊息佇列解的是「同步變非同步」與「削峰」。它讓下游服務可以用自己的節奏消化流量,代價是最終一致性與重複處理。用佇列前一定要想:這個訊息如果重送兩次會發生什麼事?如果你的下游不是冪等的,佇列會變成問題製造機。
假設一個典型情境:一家中型電商在做促銷倒數,下訂請求瞬間湧入。這時你會希望下訂寫入是同步且強一致的(不能超賣),但發信、發推播、更新推薦模型這些可以進佇列。把「必須立刻正確」和「可以稍後完成」分開,是削峰的核心,不是全部丟進 Kafka 就叫非同步架構。
無狀態服務與可觀測性是地基
CDN 和無狀態服務的價值,在於讓水平擴充變便宜。應用層一旦帶了 session、暫存檔、記憶體狀態,擴充就會變成災難。把狀態全部趕到資料庫、快取或物件儲存,應用層才能隨時被殺掉重開——這也是 K8s 生態能運作的前提。
可觀測性不是加了 Grafana 就有。真正能救命的是三件事:結構化日誌、跨服務的 trace ID、業務層級的指標。單看 CPU 和 RPS 沒用,你要能回答「這筆訂單失敗的時候,它經過了哪幾個服務、卡在哪一段」。沒有這個能力,大流量時的除錯基本上等於瞎猜。
下面是一段示意用的 middleware 骨架,說明 trace ID 該怎麼往下游帶:
// 示意:實際串接請參考 OpenTelemetry 官方文件
func TraceMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
traceID := r.Header.Get("X-Trace-Id")
if traceID == "" {
traceID = generateID()
}
ctx := context.WithValue(r.Context(), "trace_id", traceID)
w.Header().Set("X-Trace-Id", traceID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
重點不是這段程式碼,而是每一個進出你系統的請求都要能被追蹤。這件事在流量小的時候做很無聊,在流量大的時候做已經來不及。
我們的觀察
協和數位在幫客戶評估架構升級時,通常會建議先做兩件事:把應用層徹底無狀態化、把可觀測性補齊。這兩件事做完之後,很多原本以為需要分片、需要 Kafka 的問題,會突然變得有辦法量測、也有辦法漸進處理。大流量架構的重點從來不是堆疊時髦元件,而是讓系統在壞掉的時候,你還看得懂它在做什麼。
