[Day 26] Scaling Database (一)

erichung0404
2024-08-26 18:17:28

有了 Message Queue 之後, 我們已經將服務之間解綁, 現在只剩下 Database 還是單一節點, 所以今天就來 Scaling Database 吧!

由於 Database 是本身就帶有狀態, 所以處理起來會非常麻煩, 等之後再說明

先來看看幾種常見的 Scaling 策略

Replica

即將 Database 複製到其他節點上, 以提高可用性
但是由於 Database 帶有狀態, 需要一個方法來同步不同節點 Database 的資料
就像前面所介紹, 我們可以根據 CAP Theorem 將系統分類

CP 系統

強調資料 一致性 勝過 可用性, 所以在所有節點同步完成前, 我們會擋住所有使用者的操作, 造成可用性下降

AP 系統

強調 可用性 勝過 一致性, 可以允許一段時間內各節點間資料不一致的情形, 讓使用者能夠持續使用服務

Master-Slave 架構

一種模式是 Master-Slave 架構, 通常是一個 Master 節點搭配多個 Slave 節點
只有 Master 節點允許 寫入 的操作, Slave 節點只允許讀取, Master 節點定期將資料同步到 Slave 節點

優點是針對 "讀取" 的可用性提高了
代價是 "寫入" 仍然會有 SPOF 和效能瓶頸的問題

所以適合 Read-heavy 的情境

由於 Master 節點仍然有效能瓶頸, 所以可以進一步透過 Sharding 優化

Sharding (分片)

即是將資料根據 sharding key 切開分散儲存在不同的節點, 比如我們的訂單系統中 Booking 的 schema 設計如下

timestamp 對照表

1722441600000: 2024-08-01T00:00:00+08:00

1724601600000: 2024-08-26T00:00:00+08:00

1724688000000: 2024-08-27T00:00:00+08:00

room_type

1: 兩人房

2: 四人房

booking

id | id_user | id_room_type | check_in_date | check_out_date | ctime | mtime
1 | 1 | 1 | 1724601600000 | 1724688000000 | 1722441600000 | 1722441600000
2 | 1 | 2 | 1724601600000 | 1724688000000 | 1722441600000 | 1722441600000
3 | 2 | 1 | 1724601600000 | 1724688000000 | 1722441600000 | 1722441600000
4 | 3 | 2 | 1724601600000 | 1724688000000 | 1722441600000 | 1722441600000

(題外話: 實務上要記得處理時區的問題)

這邊偷懶一下都用相同的訂房時間
可以看到我們有 3 個使用者, 使用者 1 下了兩筆訂單, 使用者 2, 3 分別下了一筆訂單

假設我們的 Database 只能夠負擔 2 個並行請求, 假設這時候有 4 個併行請求, 此時就會有效能問題了 (先不管使用者 1 是怎麼做到同時下兩單的...瀏覽器多開之類的)

如果平均的併行請求已經超過我們的系統處理上限, 且沒有 Hotspot 的問題 (某些使用者特別常使用), 就可以考慮以使用者作為 sharding key, 將使用者的訂單資料分散到不同節點儲存, 比如我們將 id_user 對 2 取餘數, 0 存到節點 A, 1 存到節點 B

節點 A

id | id_user | id_room_type | check_in_date | check_out_date | ctime | mtime
2 | 1 | 2 | 1724601600000 | 1724688000000 | 1722441600000 | 1722441600000
4 | 3 | 2 | 1724601600000 | 1724688000000 | 1722441600000 | 1722441600000

節點 B

id | id_user | id_room_type | check_in_date | check_out_date | ctime | mtime
1 | 1 | 1 | 1724601600000 | 1724688000000 | 1722441600000 | 1722441600000
3 | 2 | 1 | 1724601600000 | 1724688000000 | 1722441600000 | 1722441600000

好處是能夠分散 Master 節點的寫入請求, 加快寫入速度, 提高可用性
缺點是由於只有 booking 表 sharding, 若需要跨節點的 transaction 就會很麻煩, 需要使用如 2PC, Saga Pattern 等方法解決, 前面已經提過, 就不贅述

除了 Replica 以外, 另一個常用於提高可用性和一致性的方法是 Consensus Algorithm

Consensus Algorithm (共識演算法)

共識演算法雖然是 去中心化架構, 但也算是分散式系統的一種

和 Replica 架構中將節點 讀寫 的職責分離不同, 共識演算法中所有的節點職責都是相同的, 沒有一個集中管理的節點負責分派任務或是同步節點, 因此沒有 SPOF 或是單一節點效能上限的問題

這邊以 Raft 為例, 簡單說明運作方式

參與 Raft 的所有節點稱作 群 (Cluster), 在每次 選舉 (Election) 中, 會選出一個 代表 (Leader) 節點, 其他節點為 跟隨者 (Follower)

1. 代表節點 節點定時 Polling 給所有 跟隨者節點 以同步狀態
所以即使有 跟隨者節點 下線也沒關係, 等到再次上線後也能夠被 代表節點同步

2. 跟隨者節點 收到 代表節點 的請求後更新自己的資料, 並回覆給 代表節點 完成同步

當 代表節點 下線

1. 跟隨者節點 因等不到 代表節點 的請求而超時

2. 超時的 跟隨者節點 變成 候選 (Candidate) 節點

3. 候選節點 舉辦下一輪選舉

4. 如果收到超過 群 中一半的節點回覆, 則此 候選節點 變為新的 代表節點
如果有多個 候選節點 同時發起選舉, 則採取 "先到先贏制", 即先收到 候選節點 請求的節點會支持該節點為 代表節點, 當有 候選節點 票數過半時, 則自動成為 代表節點, 並通知其他節點此更新

好處是確保資料的 強一致性 (透過選舉機制)
代價是犧牲了 可用性 (選舉期間無法寫入)
並且系統會變得很複雜 (跨節點一致性都很複雜...)
也可能受到 51% 攻擊 (內網環境下, 要入侵並控制我們系統節點的機會不高)

實作 Raft 的 RDBMS 有: YugaByteDB, CockroachDB, TiDB 等等
(題外話: 支援 Raft 的 RDBMS 有點難找囧 大部分都是以 Replication, Sharding 為主)

小結

Replica 和 共識演算法 的選擇視對於 一致性 的要求 (不考慮實作和維護的複雜度)

如果要求 強一致性 則建議用 共識演算法; 選擇 最終一致性 則建議用 Replica

Reference

Replication and Consensus Protocols

The Raft Consensus Algorithm