1. 程式人生 > 實用技巧 >一致性模型及一致性協議

一致性模型及一致性協議

一、一致性模型概念

提到分散式架構就一定繞不開“一致性”問題,而“一致性”其實又包含了資料一致性事務一致性兩種情況,下面是對強一致性、最終一致性、因果一致性、單調讀一致性、單調寫一致性、會話一致性的解釋。

1.1強一致性:在任何時刻所有的使用者或者程序查詢到的都是最近一次成功更新的資料。強一致性是程度最高一致性要求,也是最難實現的。關係型資料庫更新操作就是這個案例。關係型資料庫的強一致性其實也就是事務的特性,事務是一組操作的執行單元,相對於資料庫操作來講,事務管理的是一組SQL指令,比如增加,修改,刪除等,事務的一致性要求這個事務內的操作必須全部執行成功,如果在此過程種出現了差錯,比如有一條SQL語句沒有執行成功,那麼這一組操作都將全部回滾。

  最經典的例子便是:A向B匯款500元,B賬戶多了500元,這整個過程,要麼全部正常執行,要麼全部回滾,不然就會出現A扣款,B收不到錢,或者A沒扣款,B收到500元的情況,這種場景是災難性的。

事務ACID特性

原子性(Atomicity)
原子性是指事務是一個不可分割的工作單位,事務中的操作要麼都發生,要麼都不發生。
一致性(Consistency)
事務前後資料的完整性必須保持一致。
隔離性(Isolation)
事務的隔離性是多個使用者併發訪問資料庫時,資料庫為每一個使用者開啟的事務,不能被其他事務的操作資料所幹擾,多個併發事務之間要相互隔離。
永續性(Durability)
永續性是指一個事務一旦被提交,它對資料庫中資料的改變就是永久性的,接下來即使資料庫發生故障也不應該對其有任何影響

1.2.最終一致性:和強一致性相對,在某一時刻使用者或者程序查詢到的資料可能都不同,但是最終成功更新的資料都會被所有使用者或者程序查詢到。當前主流的nosql資料庫都是採用這種一致性策略。

1.3.因果一致性:因果一致性發生在程序之間有相互依賴關係的情形下。例如AB兩個程序相互依賴,那麼如果A對某個變數進行更新,他在更新之後會通知B,這時候B看到的就是新值,但是如果還有程序C,那麼C看到的值可能還是舊值。

1.4.單調讀一致性:如果程序已經看到過資料物件的某個值,那麼任何後續訪問都不會返回該值之前的值。

1.5.單調寫一致性:系統保證來自同一個程序的寫操作順序執行。

1.6 6.會話一致性:提交更新操作的使用者在同一個會話裡讀取該資料時能夠保證資料是最新,

二、CAP原則


CAP原則又稱CAP定理,指的是在一個分散式系統中,一致性(Consistency)、可用性(Availability)、分割槽容錯性(Partition tolerance)。CAP 原則指的是,這三個要素最多隻能同時實現兩點,不可能三者兼顧

分割槽容錯性:指的分散式系統中的某個節點或者網路分割槽出現了故障的時候,整個系統仍然能對外提供滿足一致性和可用性的服務。也就是說部分故障不影響整體使用。

事實上我們在設計分散式系統是都會考慮到bug,硬體,網路等各種原因造成的故障,所以即使部分節點或者網路出現故障,我們要求整個系統還是要繼續使用的(不繼續使用,相當於只有一個分割槽,那麼也就沒有後續的一致性和可用性了)

可用性:一直可以正常的做讀寫操作。簡單而言就是客戶端一直可以正常訪問並得到系統的正常響應。使用者角度來看就是不會出現系統操作失敗或者訪問超時等問題。

一致性:在分散式系統完成某寫操作後任何讀操作,都應該獲取到該寫操作寫入的那個最新的值。相當於要求分散式系統中的各節點時時刻刻保持資料的一致性。

CAP原則的精髓就是要麼AP,要麼CP,要麼AC,但是不存在CAP。如果在某個分散式系統中資料無副本, 那麼系統必然滿足強一致性條件, 因為只有獨一資料,不會出現資料不一致的情況,此時C和P兩要素具備,但是如果系統發生了網路分割槽狀況或者宕機,必然導致某些資料不可以訪問,此時可用性條件就不能被滿足,即在此情況下獲得了CP系統,但是CAP不可同時滿足

