CAP 一致性協議及應用解析

一、一致性

1.1 CAP 理論

  1. C 一致性:分佈式環境中,一致性是指多個副本之間,在同一時刻能否有同樣的值

  2. A 可用性:系統提供的服務必須一直處於可用的狀態。即使集羣中一部分節點故障。

  3. P 分區容錯性:系統在遇到節點故障,或者網絡分區時,任然能對外提供一致性和可用性的服務。以實際效果而言,分區相當於通信的時限要求。系統如果不能在一定實現內達成數據一致性,也就意味着發生了分區的情況。必須就當前操作在 C 和 A 之前作出選擇

1.2 CAP 不能同時滿足的證明

假設系統中有 5 個節點,n1~n5。n1,n2,n3 在 A 物理機房。n4,n5 在 B 物理機房。現在發生了網絡分區,A 機房和 B 機房網絡不通。

保證一致性:此時客戶端在 A 機房寫入數據,不能同步到 B 機房。寫入失敗。此時失去了可用性。

保證可用性:數據在 A 機房的 n1~n3 節點都寫入成功後返回成功。數據在 B 機房的 n4~n5 節點也寫入數據,返回成功。同一份數據在 A 機房和 B 機房出現了數據不一致的情況。聰明如你,可以想到 zookeeper,當一個節點 down 掉,系統會將其剔出節點,然其它一半以上的節點寫入成功即可。是不是 zookeeper 同時滿足了 CAP 呢。其實這裏有一個誤區,系統將其剔出節點。有一個隱含的條件是,系統引入了一個調度者,一個踢出壞節點的調度者。當調度者和 zookeeper 節點出現網絡分區,整個系統還是不可用的。

1.3 常見場景

CA without P:在分佈式環境中,P 是不可避免的,天災(某軟公司的 Azure 被雷劈劈中)人禍(某裏公司 A 和 B 機房之間的光纜被挖斷)都能導致 P。

CP without A:相當於每個寫請求都須在 Server 之前強一致。P (分區) 會導致同步時間無限延長。這個是可以保證的。例如數據庫的分佈式事務,兩階段提交,三階段提交等。

AP without C:當網絡分區發生,A 和 B 集羣失去聯繫。爲了保證高可用,系統在寫入時,系統寫入部分節點就會返回成功,這會導致在一定時間之內,客戶端從不同的機器上面讀取到的是數據是不一樣的。例如 redis 主從異步複製架構,當 master down 掉,系統會切換到 slave,由於是異步複製,salve 不是最新的數據,會導致一致性的問題。

二、一致性協議

2.1 兩階段提交協議(2PC)

二階段提交 (Two-phaseCommit) 是指,在計算機網絡以及數據庫領域內,爲了使基於分佈式系統架構下的所有節點在進行事務提交時保持一致性而設計的一種算法 ( Algorithm )。通常,二階段提交也被稱爲是一種協議( Protocol )。在分佈式系統中,每個節點雖然可以知曉自己的操作時成功或者失敗,卻無法知道其他節點的操作的成功或失敗。當一個事務跨越多個節點時,爲了保持事務的 ACID 特性,需要引入一個作爲協調者的組件來統一掌控所有節點(稱作參與者) 的操作結果並最終指示這些節點是否要把操作結果進行真正的提交(比如將更新後的數據寫入磁盤等等)。因此,二階段提交的算法思路可以概括爲:參與者將操作成敗通知協調者,再由協調者根據所有參與者的反饋情報決定各參與者是否要提交操作還是中止操作。

2.1.1 兩種角色

2.1.2 處理階段

2.1.3 異常情況處理

2.1.4 缺點

2.2 三階段提交協議(3PC)

2PC 當時只考慮如果單機故障的情況,是可以勉強應付的。當遇到協調者和參與者同時故障的話,2PC 的理論是不完善的。此時 3PC 登場。

3PC 就是對 2PC 漏洞的補充協議。主要改動兩點:

  1. 在 2PC 的第一階段和第二階段插入一個準備階段,做到就算參與者和協調者同時故障也不阻塞,並且保證一致性。

  2. 在協調者和參與者之間引入超時機制

2.2.1 處理的三個階段

2.2.2 缺點

由於 3PC 有超時機制的存在,2PC 中未解決的問題,參與者和協調者同時 down 掉,也就解決了。一旦參與者在超時時間內沒有收到協調者的消息,就會自己提交。這樣也能避免參與者一直佔用共享資源。但是其在網絡分區的情況下,不能保證數據的一致性。

2.3 Paxos 協議

像 2PC 和 3PC 都需要引入一個協調者的角色,當協調者 down 掉之後,整個事務都無法提交,參與者的資源都出於鎖定的狀態,對於系統的影響是災難性的,而且出現網絡分區的情況,很有可能會出現數據不一致的情況。有沒有不需要協調者角色,每個參與者來協調事務呢,在網絡分區的情況下,又能最大程度保證一致性的解決方案呢。此時 Paxos 出現了。

Paxos 算法是 Lamport 於 1990 年提出的一種基於消息傳遞的一致性算法。由於算法難以理解起初並沒有引起人們的重視,Lamport 在八年後重新發表,即便如此 Paxos 算法還是沒有得到重視。2006 年 Google 的三篇論文石破天驚,其中的 chubby 鎖服務使用 Paxos 作爲 chubbycell 中的一致性,後來纔得到關注。

2.3.1 解決了什麼問題

2.3.2 兩種角色(兩者可以是同一臺機器)

由於 Paxos 和下文提到的 zookeeper 使用的 ZAB 協議過於相似,詳細講解參照下文, Zookeeper原理部分。

2.4 Raft 協議

Paxos 是論證了一致性協議的可行性,但是論證的過程據說晦澀難懂,缺少必要的實現細節,而且工程實現難度比較高廣爲人知實現只有 zk 的實現 zab 協議。然後斯坦福大學 RamCloud 項目中提出了易實現,易理解的分佈式一致性複製協議 Raft。Java,C++,Go 等都有其對應的實現。

2.4.1 基本名詞

2.4.2 特性

2.4.3 選主契機

  1. 在超時時間內沒有收到 Leader 的心跳

  2. 啓動時

2.4.4 選主過程

如圖 raft-2所示,Raft 將時間分爲多個 term(任期),term 以連續的整數來標識,每個 term 表示一個選舉的開始。例如 Follower 節點 1,在 term1 和 term2 連接處的時間,聯繫不到 Leader,將 currentTerm 編號加 1,變成 2,進入了到 term2 任期,在 term2 的藍色部分選舉完成,綠色部分正常工作。

當然一個任期不一定能選出 Leader,那麼會將 currentTerm 繼續加 1,然後繼續進行選舉,例如圖中的 t3。選舉的原則是,每一輪選舉每個選民一張選票,投票的請求先到且選民發現候選人節點的日誌 id 大於等於自己的,就會投票,否者不會投票。獲得半數以上的票的節點成爲主節點,注意這並不是說選出來的事務 id 一定是最大的,例如下圖 raft-1a~f 六個節點(正方形框裏面的數字是選舉的輪數 term)。

在第四輪選舉中,a 先發出投票,六臺機器中,a~e 都會投 a,即使 f 不投 a,a 也會贏得選舉。如果沒有事務 id(如剛啓動時),就遵循投票請求先來先頭。然後 Leader 將最新的日誌複製到各個節點,再對外提供服務。

當然除了這些選舉限制,還會有其他的情況。如 commit 限制等保證,Leader 選舉成功一定包含所有的 commit 和 log。

2.4.5 日誌複製過程

raft 日誌寫入過程,主節點收到一個 x=1的請求後,會寫入本地日誌,然後將 x=1的日誌廣播出去,follower 如果收到請求,會將日誌寫入本地 log ,然後返回成功。當 leader 收到半數以上的節點回應時,會將此日誌的狀態變爲 commit,然後廣播消息讓 follwer 提交日誌。節點在 commit 日誌後,會更新狀態機中的 logindex 。

