一致性模型及一致性協議


一、一致性模型概念

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

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 .會話一致性:提交更新操作的用戶在同一個會話里讀取該數據時能夠保證數據是最新,

二、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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM