共識Raft:如何保證多機房數據的一致性?
當機房 A 修改了一條數據的同時,機房 B 也對該數據進行了更新,Otter 會通過合并邏輯來處理沖突的數據行或字段,以達到合并效果。為了避免這種沖突,我們在上節課對客戶端提出了要求:用戶客戶端在一定時間內只能連接一個機房。然而,如果業務對“事務和強一致性”有極高的需求,例如庫存不允許超賣的場景,通常只有兩種選擇:一種是將服務部署為本地服務,但這并不適用于所有業務;另一種是使用多機房架構,但必須采用分布式強一致性算法以確保多個副本之間的一致性。
在業界,最知名的分布式強一致性算法是 Paxos。盡管它的原理非常抽象,經過多次修改后往往會與最初設計產生很大偏離,使得很多人難以判斷這些修改是否合理。通常需要一到兩年的實踐經驗才能完全掌握該算法。隨著對分布式多副本同步需求的增加,Paxos 的抽象性已無法完全滿足市場需求,因此 Raft 算法應運而生。相比于 Paxos,Raft 更易于理解,同時能夠保證操作的順序一致性,因此被廣泛應用于分布式數據服務中,像 etcd 和 Kafka 等知名基礎組件都使用了 Raft 算法。
如何選舉 Leader?
圖片
如圖所示,我們啟動了五個 Raft 分布式數據服務節點:S1、S2、S3、S4 和 S5。每個節點可以處于以下三種狀態之一:
- Leader:負責處理數據修改,并主動將變更同步到其他 Follower 節點。
- Follower:接收并應用 Leader 推送的變更數據。
- Candidate:當集群中沒有 Leader 時,Follower 節點會進入選舉模式。
如果某個 Follower 節點在規定時間內未收到 Leader 的心跳信號,則意味著當前 Leader 可能已失效,集群無法繼續更新數據。在這種情況下,Follower 節點會進入選舉模式,并在多個 Follower 節點之間選出一個新的 Leader,確保服務集群中始終有一個 Leader,確保數據變更的唯一決策進程。
那么 Leader 是如何選舉出來的呢?在進入選舉模式后,這五個節點會各自隨機等待一段時間。等待時間一到,當前節點會先為自己投一票,并將其任期(term)加 1(如圖中的 term:4 表示第四任 Leader),然后通過發送 RequestVote RPC(即請求投票的遠程過程調用)向其他節點拉票。
圖片
S1失去聯系,S5最先超時發起選舉
當服務節點收到投票請求時,如果該請求節點的任期和日志同步進度都領先或相同,則會向其投票,并將自己的當前任期更新為新的任期。在此期間,收到投票請求的節點將不會再發起投票,而是等待其他節點的投票邀請。需要注意的是,每個節點在同一任期內只能投出一票。
如果所有節點在選舉過程中都未獲得多數票(即超過半數的票數),則在選舉超時后,節點會將任期加 1 并重新發起選舉。最終,獲得多數票且最早結束選舉倒計時的節點將當選為新的 Leader。該 Leader 隨即廣播通知其他節點,并同步新的任期和日志進度。
在成為 Leader 后,新的 Leader 會定期發送心跳信號,確保各個 Follower 節點保持連接狀態,不因超時而進入選舉模式。在選舉過程中,如果有節點收到了前一任 Leader 的心跳信號,便會停止當前的選舉并拒絕新的選舉請求。選舉結束后,所有節點進入數據同步狀態,確保日志一致性。
如何保證多副本寫一致?
當服務節點收到投票請求時,如果該請求節點的任期和日志同步進度都領先或相同,則會向其投票,并將自己的當前任期更新為新的任期。在此期間,收到投票請求的節點將不會再發起投票,而是等待其他節點的投票邀請。需要注意的是,每個節點在同一任期內只能投出一票。
如果所有節點在選舉過程中都未獲得多數票(即超過半數的票數),則在選舉超時后,節點會將任期加 1 并重新發起選舉。最終,獲得多數票且最早結束選舉倒計時的節點將當選為新的 Leader。該 Leader 隨即廣播通知其他節點,并同步新的任期和日志進度。
在成為 Leader 后,新的 Leader 會定期發送心跳信號,確保各個 Follower 節點保持連接狀態,不因超時而進入選舉模式。在選舉過程中,如果有節點收到了前一任 Leader 的心跳信號,便會停止當前的選舉并拒絕新的選舉請求。選舉結束后,所有節點進入數據同步狀態,確保日志一致性。
圖片
具體來說,當 Leader 成功修改數據后,它會生成一條對應的日志,并將該日志發送給所有 Follower 節點進行同步。只要超過半數的 Follower 返回同步成功的反饋,Leader 就會將該預提交的日志正式提交(commit),并向客戶端確認數據修改成功。
在下一個心跳中,Leader 會通過消息中的 leader commit 字段,將當前最新提交的日志索引(Log index)告知各 Follower 節點。Follower 節點依據該提交的索引更新數據,僅對外提供被 Leader 最終提交的數據,未被提交的數據不會被持久化或展示。
如果在數據同步期間,客戶端繼續向 Leader 發送其他修改請求,這些請求會進入隊列等待處理,因為此時 Leader 正在等待其他節點的同步響應,導致暫時的阻塞。
圖片
不過,這種阻塞等待的設計使得 Raft 算法對網絡性能的依賴性較強,因為每次數據修改都需要向多個節點發出并發請求,等待大多數節點成功同步。最糟糕的情況是,返回的往返時延(RTT)會受到最慢節點的網絡響應時間影響,例如“兩地三中心”的一次同步時間可能達到約 100ms。此外,由于主節點只有一個,這限制了 Raft 服務的整體性能。
為了解決這個問題,我們可以通過減少數據量和對數據進行切片來提升集群的修改性能。需要注意的是,當大多數 Follower 與 Leader 的日志進度差異過大時,數據變更請求將會處于等待狀態,直到超過一半的 Follower 與 Leader 的進度一致后,才會返回修改成功的結果。當然,這種情況并不常見。
服務之間如何同步日志進度?
講到這我們不難看出,在 Raft 的數據同步機制中,日志發揮著重要的作用。在同步數據時,Raft 采用的日志是一個有順序的指令日志 WAL(Write Ahead Log),類似 MySQL 的 binlog。該日志中記錄著每次修改數據的指令和修改任期,并通過 Log Index 標注了當前是第幾條日志,以此作為同步進度的依據。
在 Raft 中,Leader 節點的日志是永久保留的,所有 Follower 節點會與 Leader 保持完全一致。如果出現差異,Follower 的日志將被強制覆蓋以與 Leader 同步。此外,每條日志記錄都經過“寫入”和“提交”(commit)兩個階段。在選舉過程中,每個節點會基于自身未提交的日志索引(Log Index)進度優先選擇進度最靠前的節點,從而確保當選的 Leader 擁有最新、最完整的數據。
在 Leader 任期內,它會按順序向各 Follower 節點推送日志以實現同步。若 Leader 的同步進度領先于某個 Follower,該 Follower 將拒絕同步請求。收到拒絕反饋后,Leader 會從日志末尾向前回溯,找出 Follower 未同步或存在差異的日志部分,然后逐條推送覆蓋,直到 Follower 與 Leader 保持一致。
圖片
Leader 和 Follower 的日志同步進度是通過日志索引(index)來確認的。Leader 對日志的內容和順序擁有絕對的決策權,當發現自己的日志與某個 Follower 的日志存在差異時,為了確保多個副本的數據完全一致,Leader 會強制覆蓋該 Follower 的日志。
那么,Leader 是如何識別 Follower 的日志與自己的日志之間的差異呢?在向 Follower 同步日志時,Leader 會同時提供自己上一條日志的任期和索引號,與 Follower 當前的同步進度進行比較。對比主要涉及兩個方面:
- 當前日志對比:Leader 會對比自己和 Follower 的當前日志中的索引、多條操作日志和任期。
- 上一條日志對比:Leader 會對比自己和 Follower 的上一條日志的索引和任期。
如果以上任意一個方面存在差異,Leader 就會認為 Follower 的日志與自己的日志不一致。在這種情況下,Leader 會逐條倒序對比日志,直到找到日志內容和任期完全一致的索引,然后從這個索引開始按順序向下覆蓋。
在日志同步期間,Leader 只會提交其當前任期內的數據,之前任期的數據則依賴日志同步來逐步恢復。可以看到,這種逐條推送的同步方式效率較低,特別是對新啟動的服務并不友好。因此,Leader 會定期生成快照,將之前的修改日志記錄合并,以降低日志的大小。同時,進度差距過大的 Follower 會從 Leader 的最新快照中恢復數據,并按快照最后的索引追趕進度。
如何保證讀取數據的強一致性?
通過前面的講解,我們了解了 Leader 和 Follower 之間的同步機制,那么從 Follower 的角度來看,它又是如何確保自己對外提供的數據始終是最新的呢?這里有一個小技巧:當 Follower 收到查詢請求時,會同時向 Leader 請求當前最新提交的日志索引(commit log index)。如果這個日志索引大于 Follower 當前的同步進度,就意味著 Follower 的本地數據不是最新的。此時,Follower 會從 Leader 獲取最新的數據并返回給客戶端。
由此可見,保持數據的強一致性需要付出較大的代價。
圖片
你可能會好奇:如何在業務使用時保證讀取數據的強一致性呢?其實我們之前說的 Raft 同步等待 Leader commit log index 的機制,已經確保了這一點。我們只需要向 Leader 正常提交數據修改的操作,Follower 讀取時拿到的就一定是最新的數據。
總結
很多人都說 Raft 是一個分布式一致性算法,但實際上 Raft 算法是一個共識算法(多個節點達成共識),它通過任期機制、隨機時間和投票選舉機制,實現了服務動態擴容及服務的高可用。通過 Raft 采用強制順序的日志同步實現多副本的數據強一致同步,如果我們用 Raft 算法實現用戶的數據存儲層,那么數據的存儲和增刪改查,都會具有跨機房的數據強一致性。這樣一來,業務層就無需關心一致性問題,對數據直接操作,即可輕松實現多機房的強一致同步。