firstLogIndex/lastLogIndex 爲節點中開始和結束的索引位置(包含提交,未提交,寫入狀態機)commitIndex:已提交的索引。applyIndex:已寫入狀態機中的索引。

日誌複製的本質是讓 follwer 和 Leader 的已提交的日誌順序和內容都完全一樣,用於保證一致性。

具體的原則就是:

 原則 1:兩個日誌在不同的 raft 節點中,如果有兩個相同的 term 和 logIndex ,則保證兩個日誌的內容完全一樣。

原則 2:兩段日誌在不同的 raft 節點中,如果起始和終止的的 term,logIndex 都相同,那麼兩段日誌中日誌內容完全一樣。

如何保證

第一個原則只需要在創建 logIndex 的時候使用新的 logIndex,保證 logIndex 的唯一性。而且創建之後不去更改。那麼在 leader 複製到 follwer 之後,logIndex,term 和日誌內容都沒變。

第二個原則,在 Leader 複製給 Follower 時,要傳遞當前最新日誌 currenTermId 和 currentLogIndex,以及上一條日誌 preCurrentTermId 和 preCurrentLogIndex。如圖 raft-1,在 d 節點,term7,logIndex12。在給節點節點 a 同步時,發送 (term7,logIndex11),(term7,logIndex12),a 節點沒有找到(term7,logIndex11) 的日誌,會讓 Leader,d 節點重新發送。d 節點會重新發(term6,logIndex10)(term7,logIndex11),還是沒有 (term6,logIndex10) 的日誌, 依然會拒絕同步。接着發 (term6,logIndex9)(term6,logIndex10)。現在 a 節點有了(term6,logIndex9)。那麼  leader 節點就會將(term6,logIndex9) ~ (term7,logIndex11) 日誌內容給節點 a,節點 a 將會和節點 d 有一樣的日誌。

三、Zookeeper 原理

3.1 概述

Google 的粗粒度鎖服務 Chubby 的設計開發者 Burrows 曾經說過:“所有一致性協議本質上要麼是 Paxos 要麼是其變體”。Paxos 雖然解決了分佈式系統中,多個節點就某個值達成一致性的通信協議。但是還是引入了其他的問題。由於其每個節點,都可以提議提案,也可以批准提案。當有三個及以上的 proposer 在發送 prepare 請求後,很難有一個 proposer 收到半數以上的回覆而不斷地執行第一階段的協議,在這種競爭下,會導致選舉速度變慢

所以 zookeeper 在 paxos 的基礎上,提出了 ZAB 協議,本質上是,只有一臺機器能提議提案(Proposer),而這臺機器的名稱稱之爲 Leader 角色。其他參與者扮演 Acceptor 角色。爲了保證 Leader 的健壯性,引入了 Leader 選舉機制。

ZAB 協議還解決了這些問題

  1. 在半數以下節點宕機,依然能對臺提供服務

  2. 客戶端所有的寫請求,交由 Leader 來處理。寫入成功後,需要同步給所有的 follower 和 observer

  3. leader 宕機,或者集羣重啓。需要確保已經再 Leader 提交的事務最終都能被服務器提交,並且確保集羣能快速回復到故障前的狀態

3.2 基本概念

3.3 常見的誤區

3.4 選舉同步過程

3.4.1 發起投票的契機

  1. 節點啓動

  2. 節點運行期間無法與 Leader 保持連接,

  3. Leader 失去一半以上節點的連接

3.4.2 如何保證事務

ZAB 協議類似於兩階段提交,客戶端有一個寫請求過來,例如設置 /my/test 值爲 1,Leader 會生成對應的事務提議(proposal)(當前 zxid 爲 0x5000010 提議的 zxid 爲 Ox5000011),現將 set/my/test1(此處爲僞代碼)寫入本地事務日誌,然後 set/my/test1日誌同步到所有的 follower。follower 收到事務 proposal ,將 proposal 寫入到事務日誌。如果收到半數以上 follower 的迴應,那麼廣播發起 commit 請求。follower 收到 commit 請求後。會將文件中的 zxid ox5000011 應用到內存中。