因此在進行分散式架構設計時,必須做出取捨。當前一般是通過分散式快取中各節點的最終一致性來提高系統的效能,通過使用多節點之間的資料非同步複製技術來實現叢集化的資料一致性。通常使用類似 memcached 之類的 NOSQL 作為實現手段。雖然 memcached 也可以是分散式叢集環境的,但是對於一份資料來說,它總是儲存在某一臺 memcached 伺服器上。如果發生網路故障或是伺服器宕機,則儲存在這臺伺服器上的所有資料都將不可訪問。由於資料是儲存在記憶體中的,重啟伺服器,將導致資料全部丟失。當然也可以自己實現一套機制,用來在分散式 memcached 之間進行資料的同步和持久化,但是實現難度是非常大的。

(1) CA: 優先保證一致性和可用性,放棄分割槽容錯。 這也意味著放棄系統的擴充套件性,系統不再是分散式的,有違設計的初衷。

(2) CP: 優先保證一致性和分割槽容錯性,放棄可用性。在資料一致性要求比較高的場合(譬如:zookeeper,Hbase) 是比較常見的做法,一旦發生網路故障或者訊息丟失,就會犧牲使用者體驗,等恢復之後使用者才逐漸能訪問。

(3) AP: 優先保證可用性和分割槽容錯性,放棄一致性。NoSQL中的Cassandra 就是這種架構。跟CP一樣,放棄一致性不是說一致性就不保證了,而是逐漸的變得一致。

三、一致性協議

3.1 Zab協議

Zab協議 的全稱是Zookeeper Atomic Broadcast(Zookeeper原子廣播)。ZAB 協議是為分散式協調服務ZooKeeper專門設計的一種支援崩潰恢復的一致性協議。基於該協議,ZooKeeper 實現了一種主從模式的系統架構來保持叢集中各個副本之間的資料一致性。

就這樣,客戶端傳送來的寫請求,全部給Leader,然後leader再轉給Follower。這時候需要解決兩個問題:

(1)leader伺服器是如何把資料更新到所有的Follower的。

(2)Leader伺服器突然間失效了,怎麼辦?

因此ZAB協議為了解決上面兩個問題,Zab 協議設計包括兩種基本的模式:崩潰恢復訊息廣播

(1)訊息廣播模式:把資料更新到所有的Follower

(2)崩潰恢復模式:Leader發生崩潰時,如何恢復

協議過程

當整個叢集啟動過程中,或者當 Leader 伺服器出現網路中弄斷、崩潰退出或重啟等異常時,Zab協議就會進入崩潰恢復模式,選舉產生新的Leader。

當選舉產生了新的 Leader,同時叢集中有過半的機器與該 Leader 伺服器完成了狀態同步(即資料同步)之後,Zab協議就會退出崩潰恢復模式,進入訊息廣播模式

這時,如果有一臺遵守Zab協議的伺服器加入叢集,因為此時叢集中已經存在一個Leader伺服器在廣播訊息,那麼該新加入的伺服器自動進入恢復模式:找到Leader伺服器,並且完成資料同步。同步完成後,作為新的Follower一起參與到訊息廣播流程中。

協議狀態切換

當Leader出現崩潰退出或者機器重啟,亦或是叢集中不存在超過半數的伺服器與Leader儲存正常通訊,Zab就會再一次進入崩潰恢復,發起新一輪Leader選舉並實現資料同步。同步完成後又會進入訊息廣播模式,接收事務請求。

保證訊息有序

在整個訊息廣播中,Leader會將每一個事務請求轉換成對應的 proposal 來進行廣播,並且在廣播 事務Proposal 之前,Leader伺服器會首先為這個事務Proposal分配一個全域性單遞增的唯一ID,稱之為事務ID(即zxid),由於Zab協議需要保證每一個訊息的嚴格的順序關係,因此必須將每一個proposal按照其zxid的先後順序進行排序和處理。

訊息廣播

