iT邦幫忙

2026 iThome 鐵人賽

DAY 7
0
Software Development

系統設計就像九頭蛇:打造社群網站的 30 天系列 第 7

Day 7 一顆 Database 不夠用:Database Replication

  • 分享至 

  • xImage
  •  

前幾天把 Application Server 從一台變成多台,也把 Session 從 Server Memory 搬到共用的 Session Store:

瀏覽器 -> Load Balancer -> Server(多台,Stateless)-> Database

你可注意到了:Server 變多了,Database 卻還是只有一台。

不管前面有幾台 Server 在分攤流量,最後所有的讀寫都會匯聚到同一個 Database。

Application Layer 雖然順利做到了 Horizontal Scaling,Database 反而變成了新的瓶頸。

這也是大型系統很常出現的現象:解決了一個瓶頸,下一層往往會冒出新的瓶頸。

檢視現在的流量組成

對一個社群平台來說,大部分操作其實都是 Read:

  • 看 Feed
  • 看 Profile
  • 看某一篇 Post

Write 則相對少很多:

  • 發文
  • Follow
  • Like

多數使用者「看」的頻率遠高於「發」,所以 Read Traffic 通常遠高於 Write Traffic

這帶出一個很自然的想法:

如果多準備幾個 Database,專門用來處理 Read,會不會就能分攤壓力?

這就是 Database Replication 要解決的問題。

Database Replication

Replication 的概念很直白:建立 Database 的複本(Replica)。

原本一個 Database 要同時處理讀跟寫,現在改成:

  • Primary(也稱 Master):只負責寫入,包括 Insert / Update / Delete
  • Replica(也稱 Slave):持續同步 Primary 的資料變化,專門負責 Read

寫入固定走 Primary:

POST /posts
        ↓
    Primary

讀取則交給 Replica:

GET /feed
        ↓
    Replica

畫成架構圖:

Database Replication:Primary 與 Read Replica 架構圖

原本所有 Request 都擠在同一個 Database,現在讓多個擁有相同資料的 Database Instance 一起分攤 Read Traffic。

有一點要特別澄清:這樣做還稱不上 Database 的 Horizontal Scaling。

因為每個 Replica 保存的都是同一份完整資料,真正把資料切開、分散存放到不同節點的做法叫 Sharding / Partitioning,這是之後才會碰到的主題(Day 18)。

Replication 目前解決的,單純是 Read Scaling

不過,這個架構把讀跟寫拆到不同節點之後,也同時留下兩個要面對的代價:一個出在 Read 這一側,一個出在 Write 這一側,先從比較容易感覺到的 Read 開始看。

Replication Lag

假設 Pikachu 剛發布一篇貼文,Request 成功寫進 Primary,但這筆資料同步到其他 Replica 需要一點時間。

如果 Pikachu 發文後立刻重整頁面,而這次 Request 剛好被導到還沒同步完成的 Replica,他可能會看不到自己剛剛發的文章

這種「Primary 與 Replica 之間存在同步延遲」的現象,就是 Replication Lag

換句話說,Replication 讓我們拿到了 Read Scalability,代價是要面對資料一致性(Consistency) 的問題。

這其實已經涉及分散式系統一個很經典的理論:CAP Theorem,不過這個主題份量不小,值得完整用一篇來討論,所以先留到日後再深入拆解。

Replication Lag 只是「讀到舊資料」的問題,頂多體驗不佳;Write 這一側藏著一個嚴重得多的問題

如果 Primary 本身掛掉了呢?

Primary 掛掉了怎麼辦:Failover

在 Primary-Replica 架構下,Primary 是整個系統唯一能寫入的節點。一旦 Primary 掛掉,即使旁邊還有好幾個 Replica 好端端地活著,整個系統照樣寫不進去任何資料,所以沒有人能發文、按讚、註冊新帳號。

Replica 掛掉的影響則小很多:Load Balancer 只要把這個掛掉的 Replica 從輪詢名單裡拿掉,Read 流量分給其他還活著的 Replica 就好,使用者頂多感覺載入慢一點,不會整個服務打不開。

正因為 Primary 是單點故障(Single Point of Failure),需要一套明確的處理流程,把某一個 Replica「升格」成新的 Primary,讓系統能繼續接受寫入。這個流程就叫 Failover

Failover 大致的流程

  1. 偵測 Primary 掛了:通常會有一個獨立的監控角色(可能是 Database 內建的機制、雲端服務商提供的健康檢查,或另外架設的 Orchestrator),持續對 Primary 做心跳檢查(Health Check)。要連續幾次都沒回應,才會判定 Primary 真的掛了,而不是網路短暫抖動造成的誤判。
  2. 選出新的 Primary:從現有的 Replica 裡,挑一個資料最新(跟原本 Primary 的落差最小)的 Replica,升級成新的 Primary。
  3. 切換流量:所有原本寫入舊 Primary 的 Request,都要改成指向新 Primary。實務上通常靠一個「浮動」的位址(例如 DNS、VIP,或是統一管理連線的 Proxy)來做,這樣 Application 端不需要自己去改連線設定。
  4. 其他 Replica 認新主人:剩下還活著的 Replica,要重新指向新 Primary,繼續同步它的資料變化。

畫成時間軸:

[偵測] Primary 沒回應 → 連續 N 次心跳失敗,判定為 Down
    ↓
[選舉] 從 Replica A / B / C 裡,挑資料最新的 Replica B
    ↓