上面說的是正常的情況。有兩種情況。第一種 Leader 寫入本地事務日誌後,沒有發送同步請求,就 down 了。即使選主之後又作爲 follower 啓動。此時這種還是會日誌會丟掉(原因是選出的 leader 無此日誌,無法進行同步)。第二種 Leader 發出同步請求,但是還沒有 commit 就 down 了。此時這個日誌不會丟掉,會同步提交到其他節點中。

3.4.3 服務器啓動過程中的投票過程

現在 5 臺 zk 機器依次編號 1~5

  1. 節點 1 啓動,發出去的請求沒有響應,此時是 Looking 的狀態

  2. 節點 2 啓動,與節點 1 進行通信,交換選舉結果。由於兩者沒有歷史數據,即 zxid 無法比較,此時 id 值較大的節點 2 勝出,但是由於還沒有超過半數的節點,所以 1 和 2 都保持 looking 的狀態

  3. 節點 3 啓動,根據上面的分析,id 值最大的節點 3 勝出,而且超過半數的節點都參與了選舉。節點 3 勝出成爲了 Leader

  4. 節點 4 啓動,和 1~3 個節點通信,得知最新的 leader 爲節點 3,而此時 zxid 也小於節點 3,所以承認了節點 3 的 leader 的角色

  5. 節點 5 啓動,和節點 4 一樣,選取承認節點 3 的 leader 的角色

3.4.4 服務器運行過程中選主過程

  1. 節點 1 發起投票,第一輪投票先投自己,然後進入 Looking 等待的狀態。

  2. 其他的節點(如節點 2 )收到對方的投票信息。節點 2 在 Looking 狀態,則將自己的投票結果廣播出去(此時走的是上圖中左側的 Looking 分支);如果不在 Looking 狀態,則直接告訴節點 1 當前的 Leader 是誰,就不要瞎折騰選舉了(此時走的是上圖右側的 Leading/following 分支)。

  3. 此時節點 1,收到了節點 2 的選舉結果。如果節點 2 的 zxid 更大,那麼清空投票箱,建立新的投票箱,廣播自己最新的投票結果。在同一次選舉中,如果在收到所有節點的投票結果後,如果投票箱中有一半以上的節點選出了某個節點,那麼證明 leader 已經選出來了,投票也就終止了。否則一直循環。

zookeeper 的選舉,優先比較大 zxid,zxid 最大的節點代表擁有最新的數據。如果沒有 zxid,如系統剛剛啓動的時候,則比較機器的編號,優先選擇編號大的。

3.5 同步的過程

在選出 Leader 之後,zk 就進入狀態同步的過程。其實就是把最新的 zxid 對應的日誌數據,應用到其他的節點中。此 zxid 包含 follower 中寫入日誌但是未提交的 zxid 。稱之爲服務器提議緩存隊列 committedLog 中的 zxid。

同步會完成三個 zxid 值的初始化。

peerLastZxid:該 learner 服務器最後處理的 zxid。minCommittedLog:leader 服務器提議緩存隊列 committedLog 中的最小 zxid。maxCommittedLog:leader 服務器提議緩存隊列 committedLog 中的最大 zxid。

系統會根據 learner 的 peerLastZxid和 leader 的 minCommittedLogmaxCommittedLog做出比較後做出不同的同步策略。

3.5.1 直接差異化同步

場景:peerLastZxid介於 minCommittedLogZxidmaxCommittedLogZxid間。

此種場景出現在,上文提到過的,Leader 發出了同步請求,但是還沒有 commit 就 down 了。leader 會發送 Proposal 數據包,以及 commit 指令數據包。新選出的 leader 繼續完成上一任 leader 未完成的工作。

例如此刻 Leader 提議的緩存隊列爲 0x20001,0x20002,0x20003,0x20004,此處 learn 的 peerLastZxid 爲 0x20002,Leader 會將 0x20003 和 0x20004 兩個提議同步給 learner。