1)在zookeeper叢集中,資料副本的傳遞策略就是採用訊息廣播模式。zookeeper中農資料副本的同步方式與二段提交相似,但是卻又不同。二段提交要求協調者必須等到所有的參與者全部反饋ACK確認訊息後,再發送commit訊息。要求所有的參與者要麼全部成功,要麼全部失敗。二段提交會產生嚴重的阻塞問題。

2)Zab協議中 Leader 等待 Follower 的ACK反饋訊息是指“只要半數以上的Follower成功反饋即可,不需要收到全部Follower反饋”

訊息廣播具體步驟

1)客戶端發起一個寫操作請求。

2)Leader 伺服器將客戶端的請求轉化為事務 Proposal 提案,同時為每個 Proposal 分配一個全域性的ID,即zxid。

3)Leader 伺服器為每個 Follower 伺服器分配一個單獨的佇列,然後將需要廣播的 Proposal 依次放到佇列中取,並且根據 FIFO 策略進行訊息傳送。

4)Follower 接收到 Proposal 後,會首先將其以事務日誌的方式寫入本地磁碟中,寫入成功後向 Leader 反饋一個 Ack 響應訊息。

5)Leader 接收到超過半數以上 Follower 的 Ack 響應訊息後,即認為訊息傳送成功,可以傳送 commit 訊息。

6)Leader 向所有 Follower 廣播 commit 訊息,同時自身也會完成事務提交。Follower 接收到 commit 訊息後,會將上一條事務提交。

zookeeper 採用 Zab 協議的核心,就是隻要有一臺伺服器提交了 Proposal,就要確保所有的伺服器最終都能正確提交 Proposal。這也是 CAP/BASE 實現最終一致性的一個體現。

Leader 伺服器與每一個 Follower 伺服器之間都維護了一個單獨的 FIFO 訊息佇列進行收發訊息,使用佇列訊息可以做到非同步解耦。 Leader 和 Follower 之間只需要往佇列中發訊息即可。如果使用同步的方式會引起阻塞,效能要下降很多。

崩潰恢復

一旦 Leader 伺服器出現崩潰或者由於網路原因導致 Leader 伺服器失去了與過半 Follower 的聯絡,那麼就會進入崩潰恢復模式。

在 Zab 協議中,為了保證程式的正確執行,整個恢復過程結束後需要選舉出一個新的 Leader 伺服器。因此 Zab 協議需要一個高效且可靠的 Leader 選舉演算法,從而確保能夠快速選舉出新的 Leader 。

Leader 選舉演算法不僅僅需要讓 Leader 自己知道自己已經被選舉為 Leader ,同時還需要讓叢集中的所有其他機器也能夠快速感知到選舉產生的新 Leader 伺服器。

崩潰恢復主要包括兩部分:Leader選舉資料恢復。

特殊情況下需要解決的兩個問題:

問題一:已經被處理的事務請求(proposal)不能丟(commit的)

當 leader 收到合法數量 follower 的 ACKs 後,就向各個 follower 廣播 COMMIT 命令,同時也會在本地執行 COMMIT 並向連線的客戶端返回「成功」。但是如果在各個 follower 在收到 COMMIT 命令前 leader 就掛了,導致剩下的伺服器並沒有執行都這條訊息。如何解決已經被處理的事務請求(proposal)不能丟(commit的)呢?

1、選舉擁有 proposal 最大值(即 zxid 最大) 的節點作為新的 leader。 由於所有提案被 COMMIT 之前必須有合法數量的 follower ACK,即必須有合法數量的伺服器的事務日誌上有該提案的 proposal,因此,zxid最大也就是資料最新的節點儲存了所有被 COMMIT 訊息的 proposal 狀態。
2、新的 leader 將自己事務日誌中 proposal 但未 COMMIT 的訊息處理。 3、新的 leader 與 follower 建立先進先出的佇列, 先將自身有而 follower 沒有的 proposal 傳送給 follower,再將這些 proposal 的 COMMIT 命令傳送給 follower,以保證所有的 follower 都儲存了所有的 proposal、所有的 follower 都處理了所有的訊息。通過以上策略,能保證已經被處理的訊息不會丟。 問題二:沒被處理的事務請求(proposal)不能再次出現什麼時候會出現事務請求被丟失呢?
當 leader 接收到訊息請求生成 proposal 後就掛了,其他 follower 並沒有收到此 proposal,因此經過恢復模式重新選了 leader 後,這條訊息是被跳過的。 此時,之前掛了的 leader 重新啟動並註冊成了 follower,他保留了被跳過訊息的 proposal 狀態,與整個系統的狀態是不一致的,需要將其刪除。如果解決呢? Zab 通過巧妙的設計 zxid 來實現這一目的。一個 zxid 是64位,高 32 是紀元(epoch)編號,每經過一次 leader 選舉產生一個新的 leader,新 leader 會將 epoch 號 +1。低 32 位是訊息計數器,每接收到一條訊息這個值 +1,新 leader 選舉後這個值重置為 0。

