【從一台 Server 到分散式架構】第 14 篇:DB 又爆了——讀寫分離進階與多從庫
限時特賣結束了。限流守住了後端,排隊拉平了洪峰,系統撐過去了。
小明以為可以喘口氣,直到下週的例會上,DBA 小陳打開了監控儀表板,指著一條持續往上爬的折線圖說:「你看這條線——這是 Read Replica 的查詢延遲。特賣結束之後,流量沒有回到以前的水位,每天的日活用戶比特賣前多了三成。一個 Replica 已經扛不住了。」
小明想起來了。第 6 篇做讀寫分離的時候,他們加了一個 Read Replica,讓查詢打從庫、寫入才打主庫。那是當時的正確決策——但那是很久以前了。現在課程平台每天有幾十萬次的課程列表查詢、學習進度更新、評論載入……一個 Replica 早就不夠用了。
解法很直觀:再加幾個 Replica。
但做起來有幾個細節,得想清楚。
🏗️ 本篇開始前的架構 — 一主一從,讀取壓力全集中在唯一的 Read Replica
第 6 篇的讀寫分離只加了一個從庫,所有讀取請求全打到這一台,已成瓶頸。
解法很直觀:多加幾個 Replica
多個 Read Replica 的概念很好理解——把原本全打到一台從庫的讀取流量,分散到多台從庫上。原本一台要扛全部,現在三台各扛三分之一。
但加了多個 Replica 之後,馬上有一個新問題:後端要怎麼知道,這次的讀取請求要打給哪一台 Replica?
在後端程式碼裡輪詢(Round-Robin)
最簡單的方式:在應用程式裡維護一個 Replica 清單,每次讀取請求輪流打到不同的 Replica。
第 1 次讀取 → Replica 1
第 2 次讀取 → Replica 2
第 3 次讀取 → Replica 3
第 4 次讀取 → Replica 1(回頭)
……
優點是簡單,不需要額外的基礎建設。缺點是這段邏輯散在每個服務裡,如果有十個後端服務、每個都要自己管這份清單,維護起來很麻煩。
用 DB Proxy 做路由(推薦)
更常見的做法是在後端服務和資料庫之間,加一層資料庫代理(DB Proxy),例如 ProxySQL(MySQL)或 PgBouncer(PostgreSQL)。
後端服務只需要連接到 Proxy,Proxy 負責辨識「這是讀取還是寫入」——寫入轉發到 Primary,讀取輪流分到各個 Replica。後端服務完全不用知道背後有幾台 Replica。
繞不過去的坑:複製延遲(Replication Lag)
多個 Replica 的方案看起來很美好,但有一個在第 6 篇就提過、在這裡必須正視的問題:複製延遲。
主從複製在大多數情況下是非同步的。也就是說,主庫完成一筆寫入後,這筆資料不是立刻出現在所有從庫上,而是需要一點時間(通常是幾毫秒到幾秒,壓力大時可能更長)才能同步過去。
在只有一個 Replica 的時候,這個問題比較容易被忽略。但 Replica 多了之後,每台 Replica 的複製延遲可能還不一樣——有的同步比較快,有的因為機器負載高而稍微落後。
複製延遲最明顯的場景:讀自己寫的東西
想像這個操作流程:
- 小美買了一門課,系統寫入訂單到 Primary DB。
- 購買成功,頁面跳轉到「我的課程」。
- 後端讀取小美的課程清單,請求打到 Replica 2。
- 但 Replica 2 的複製還沒追上,小美剛買的那門課還沒出現。
- 小美一臉疑惑:「我剛剛明明買了,為什麼這裡沒有?」
這個問題有個名字:Read-Your-Writes 一致性問題——用戶寫入後,立刻讀取,卻看不到自己剛寫的東西。
幾種應對策略
策略一:寫完之後短時間內,把讀取也打回 Primary
如果後端偵測到「這個請求是剛完成寫入後的讀取」(例如購買成功後的第一次課程列表查詢),就強制打到 Primary 而不是 Replica。
這樣讀取的一致性可以保證,但 Primary 會多承擔一部分讀取請求。通常只針對「寫後立刻讀」的場景做,不影響大部分的普通查詢。
策略二:只讓特定 API 打 Primary
把需要強一致的 API(例如「取得我的訂單」、「取得我剛付款的收據」)固定打 Primary;把可以接受輕微延遲的 API(例如「課程列表」、「熱門排行」)打 Replica。這個路由規則可以在 DB Proxy 層或應用層設定。
策略三:接受「最終一致」
對於那些「稍微晚一點看到也無所謂」的資料,直接接受 Replica 可能有幾秒延遲的現實。例如:課程的觀看人數、評論數量,晚個三秒顯示,用戶幾乎感受不到。這是 AP 的取捨——可用性優先,允許暫時的不一致(第 10 篇 CAP 理論的實際應用)。
生活化的比喻:中央廚房與各分店的備忘板
把 Primary 想像成中央廚房,每天負責製作所有的菜單更新和食材採購紀錄(寫入)。各個分店有自己的備忘板(Read Replica),定時從中央廚房抄一份最新的菜單過來。
客人到分店問「今天有什麼菜?」服務生查備忘板就能回答,不用打電話去中央廚房問(讀取打 Replica,不打 Primary)。中央廚房的電話線就被解放出來,只用來接「我要修改菜單」的請求(寫入打 Primary)。
但有一個情況:客人下午剛剛訂了一道新菜(寫入了一筆自訂餐點),五分鐘後再打來問「我剛剛訂的菜有沒有確認?」——這時候,服務生看的備忘板還沒有更新,只能說「查無此菜」。
這就是複製延遲的問題。聰明的服務生會說:「這個問題我直接幫您轉給中央廚房確認。」——也就是把這個查詢打回 Primary。
加 Replica 可以無限擴充讀取嗎?
理論上,讀取壓力越大,就繼續加 Replica,可以一直加嗎?
短期內是可行的。但有兩個隱性代價要注意:
1. 複製本身也是主庫的負擔
每多一個 Replica,主庫就要多一條複製連線、多傳送一份 binlog 資料給那台 Replica。Replica 加到一定數量後,主庫的複製網路開銷也會影響到正常的寫入效能。
2. 加 Replica 解決不了寫入壓力
讀取壓力可以靠多個 Replica 分散,但所有的寫入永遠只打主庫。隨著平台繼續成長,寫入量(新用戶、新訂單、學習進度更新)也在持續增加。加再多的 Replica,主庫的寫入壓力一點都不會減少。
3. 磁碟容量是單機上限
課程平台的資料量每個月都在成長:訂單、用戶行為紀錄、學習心跳數據……這些資料全部存在主庫那一台機器上。一台機器的磁碟終究有個物理上限。
這兩個問題——寫入擴展和資料容量——是讀寫分離架構根本解決不了的。它們需要下一個層級的解法。
🏗️ 本篇結束後的架構 — 一主三從,讀取流量分散了,DB Proxy 負責路由
讀取壓力暫時解決。但主庫的磁碟使用量和寫入量仍然只增不減。
小結與預告
這篇我們做了幾件事:
- 擴充成多個 Read Replica:讀取流量不再集中於一台從庫,分散到多台平行承擔。
- 用 DB Proxy 統一路由:後端服務不用自己管 Replica 清單,Proxy 自動辨識讀寫並轉發。
- 正視複製延遲:非同步複製帶來的一致性問題需要有意識地處理——強一致的請求打 Primary,可接受延遲的請求打 Replica。
- 認清讀寫分離的邊界:加多少個 Replica 都解決不了寫入擴展和資料容量的問題,這是它的根本限制。
三個月後,DBA 小陳帶著兩個更嚴峻的數字回來了:「主庫磁碟用了 87%,而且每秒寫入量已經到了 1,500 筆,照這個速度,頂多再撐半年。」
讀取問題解了,但資料量和寫入量的上限,是一台機器說了算的——除非我們把資料本身切開,分散到多台機器上存。
下一篇,我們來談當一顆 DB 真的裝不下的時候,該怎麼做:Database Sharding(資料庫分片)。