3.5.2 先回滾在差異化同步 / 僅回滾同步

此種場景出現在,上文提到過的,Leader 寫入本地事務日誌後,還沒發出同步請求,就 down 了,然後在同步日誌的時候作爲 learner 出現。

例如即將要 down 掉的 leader 節點 1,已經處理了 0x20001,0x20002,在處理 0x20003 時還沒發出提議就 down 了。後來節點 2 當選爲新 leader,同步數據的時候,節點 1 又神奇復活。如果新 leader 還沒有處理新事務,新 leader 的隊列爲,0x20001, 0x20002,那麼僅讓節點 1 回滾到 0x20002 節點處,0x20003 日誌廢棄,稱之爲僅回滾同步。如果新 leader 已經處理 0x30001 , 0x30002 事務,那麼新 leader 此處隊列爲 0x20001,0x20002,0x30001,0x30002,那麼讓節點 1 先回滾,到 0x20002 處,再差異化同步 0x30001,0x30002。

3.5.3 全量同步

peerLastZxid小於 minCommittedLogZxid或者 leader 上面沒有緩存隊列。leader 直接使用 SNAP 命令進行全量同步。

四、使用 Raft + RocksDB 有贊分佈式 KV 存儲服務

當前開源的緩存 kv 系統,大都是 AP 系統,例如設置主從同步集羣 redis,master 異步同步到 slave。雖然在 master 停止服務後,slave 會頂上來。但是在 master 寫入了數據,但是還沒來得及同步到 slave 就 down 了,然後 slave 被選爲主節點繼續對外提供服務的情況下,會丟失部分數據。這對於要求強一致性的系統來說是不可接受的。例如很多場景下 redis 做分佈式鎖,有天然的缺陷在裏面,如果 master 停止服務,這個鎖不很不可靠的,雖然出現的幾率很小,但一旦出現,將是致命的錯誤。

爲了實現 CP 的 KV 存儲系統,且要兼容現有的 redis 業務。有贊開發了 ZanKV(先已開源 ZanRedisDB)。

底層的存儲結構是 RocksDB(底層採用 LSM 數據結構)。一個 setx=1的會通過 redis protocol 協議傳輸,內容會通過 Raft 協議,同步寫入到其他的節點的 RocksDB。有了 Raft 理論的加持,RocksDB 優秀的存儲性能,即使遇到網絡分區,master 節點 down 掉, slave 節點 down 掉,等一系列異常情況,其都能輕鬆應對。在擴容方面,系統用選擇維護映射表的方式來建立分區和節點的關係,映射表會根據一定的算法並配合靈活的策略生成,來達到方便擴容。具體原理可參見《使用開源技術構建有贊分佈式 KV 存儲服務》。

五、總結

本文從三個方面介紹了一致性,首先是描述分佈架構中的核心理論 - CAP,以及其簡單的證明。第二部分介紹了 CAP 裏面協議,重點介紹了 Raft 協議。第三部分,重點介紹了常用的 zookeeper 原理。

爲了保證數據 commit 之後不可丟,系統都會採用(WAL write ahead log)(在每次修改數據之前先寫操作內容日誌,然後再去修改數據。即使修改數據時異常,也可以通過操作內容日誌恢復數據)

分佈式存儲系統中,是假設機器是不穩定,隨時都有可能 down 掉的情況下來設計的。也就是說就算機器 down 掉了,用戶寫入的數據也不能丟,避免單點故障。爲此每一份寫入的數據,需要在多個副本中同時存放。例如 zk 節點數據複製,etcd 的數據複製。而複製數據給節點又會帶來一致性的問題,例如主節點和從節點數據不一致改如何去同步數據。也會帶來可用性的問題,如 leader 節點 down 掉,如何快速選主,恢復數據等。好在已有成熟的理論如 Paxos 協議,ZAB 協議 Raft 協議等做爲支撐。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/d2dIUzjNjvVdDl6_uxMt3g