複製目的:
- 訪問的數據地理位置更接近用戶 減少訪問延時 (多數據中心)
- 節點宕機數據仍可用 (高可用)
- 多副本可讀,增加讀吞吐量
主從複製:
同步複製 -> 強一致性, 弱可用性 一旦某個從節點宕機則寫失敗
解決方案:一個從節點同步複製,其他異步複製,一旦同步節點宕機提升一個異步節點爲同步節點
新強一致性複製算法: chain replication
異步複製 -> 強可用性,弱一致性
增加從節點:
挑戰:主節點數據仍在寫入,直接複製會導致數據不一致
方案:
複製數據時鎖主節點不允許寫 ,太粗暴
更好的方案分3步:
- 對主節點數據做一個一致性快照,並記錄對應的複製日誌位置
- 複製快照到從節點
- 將快照後新增的複製日誌應用到從節點上,直到追上主節點
從節點宕機恢復:
從宕機前複製日誌位置開始追趕
主節點宕機 -> FAILOVER 機制
比較複雜,一般需要3步:
- 宕機檢測 : 一般通過心跳
- 重新選主,一致性算法和多數派協議
- 請求路由到新主節點,並通知客戶端
問題: - 數據丟失,新選的主節點沒有全部原主節點的數據
- 腦裂:兩個節點同時認爲自己是主節點
- 合理的心跳超時設置 (網絡抖動可能誤報)
手動FAILOVER優於自動?
複製日誌實現:
- 基於SQL語句: (MYSQL早期版本)
問題: 特殊函數now(), random() 等帶來的不確定性. 自增主鍵衝突. trigger, 存儲過程等的影響 - 基於WAL日誌: (POSTGRESQL, ORACLE)
問題:和存儲引擎強耦合, 滾動升級可能有問題 - 邏輯複製: 自定義結構,基於修改的數據值.
優點:不依賴於存儲引擎, 兼容性好 - 觸發器(TRIGGER): (DATABUS FOR ORACLE, BUCARDO)
優點:靈活性
問題:OVERHEAD
複製延時的問題:
最終一致性: 以較弱的一致性換取讀的可擴展性
一致性多弱取決於複製延時和讀取策略
-
read-your-own-write-consistency (read-after-write consistency): 確保自己寫入的數據一定會被隨後的讀請求讀到
實現方式:
如果用戶只修改自身的數據並知道自身數據的ID, 自身數據從主節點讀,其他數據從隨機節點讀
如果用戶可以修改許多其他的數據,需要客戶端記錄 key -> last modify time. 在一定時間範圍內只從主節點讀, 否則從隨機節點讀. 或者客戶端把 key+last modify time 發給查詢節點, 查詢節點比較時間戳,等待趕上再返回結果或者將讀請求
多數據中心: 路由讀請求到同一數據中心 -
monotonic read: 確保第二次讀結果不會比第一次舊
實現方式:
對同一主鍵每次讀從同一副本(可能出現數據傾斜) - consistent prefix read: 確保讀寫的因果順序不會錯亂 (比如問答順序)
實現方式:
全局的一致性寫順序
追蹤因果關係causual dependency的算法
通過分佈式事務解決複製延時問題.
多主複製:
支持多點寫入
每個LEADER對於另一個LEADER是FOLLOWER
主要應用場景是多數據中心
每個數據中心有一個LEADER, 數據從LEADER複製到本數據中心的FOLLOWER和另一個數據中心的LEADER.
性能:寫請求可由LOCAL 數據中心的LEADER處理. 不用跨廣域網
數據中心容錯: 一個數據中心宕機後另一個數據中心仍可獨立運行,數據中心恢復後再將數據複製過去
網絡容錯:跨廣域網異步複製,暫時網絡不穩定不會影響寫入
缺點:必須解決寫衝突
設計時必須考慮自增主鍵衝突. TRIGGER, 數據完整性約束等
寫衝突檢測和解決
- 同步檢測:等數據複製到每個節點再檢測衝突並通知用戶. 延遲大,完全犧牲了多主複製的優勢
- 衝突避免: 保證相同的鍵寫入到同一個數據中心, 數據HASH,路由等. 數據中心如果宕機或者用戶位置改變則會出現問題
- 每個寫操作一個UUID,對同一數據的最後一次寫獲勝 (基於時間戳等), 可能丟失數據
- 每個REPLICA有一個UUID, 對同一數據寫來自高位REPLICA獲勝, 可能丟失數據
- 保存寫衝突數據,之後自動解決或者提示用戶解決
解決衝突的時機:
寫時:一旦衝突發生,只能後臺解決無法讓用戶干預 (Bucardo)
讀時: 保存衝突數據,在下次讀的時候自動解決或提示用戶(CouchDB)
事務中的每個寫衝突會單獨解決,無法保持事務邊界
自動衝突解決:
Conflict free replicated data types
Mergeable persistent data structures (GIT)
Operational transformation
多主複製拓撲:(超過兩個MASTER)
星狀
環狀
所有對所有
需要防止循環複製(寫請求記錄經過的節點編號)
星狀或者環狀複製某個節點宕機會導致不可寫
所有對所有 容易因延遲導致數據一致性問題或寫失敗
無主複製:
沒有副本概念,客戶端直接寫多個節點
多數節點響應寫請求則成功
讀請求也發送到多個節點,版本號保證讀取最新的數據
保證: 寫成功節點數(W) + 讀節點數(R) > 集羣節點數(N)
讀寫並行發往所有節點, W, R 爲響應的節點數量
如果 W + R < N 犧牲一致性換取低延時和高可用性
一般選擇 W > N/2, R > N/2,容許最多N/2 節點宕機
W + R > N 依然有可能讀到舊數據:
- Sloppy Quorum
- 併發寫衝突
- 讀寫衝突, 讀的節點可能沒寫入最新數據
- 寫某些節點失敗,寫成功節點未回滾
- 寫成功節點宕機,數據從舊節點恢復
監控數據陳舊程度很困難, 無法像主從複製一樣監控REPLICATION LAG. 寫順序不確定
Sloppy quorum: 部分節點宕機導致無法寫入通常寫入的節點(讀寫遷移)
Hinted handoff: 節點恢復後讀寫遷移的節點將臨時寫入數據寫回通常寫入節點
Sloppy quorum 提高了可用性但是降低了讀到最新數據的可能
無主複製也適合多數據中心的場景: 寫請求發送多數據中心但只等待本數據中心確認
保證宕機節點最終一致性:
Read repair: 客戶端發現讀版本衝突自動更新版本比較舊的節點
Anti-entropy process: 後臺進程自動比較數據版本並修復 (不一致時間長)
寫衝突可能發生在以下情況:
- 節點因網絡故障丟失寫請求
- 不同節點收到寫順序不一致
寫衝突處理:
- 最後寫獲勝:爲寫請求分配版本號,衝突只保留版本最大的數據. 可能丟失數據,適用於每個KEY寫一次不再更新的場景 (時序數據?)
- 併發寫定義: 如果兩次寫操作有強時序關係或互相依賴 (如A爲插入key1,B爲key + 1),則A,B稱爲causually dependent. 任意寫操作A, B, 必有A在B之前,A在B之後,或者AB爲併發
- 併發寫檢測算法:
每個KEY維護一個版本,每次寫版本+1
客戶端寫KEY之前必須先從SERVER讀一次KEY的最新版本
寫操作帶上寫之前讀到的KEY版本,可以覆蓋同一個KEY更低或者相同版本,但是更高版本必須保留 (併發寫)
購物車舉例:
A, B 兩個客戶端同時向購物車添加產品,每次寫提交,服務器端生成一個購物車新版本,並將最新版本的購物車內容及相應版本號返回給提交寫請求的客戶端
客戶端提交寫請求時要將提交的內容和之前的購物車版本合併,並附帶之前的版本
服務端針對同一個客戶端,把新的版本覆蓋之前的版本,但是不同客戶端提交的最新版本認爲是concurrent write,不會互相覆蓋,需要衝突解決 - 處理併發寫:
由客戶端合併多個concurrent write的版本.
刪除操作不能直接物理刪除,而要標記(tombstone)
自動衝突解決, 見上文 - 向量鍾 (version vector):
適用於leaderless replication, 沒有主節點而是多個對等的replica
版本號針對於每個replica的每個KEY
寫之前先從replica讀取最新的version vector, 合併寫數據再發回
保證從replica A讀,再寫到replica B是安全的