這樣設計的好處是舊的 leader 掛了後重啟,它不會被選舉為 leader,因為此時它的 zxid 肯定小於當前的新 leader。當舊的 leader 作為 follower 接入新的 leader 後,新的 leader 會讓它將所有的擁有舊的 epoch 號的未被 COMMIT 的 proposal 清除。


3.2 兩階段提交協議

2PC(two-phase commit),即二階段提交,是分散式事務中一個很重要的協議,當一個事務跨越多個節點時,為了保持事務的ACID特性,需要引入一個coordinator,即協調者作為的元件來統一掌控所有節點(稱作參與者)的操作結果並最終指示這些節點是否要把操作結果進行真正的提交或回滾。

  兩階段提交是一個非常經典的強一致、中心化的原子提交協議。這裡所說的中心化是指協議中有兩類節點:一個是中心化協調者節點(coordinator)和N個參與者節點(partcipant)。兩個階段:第一階段:投票階段和第二階段:提交/執行階段舉例訂單服務A,需要呼叫支付服務B去支付,支付成功則處理購物訂單為待發貨狀態,否則就需要將購物訂單處理為失敗狀態。

1、第一階段:投票階段

第一階段主要分為3步

1)事務詢問

協調者向所有的參與者傳送事務預處理請求,稱之為Prepare,並開始等待各參與者的響應。

2)執行本地事務

各個參與者節點執行本地事務操作,但在執行完成後並不會真正提交資料庫本地事務,而是先向協調者報告說:“我這邊可以處理了/我這邊不能處理”。.

3)各參與者向協調者反饋事務詢問的響應

如果參與者成功執行了事務操作,那麼就反饋給協調者Yes響應,表示事務可以執行,如果沒有參與者成功執行事務,那麼就反饋給協調者No響應,表示事務不可以執行。

第一階段執行完後,會有兩種可能。1、所有都返回Yes. 2、有一個或者多個返回No。

2、第二階段:提交/執行階段(成功流程)

成功條件:所有參與者都返回Yes。

第二階段主要分為兩步

1)所有的參與者反饋給協調者的資訊都是Yes,那麼就會執行事務提交,協調者向所有參與者節點發出Commit請求.

2)事務提交參與者收到Commit請求之後,就會正式執行本地事務Commit操作,並在完成提交之後釋放整個事務執行期間佔用的事務資源。

3、第二階段:提交/執行階段(異常流程)

異常條件:任何一個參與者協調者反饋了No響應,或者等待超時之後,協調者尚未收到所有參與者的反饋響應。

異常流程第二階段也分為兩步

1)傳送回滾請求

協調者向所有參與者節點發出RoollBack請求.

2)事務回滾

參與者接收到RoollBack請求後,會回滾本地事務。