[升級] Replica B 升級為新 Primary
    ↓
[改道] 所有 Write 流量改指向新 Primary(Replica B)
    ↓
[跟隨] Replica A、C 改成跟著新 Primary 同步

Failover 沒有那麼單純

  • 自動還是手動? 手動 Failover 由 On-call 工程師確認後手動切換,安全但慢(可能要好幾分鐘甚至更久),這段時間系統完全無法寫入;自動 Failover 靠工具自動判斷、自動切換,恢復速度快很多,但也可能誤判(例如短暫網路分區被誤認為 Primary 真的掛了),反而製造出不必要的切換。
  • 資料遺失的風險:如果 Primary 和 Replica 之間是非同步(Asynchronous)複製(最常見的預設模式),Primary 掛掉的當下,可能還有幾筆剛寫入 Primary、但還來不及同步到任何 Replica 的資料,這些資料會直接遺失,回不來了。想要「零遺失」,需要改用同步複製(Synchronous Replication),但代價是每次寫入都要等 Replica 確認收到才能回應,寫入延遲會變高。
  • Split-Brain(腦裂):如果判斷失誤,舊 Primary 其實沒死透(只是暫時網路失聯),一恢復連線又以為自己還是 Primary 繼續接受寫入,就會同時存在兩個「自認為是 Primary」的節點,各自接受寫入,資料就此分岔。這正是為什麼 Failover 機制通常需要嚴謹的共識機制來確認「誰才是真正的 Primary」,而不是隨便哪個節點覺得自己是就算數。

實務上,很少有團隊會自己從零刻一套 Failover 機制,常見做法是直接仰賴雲端服務商內建的能力(例如 AWS RDS 的 Multi-AZ 自動 Failover),或是採用成熟的開源工具(例如 PostgreSQL 生態的 Patroni、MySQL 生態的 Orchestrator)來處理偵測、選舉與流量切換這整套流程。

看完這套「偵測 → 選舉 → 切換」的流程,會發現 Primary-Replica 架構下,Write 這一側從故障到恢復,中間終究有一段無法寫入的空窗期。如果系統真的無法接受這段空窗,還有沒有別的架構選擇?

Primary-Replica 不是唯一選擇:Multi-Primary

到目前為止採用的做法,正式名稱叫 Primary-Replica Replication:只有一個節點(Primary)能寫,其他節點(Replica)只能讀。

但這不是唯一的模式,另一種常見做法是 Multi-Primary Replication(也稱 Multi-Leader Replication):讓兩個(或多個)節點都能接受寫入,並且互相同步彼此的資料變化。

Primary A  ⇄  Primary B
(兩邊都能 Write,也互相同步對方)

Multi-Primary 的優點

  • 沒有單一寫入瓶頸:Primary-Replica 架構下,所有 Write 永遠只能走 Primary 這一個節點;Multi-Primary 則讓寫入壓力可以分攤到多個節點。
  • 故障時幾乎不會中斷寫入:Primary-Replica 架構下,Primary 一旦掛掉,得先經過上一節的 Failover 流程,把某個 Replica 升級成新 Primary 才能繼續寫入,中間有一段空窗期;Multi-Primary 因為兩邊本來就都能寫,其中一個掛掉,另一個可以直接繼續服務,不需要等待升級。

Multi-Primary 的代價:寫入衝突

問題也很直接:如果同一瞬間,有人對 Primary A 寫入 nickname = "黃色老鼠",又有人對 Primary B 寫入同一筆資料的 nickname = "皮神",兩邊都成功寫入之後,到底哪一個才是「正確」的結果?

這種 Write Conflict(寫入衝突) 是 Multi-Primary 最大的挑戰,常見的處理方式包括:

  • Last Write Wins:以時間戳記較新的那筆為準,方法簡單但可能悄悄丟掉另一邊的更新
  • Vector Clock(版本向量):記錄每筆資料的因果關係,區分「真的同時衝突」跟「只是先後順序不同」
  • 應用層合併邏輯:由 Application 自己定義衝突時該怎麼合併

這些機制都不簡單,這也是為什麼 Multi-Primary 通常只在真正需要「多個地點都要能寫入」的場景才會採用,例如之後會討論到的跨地區部署,讓每個地區都有自己的 Primary,本地寫入不需要跨海等待。

對現在規模的 PokeThreads 來說,Primary-Replica 已經夠用,沒必要為了還不存在的問題,提前引入 Multi-Primary 的複雜度。

小結

今天透過 Database Replication,把 Read Traffic 從單一 Database 分攤到多個 Replica 上:

Database Replication:Primary 與 Read Replica 架構圖

同時也拆解了 Primary-Replica 架構要付出的兩個代價:Read 這一側的 Replication Lag,以及 Write 這一側 Primary 故障時的 Failover 空窗期;並且看到 Multi-Primary 如何用「兩邊都能寫」換掉 Failover 的等待時間,但也換來了寫入衝突要處理。

Read 的壓力暫時緩解了,但每次 Feed Query 其實都要重新查一次 Database(不管是 Primary 還是 Replica),如果同樣的資料一直被大量使用者重複讀取,這樣做真的有必要嗎?這是明天要討論的問題。


上一篇
Day 6 Session 要放哪?
下一篇
Day 8 Cache 讓你不用每次都要讀 Database
系列文
系統設計就像九頭蛇:打造社群網站的 30 天10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言