結論:不管最後結果如何,第二階段都會結束當前事務。

  建議:少使用分散式事務,在分散式事務這個問題上,還很少有成熟牛逼的產品,而且分散式事務過程中,涉及到了各個節點的通知,二次通知,當節點多的時候,協調者的壓力巨大,而且整個流程對業務的時間開銷是巨大的,所以建議謹慎使用分散式事務,即使二階段看似能處理好分散式節點的ACID問題,但是其本身也存在不小的問題。 

  1、同步阻塞問題。執行過程中,所有參與節點都是事務阻塞型的。當參與者佔有公共資源時,其他第三方節點訪問公共資源不得不處於阻塞狀態。

  2、單點故障。由於協調者的重要性,一旦協調者發生故障。參與者會一直阻塞下去。尤其在第二階段,協調者發生故障,那麼所有的參與者還都處於鎖定事務資源的狀態中,而無法繼續完成事務操作。(如果是協調者掛掉,可以重新選舉一個協調者,但是無法解決因為協調者宕機導致的參與者處於阻塞狀態的問題)

  3、資料不一致。在二階段提交的階段二中,當協調者向參與者傳送commit請求之後,發生了局部網路異常或者在傳送commit請求過程中協調者發生了故障,這回導致只有一部分參與者接受到了commit請求。而在這部分參與者接到commit請求之後就會執行commit操作。但是其他部分未接到commit請求的機器則無法執行事務提交。於是整個分散式系統便出現了資料部一致性的現象。

  4、二階段無法解決的問題:協調者再發出commit訊息之後宕機,而唯一接收到這條訊息的參與者同時也宕機了。那麼即使協調者通過選舉協議產生了新的協調者,這條事務的狀態也是不確定的,沒人知道事務是否被已經提交。

3.3 向量時鐘協議

先說一下需要用到向量時鐘的場景。我們在寫資料時候,經常希望資料不要儲存在單點。如db1,db2都可以同時提供寫服務,並且都存有全量資料。而client不管是寫哪一個db都不用擔心資料寫亂問題。但是現實場景中往往會碰到並行同時修改。導致db1和db2資料不一致。於是乎就有人想出一些解決策略。向量時鐘算是其中一種。簡單易懂。但是並沒有徹底解決衝突問題,現實分散式儲存補充了很多額外技巧。

這裡反向敘述方式,介紹向量時鐘。先舉實際例子讓讀者有個感性認識,然後再說演算法規則。

1、舉個例子

向量時鐘實際是一組版本號(版本號=邏輯時鐘),假設資料需要存放3份,需要3臺db儲存(用A,B,C表示),那麼向量維度就是3,每個db有一個版本號,從0開始,這樣就形成了一個向量版本[A:0, B:0, C:0];

Step 1: 初始狀態下,所有機器都是[A:0, B:0, C:0]

DB_A——> [A:0, B:0, C:0]

DB_B——> [A:0, B:0, C:0]

DB_C——> [A:0, B:0, C:0]

Step 2: 假設現在應用是一個商場,現在錄入一個iphone6 price 5888; 客戶端隨機選擇一個db機器寫入。現假設選擇了A,資料大概是這樣 :

{key=iphone_price; value=5888; vclk=[A:1,B:0,C:0]}

Step 3: 接下來A會把資料同步給BC;於是最終同步結果如下

DB_A——> {key=iphone_price; value=5888; vclk=[A:1,B:0,C:0]}

DB_B——> {key=iphone_price; value=6888; vclk=[A:1, B:0,C:0]}

DB_C——> {key=iphone_price; value=5888; vclk=[A:1,B:0,C:0]}

Step 4:過了分鐘,價格出現波動,升值到6888;於是某個業務員更新價格。這時候系統隨機選擇了B做為寫入儲存,於是結果看起來是這樣:

DB_A——> {key=iphone_price; value=5888; vclk=[A:1,B:0,C:0]}

DB_B——> {key=iphone_price; value=6888; vclk=[A:1,B:1,C:0]}

DB_C——> {key=iphone_price; value=5888; vclk=[A:1,B:0,C:0]}

Step 5:於是B就把更新同步給其他幾個儲存

DB_A——> {key=iphone_price; value=6888; vclk=[A:1,B:1,C:0]}

DB_B——> {key=iphone_price; value=6888; vclk=[A:1,B:1,C:0]}

DB_C——> {key=iphone_price; value=6888; vclk=[A:1,B:1,C:0]}

到目前為止都是正常同步,下面開始演示一下不正常的情況。

Step 6:價格再次發生波動,變成4000,這次選擇C寫入:

DB_A——> {key=iphone_price; value=6888; vclk=[A:1, B:1,C:0]}

DB_B——> {key=iphone_price; value=6888; vclk=[A:1,B:1,C:0]}

DB_C——> {key=iphone_price; value=4000; vclk=[A:1, B:1,C:1]}

Step 7: C把更新同步給AB,因為某些問題,只同步到A,結果如下:

DB_A——> {key=iphone_price; value=4000; vclk=[A:1, B:1,C:1]}

DB_B——> {key=iphone_price; value=6888; vclk=[A:1,B:1,C:0]}

DB_C——> {key=iphone_price; value=4000; vclk=[A:1, B:1,C:1]}

Step 8:價格再次波動,變成6000元,系統選擇B寫入

DB_A——> {key=iphone_price; value=6888; vclk=[A:1, B:1,C:1]}

DB_B——> {key=iphone_price; value=6000; vclk=[A:1,B:2, C:0]}

DB_C——> {key=iphone_price; value=4000; vclk=[A:1, B:1,C:1]}

Step 9: 當B同步更新給A和C時候就出現問題了,A自己的向量時鐘是[A:1, B:1,C:1], 而收到更新訊息攜帶過來的向量時鐘是[A:1,B:2, C:0], B:2 比B:1新,但是C:0卻比C1舊。這時候發生不一致衝突。不一致問題如何解決?向量時鐘策略並沒有給出解決版本,留給使用者自己去解決,只是告訴你目前資料存在衝突

2、規則介紹

版本號變更規則其實就2條,比較簡單

1、 每次修改資料,本節點的版本號 加1,例如上述step 8中 向B寫入,於是從B:1變成B:2,其他節點的版本號不發生變更。

2、 每次同步資料(這裡需要注意,同步和修改是不一樣的寫操作哦),會有三種情況:

a: 本節點的向量版本都要比訊息攜帶過來的向量版本低(小於或等於) 如本節點為[A:1, B:2,C:3]}, 訊息攜帶過來為[A:1, B:2,C:4]或[A:2, B:3,C:4]等。 這時候合併規則取每個分量的最大值。

b: 本節點的向量版本都要比比訊息攜帶過來的向量版本高,這時候可以認為本地資料比同步過來的資料要新,直接丟棄要同步的版本。

c: 出現衝突,如上述step 9中,有的分量版本大,有的分量版本小,無法判斷出來到底誰是最新版本。就要進行衝突仲裁。

3、衝突解決

其實沒有一個比較好的解決衝突的版本:就筆者目前所瞭解,加上時間戳算是一個策略。具體方法是再加一個維度資訊:資料更新的時間戳(timestamp)。[A:1, B:2,C:4,ts:123434354] ,如果發生衝突,再比較一下兩個資料的ts,大的數值說明比較後更新,選擇它作為最終資料。並對向量時鐘進行訂正。

3.4 Raft協議

在說Raft協議之前先說一下態機複製(State Machine Replication),狀態機複製的理論基礎是:如果叢集裡的每一個節點上都執行著相同的確定性狀態機S,並且所有的狀態機剛開始都處於同樣的初始狀態s0,那麼給予這些狀態機相同的輸入序列:{i1, i2, i3, i4, i5, i6, …, in}, 這些狀態機必然會經過相同的狀態轉換路徑:s0->s1->s2->s3->…->sn最終達到相同的狀態sn, 同時生成相同的輸出序列{o1(s1), o2(s2), o3(s3), …, on(sn)}

狀態機複製在實際應用中的一個例子就是MySQL叢集。我們知道,MySQL叢集中的master會把所有的操作記錄到binlog中,這裡的操作就是輸入序列I, 然後slave會把master上的binlog複製到自己的relaylog中,然後把把relaylog裡的操作回放一遍(相當於執行了一遍輸入序列I)。所以,如果master和slave裡的狀態機是完全相同的,並且在執行序列I之前都處於相同的狀態下,那麼執行完序列I後,它們的狀態依舊是相同的(一致性)。
在執行輸入序列I的過程中,根據同步方式的不同,系統就有了強一致性和最終一致性。如果我們要求對於序列I中的每一個in, 都需要所有的服務副本確認成功執行了in,才能執行in+1,那麼這個系統就是強一致性的系統。如果我們取消掉這個限制,僅僅要求所有的服務副本執行相同的輸入序列I,但是完全各自獨立執行,而不需要在中間同步,那麼就有了最終一致性(各服務都會達到相同的最終狀態,但是達到的時間不確定)。

參考https://blog.csdn.net/weixin_43778179/article/details/90612726