1. 程式人生 > 程式設計 >神一樣的CAP理論被應用在何方

神一樣的CAP理論被應用在何方

對於開發或設計分散式系統的架構師工程師來說,CAP是必須要掌握的理論。

(but:這個文章的重點並不是討論CAP理論和細節,重點是說說CAP在微服務中的開發怎麼起到一個指引作用,會通過幾個微服務開發的例子說說明,儘量的去貼近開發)

CAP定理又被成為布魯爾定理,是加州大學電腦科學家埃裡克·布魯爾提出來的猜想,後來被證明成為分散式計算領域公認的定理。不過布魯爾在出來CAP的時候並沒有對CAP三者(Consistency,Availability,Partition tolerance)進行詳細的定義,所以在網上也出現了不少對CAP不同解讀的聲音。

CAP 定理

CAP定理在發展中存在過兩個版本,我們以第二個版本為準

在一個分散式系統中(指互相連線並共享資料的節點集合)中,當涉及到讀寫操作時,只能保證一致性(Consistence)、可用性(Availability)、分割槽容錯性(Partition Tolerance)三者中的兩個,另外一個必須被犧牲。

image

這個版本的CAP理論在探討分散式系統,更加強調兩點是互聯和共享資料,其實也是理清楚了第一個版本中三選二的一些缺陷,分散式系統不一定都存在互聯和共享資料,例如memcached叢集相互間就沒有存在連線和共享資料,所以memcached叢集這類的分散式系統並不在CAP理論討論的範圍,而想Mysql叢集就是互聯和資料共享複製,因此mysql叢集式屬於CAP理論討論的物件。

一致性(Consistency)

一致性意思就是寫操作之後進行讀操作無論在哪個節點都需要返回寫操作的值

可用性(Availability)

非故障的節點在合理的時間內返回合理的響應

分割槽容錯性(Partition Tolerance)

當網路出現分割槽後,系統依然能夠繼續旅行社職責

在分散式的環境下,網路無法做到100%可靠,有可能出現故障,因此分割槽是一個必須的選項,如果選擇了CA而放棄了P,若發生分割槽現象,為了保證C,系統需要禁止寫入,此時就與A發生衝突,如果是為了保證A,則會出現正常的分割槽可以寫入資料,有故障的分割槽不能寫入資料,則與C就衝突了。因此分散式系統理論上不可能選擇CA架構,而必須選擇CP或AP架構。

分散式事務BASE理論

BASE理論是對CAP的延伸和補充,是對CAP中的AP方案的一個補充,即使在選擇AP方案的情況下,如何更好的最終達到C。

BASE是基本可用,柔性狀態,最終一致性三個短語的縮寫,核心的思想是即使無法做到強一致性,但應用可以採用適合的方式達到最終一致性。

CAP在服務中實際的應用例子

image

理解貌似講多了,專案的CAP可以參考下李運華的《從零開始學架構》的書,裡面的21,22章比較詳細的描繪了CAP的理論細節和CAP的版本演化過程。

這裡著重的講解的是神一樣的CAP在我們的微服務中怎麼去指導和應用起來,大概會舉幾個平時常見的例子

image

服務註冊中心,是選擇AP還是選擇CP ?

服務註冊中心解決的問題

在討論CAP之前先明確下服務註冊中心主要是解決什麼問題:一個是服務註冊,一個是服務發現。

  • 服務註冊:例項將自身服務資訊註冊到註冊中心,這部分資訊包括服務的主機IP和服務的Port,以及暴露服務自身狀態和訪問協議資訊等。

  • 服務發現:例項請求註冊中心所依賴的服務資訊,服務例項通過註冊中心,獲取到註冊到其中的服務例項的資訊,通過這些資訊去請求它們提供的服務。

image

目前作為註冊中心的一些元件大致有:dubbo的zookeeper,springcloud的eureka,consul,rocketMq的nameServer,hdfs的nameNode。目前微服務主流是dubbo和springcloud,使用最多是zookeeper和eureka,我們就來看看應該根據CAP理論應該怎麼去選擇註冊中心。(springcloud也可以用zk,不過不是主流不討論)。

zookeeper選擇CP

zookeep保證CP,即任何時刻對zookeeper的訪問請求能得到一致性的資料結果,同時系統對網路分割具備容錯性,但是它不能保證每次服務的可用性。從實際情況來分析,在使用zookeeper獲取服務列表時,如果zk正在選舉或者zk叢集中半數以上的機器不可用,那麼將無法獲取資料。所以說,zk不能保證服務可用性。

eureka選擇AP

eureka保證AP,eureka在設計時優先保證可用性,每一個節點都是平等的,一部分節點掛掉不會影響到正常節點的工作,不會出現類似zk的選舉leader的過程,客戶端發現向某個節點註冊或連線失敗,會自動切換到其他的節點,只要有一臺eureka存在,就可以保證整個服務處在可用狀態,只不過有可能這個服務上的資訊並不是最新的資訊。

zookeeper和eureka的資料一致性問題

先要明確一點,eureka的建立初心就是為一個註冊中心,但是zk更多是作為分散式協調服務的存在,只不過因為它的特性被dubbo賦予了註冊中心,它的職責更多是保證資料(配置資料,狀態資料)在管轄下的所有服務之間保持一致,所有這個就不難理解為何zk被設計成CP而不是AP,zk最核心的演演算法ZAB,就是為瞭解決分散式系統下資料在多個服務之間一致同步的問題。

更深層的原因,zookeeper是按照CP原則構建,也就是說它必須保持每一個節點的資料都保持一致,如果zookeeper下節點斷開或者叢集中出現網路分割(例如交換機的子網間不能互訪),那麼zk會將它們從自己的管理範圍中剔除,外界不能訪問這些節點,即使這些節點是健康的可以提供正常的服務,所以導致這些節點請求都會丟失。

而eureka則完全沒有這方面的顧慮,它的節點都是相對獨立,不需要考慮資料一致性的問題,這個應該是eureka的誕生就是為了註冊中心而設計,相對zk來說剔除了leader節點選取和事務日誌極致,這樣更有利於維護和保證eureka在執行的健壯性。

image

再來看看,資料不一致性在註冊服務中中會給eureka帶來什麼問題,無非就是某一個節點被註冊的服務多,某個節點註冊的服務少,在某一個瞬間可能導致某些ip節點被呼叫數少,某些ip節點呼叫數少的問題。也有可能存在一些本應該被刪除而沒被刪除的髒資料。

image

小結:服務註冊應該選擇AP還是CP

對於服務註冊來說,針對同一個服務,即使註冊中心的不同節點儲存的服務註冊資訊不相同,也並不會造成災難性的後果,對於服務消費者來說,能消費才是最重要的,就算拿到的資料不是最新的資料,消費者本身也可以進行嘗試失敗重試。總比為了追求資料的一致性而獲取不到例項資訊整個服務不可用要好。

所以,對於服務註冊來說,可用性比資料一致性更加的重要,選擇AP。

分散式鎖,是選擇AP還是選擇CP ?

這裡實現分散式鎖的方式選取了三種:

  • 基於資料庫實現分散式鎖
  • 基於redis實現分散式鎖
  • 基於zookeeper實現分散式鎖

基於資料庫實現分散式鎖

構建表結構

image

利用表的 UNIQUE KEY idx_lock (method_lock) 作為唯一主鍵,當進行上鎖時進行insert動作,資料庫成功錄入則以為上鎖成功,當資料庫報出 Duplicate entry 則表示無法獲取該鎖。

image

不過這種方式對於單主卻無法自動切換主從的mysql來說,基本就無法現實P分割槽容錯性,(Mysql自動主從切換在目前並沒有十分完美的解決方案)。可以說這種方式強依賴於資料庫的可用性,資料庫寫操作是一個單點,一旦資料庫掛掉,就導致鎖的不可用。這種方式基本不在CAP的一個討論範圍。

基於redis實現分散式鎖

redis單執行緒序列處理天然就是解決序列化問題,用來解決分散式鎖是再適合不過。

實現方式:

setnx key value Expire_time
獲取到鎖 返回 1 , 獲取失敗 返回 0
複製程式碼

為瞭解決資料庫鎖的無主從切換的問題,可以選擇redis叢集,或者是 sentinel 哨兵模式,實現主從故障轉移,當master節點出現故障,哨兵會從slave中選取節點,重新變成新的master節點。

image

哨兵模式故障轉移是由sentinel叢集進行監控判斷,當maser出現異常即複製中止,重新推選新slave成為master,sentinel在重新進行選舉並不在意主從資料是否複製完畢具備一致性。

所以redis的複製模式是屬於AP的模式。保證可用性,在主從複製中“主”有資料,但是可能“從”還沒有資料,這個時候,一旦主掛掉或者網路抖動等各種原因,可能會切換到“從”節點,這個時候可能會導致兩個業務縣城同時獲取得兩把鎖

image

這個過程如下:

  1. 業務執行緒-1 向主節點請求鎖
  2. 業務執行緒-1 獲取鎖
  3. 業務執行緒-1 獲取到鎖並開始執行業務
  4. 這個時候redis剛生成的鎖在主從之間還未進行同步
  5. redis這時候主節點掛掉了
  6. redis的從節點升級為主節點
  7. 業務執行緒-2 想新的主節點請求鎖
  8. 業務執行緒-2 獲取到新的主節點返回的鎖
  9. 業務執行緒-2 獲取到鎖開始執行業務
  10. 這個時候 業務執行緒-1 和 業務執行緒-2 同時在執行任務

上述的問題其實並不是redis的缺陷,只是redis採用了AP模型,它本身無法確保我們對一致性的要求。redis官方推薦redlock演演算法來保證,問題是redlock至少需要三個redis主從例項來實現,維護成本比較高,相當於redlock使用三個redis叢集實現了自己的另一套一致性演演算法,比較繁瑣,在業界也使用得比較少。

能否使用redis作為分散式鎖?

能不能使用redis作為分散式鎖,這個本身就不是redis的問題,還是取決於業務場景,我們先要自己確認我們的場景是適合 AP 還是 CP , 如果在社交發帖等場景下,我們並沒有非常強的事務一致性問題,redis提供給我們高效能的AP模型是非常適合的,但如果是交易型別,對資料一致性非常敏感的場景,我們可能要尋在一種更加適合的 CP 模型

基於zookeeper實現分散式鎖

剛剛也分析過,redis其實無法確保資料的一致性,先來看zookeeper是否合適作為我們需要的分散式鎖,首先zk的模式是CP模型,也就是說,當zk鎖提供給我們進行訪問的時候,在zk叢集中能確保這把鎖在zk的每一個節點都存在。

image

(這個實際上是zk的leader通過二階段提交寫請求來保證的,這個也是zk的叢集規模大了的一個瓶頸點)

zk鎖實現的原理

說zk的鎖問題之前先看看zookeeper中幾個特性,這幾個特性構建了zk的一把分散式鎖

特性:

  • 有序節點

當在一個父目錄下如 /lock 下建立 有序節點,節點會按照嚴格的先後順序創建出自節點 lock000001,lock000002,lock0000003,以此類推,有序節點能嚴格保證各個自節點按照排序命名生成。

  • 臨時節點

客戶端建立了一個臨時節點,在客戶端的會話結束或會話超時,zookepper會自動刪除該解ID那。

  • 事件監聽

在讀取資料時,我們可以對節點設定監聽,當節點的資料發生變化(1 節點建立 2 節點刪除 3 節點資料變成 4 自節點變成)時,zookeeper會通知客戶端。

結合這幾個特點,來看下zk是怎麼組合分散式鎖。

image

  1. 業務執行緒-1 業務執行緒-2 分別向zk的/lock目錄下,申請建立有序的臨時節點
  2. 業務執行緒-1 搶到/lock0001 的檔案,也就是在整個目錄下最小序的節點,也就是執行緒-1獲取到了鎖
  3. 業務執行緒-2 只能搶到/lock0002的檔案,並不是最小序的節點,執行緒2未能獲取鎖
  4. 業務執行緒-1 與 lock0001 建立了連線,並維持了心跳,維持的心跳也就是這把鎖的租期
  5. 當業務執行緒-1 完成了業務,將釋放掉與zk的連線,也就是釋放了這把鎖
zk分散式鎖的程式碼實現

zk官方提供的客戶端並不支援分散式鎖的直接實現,我們需要自己寫程式碼去利用zk的這幾個特性去進行實現。

image

小結:究竟該用CP還是AP的分散式鎖

首先得了解清楚我們使用分散式鎖的場景,為何使用分散式鎖,用它來幫我們解決什麼問題,先聊場景後聊分散式鎖的技術選型。

無論是redis,zk,例如redis的AP模型會限制很多使用場景,但它卻擁有了幾者中最高的效能,zookeeper的分散式鎖要比redis可靠很多,但他繁瑣的實現機制導致了它的效能不如redis,而且zk會隨著叢集的擴大而效能更加下降。

簡單來說,先了解業務場景,後進行技術選型。

分散式事務,是怎麼從ACID解脫,投身CAP/BASE

如果說到事務,ACID是傳統資料庫常用的設計理念,追求強一致性模型,關係資料庫的ACID模型擁有高一致性+可用性,所以很難進行分割槽,所以在微服務中ACID已經是無法支援,我們還是回到CAP去尋求解決方案,不過根據上面的討論,CAP定理中,要麼只能CP,要麼只能AP,如果我們追求資料的一致性而忽略可用性這個在微服務中肯定是行不通的,如果我們追求可用性而忽略一致性,那麼在一些重要的資料(例如支付,金額)肯定出現漏洞百出,這個也是無法接受。所以我們既要一致性,也要可用性。

image

都要是無法實現的,但我們能不能在一致性上作出一些妥協,不追求強一致性,轉而追求最終一致性,所以引入BASE理論,在分散式事務中,BASE最重要是為CAP提出了最終一致性的解決方案,BASE強調犧牲高一致性,從而獲取肯用性,資料允許在一段時間內不一致,只要保證最終一致性就可以了。

實現最終一致性

弱一致性:系統不能保證後續訪問返回更新的值。需要在一些條件滿足之後,更新的值才能返回。從更新操作開始,到系統保證任何觀察者總是看到更新的值的這期間被稱為不一致視窗。

最終一致性:這是弱一致性的特殊形式;儲存系統保證如果沒有對某個物件的新更新操作,最終所有的訪問將返回這個物件的最後更新的值。

BASE模型

BASE模型是傳統ACID模型的反面,不同與ACID,BASE強調犧牲高一致性,從而獲得可用性,資料允許在一段時間內的不一致,只要保證最終一致就可以了。

BASE模型反ACID模型,完全不同ACID模型,犧牲高一致性,獲得可用性或可靠性: Basically Available基本可用。支援分割槽失敗(e.g. sharding碎片劃分資料庫) Soft state軟狀態 狀態可以有一段時間不同步,非同步。 Eventually consistent最終一致,最終資料是一致的就可以了,而不是時時一致。

分散式事務

在分散式系統中,要實現分散式事務,無外乎幾種解決方案。方案各有不同,不過其實都是遵循BASE理論,是最終一致性模型。

  • 兩階段提交(2PC)
  • 補償事務(TCC)
  • 本地訊息表
  • MQ事務訊息

兩階段提交(2PC)

其實還有一個資料庫的XA事務,不過目前在真正的網際網路中實際的應用基本很少,兩階段提交就是使用XA原理。

image

在 XA 協議中分為兩階段:

  1. 事務管理器要求每個涉及到事務的資料庫預提交(precommit)此操作,並反映是否可以提交。
  2. 事務協調器要求每個資料庫提交資料,或者回滾資料。

說一下,為何在網際網路的系統中沒被改造過的兩階段提交基本很少被業界應用,最最大的缺點就是同步阻塞問題,在資源準備就緒之後,資源管理器中的資源就一直處於阻塞,直到提交完成之後,才進行資源釋放。這個在網際網路高併發大資料的今天,兩階段的提交是不能滿足現在網際網路的發展。

還有就是兩階段提交協議雖然為分散式資料強一致性所設計,但仍然存在資料不一致性的可能,例如:

比如在第二階段中,假設協調者發出了事務 Commit 的通知,但是因為網路問題該通知僅被一部分參與者所收到並執行了 Commit 操作,其餘的參與者則因為沒有收到通知一直處於阻塞狀態,這時候就產生了資料的不一致性。

補償事務(TCC)

TCC是服務化的兩階段變成模型,每個業務服務都必須實現 try,confirm,calcel三個方法,這三個方式可以對應到SQL事務中Lock,Commit,Rollback。

image

相比兩階段提交,TCC解決了幾個問題

同步阻塞,引入了超時機制,超時後進行補償,並不會像兩階段提交鎖定了整個資源,將資源轉換為業務邏輯形式,粒度變小。 因為有了補償機制,可以由業務活動管理器進行控制,保證資料一致性。

1). try階段

try只是一個初步的操作,進行初步的確認,它的主要職責是完成所有業務的檢查,預留業務資源

2). confirm階段

confirm是在try階段檢查執行完畢後,繼續執行的確認操作,必須滿足冪等性操作,如果confirm中執行失敗,會有事務協調器觸發不斷的執行,直到滿足為止

3). cancel是取消執行,在try沒通過並釋放掉try階段預留的資源,也必須滿足冪等性,跟confirm一樣有可能被不斷執行

一個下訂單,生成訂單扣庫存的例子:

image

接下來看看,我們的下單扣減庫存的流程怎麼加入TCC

image

在try的時候,會讓庫存服務預留n個庫存給這個訂單使用,讓訂單服務產生一個“未確認”訂單,同時產生這兩個預留的資源, 在confirm的時候,會使用在try預留的資源,在TCC事務機制中認為,如果在try階段能正常預留的資源,那麼在confirm一定能完整的提交

image

在try的時候,有任務一方為執行失敗,則會執行cancel的介面操作,將在try階段預留的資源進行釋放。

這個並不是重點要論tcc事務是怎麼實現,重點還是討論分散式事務在CAP+BASE理論的應用。實現可以參考:github.com/changmingxi…

本地訊息表

本地訊息表這個方案最初是 eBay 提出的,eBay 的完整方案 queue.acm.org/detail.cfm?…

本地訊息表這種實現方式應該是業界使用最多的,其核心思想是將分散式事務拆分成本地事務進行處理。

image

對於本地訊息佇列來說,核心就是將大事務轉變為小事務,還是用上面下訂單扣庫存的例子說說明

  1. 當我們去建立訂單的時候,我們新增一個本地訊息表,把建立訂單和扣減庫存寫入到本地訊息表,放在同一個事務(依靠資料庫本地事務保證一致性)
  2. 配置一個定時任務去輪訓這個本地事務表,掃描這個本地事務表,把沒有傳送出去的訊息,傳送給庫存服務,當庫存服務收到訊息後,會進行減庫存,並寫入伺服器的事務表,更新事務表的狀態。
  3. 庫存伺服器通過定時任務或直接通知訂單服務,訂單服務在本地訊息表更新狀態。

這裡須注意的是,對於一些掃描傳送未成功的任務,會進行重新傳送,所以必須保證介面的冪等性。

本地訊息佇列是BASE理論,是最終一致性模型,適用對一致性要求不高的情況。

MQ事務

RocketMq在4.3版本已經正式宣佈支援分散式事務,在選擇Rokcetmq做分散式事務請務必選擇4.3以上的版本。

RocketMQ中實現了分散式事務,實際上是對本地訊息表的一個封裝,將本地訊息表移動到了MQ內部。

image

事務訊息作為一種非同步確保型事務, 將兩個事務分支通過 MQ 進行非同步解耦,RocketMQ 事務訊息的設計流程同樣借鑑了兩階段提交理論,整體互動流程如下圖所示:

image

MQ事務是對本地訊息表的一層封裝,將本地訊息表移動到了MQ內部,所以也是基於BASE理論,是最終一致性模式,對強一致性要求不那麼高的事務適用,同時MQ事務將整個流程非同步化了,也非常適合在高併發情況下使用。

RocketMQ選擇非同步/同步刷盤,非同步/同步複製,背後的CP和AP思考

雖然同步刷盤/非同步刷盤,同步/非同步複製,並沒有對cAP直接的應用,但在配置的過程中也一樣涉及到可用性和一致性的考慮

同步刷盤/非同步刷盤

RocketMQ的訊息是可以做到持久化的,資料會持久化到磁碟,RocketMQ為了提高效能,儘可能保證磁碟的順序寫入,訊息在Producer寫入RocketMq的時候,有兩種寫入磁碟方式:

  1. 非同步刷盤: 訊息快速寫入到記憶體的pagecache,就立馬返回寫成功狀態,當記憶體的訊息累計到一定程度的時候,會觸發統一的寫磁碟操作。這種方式可以保證大吞吐量,但也存在著訊息可能未存入磁碟丟失的風險。
  2. 同步刷盤: 訊息快速寫入記憶體的pagecahe,立刻通知刷盤執行緒進行刷盤,等待刷盤完成之後,喚醒等待的執行緒,返回訊息寫成功的狀態。

image

同步複製/非同步複製

一個broker組有Master和Slave,訊息需要從Master複製到Slave上,所以有同步和非同步兩種複製方式。

  1. 同步複製: 是等Master和Slave均寫成功後才反饋給客戶端寫成功狀態。
  2. 非同步複製: 是隻要Master寫成功即可反饋給客戶端寫成功狀態。

image

非同步複製的優點是可以提高響應速度,但犧牲了一致性 ,一般實現該類協議的演演算法需要增加額外的補償機制。同步複製的優點是可以保證一致性(一般通過兩階段提交協議),但是開銷較大,可用性不好(參見CAP定理),帶來了更多的衝突和死鎖等問題。值得一提的是Lazy+Primary/Copy的複製協議在實際生產環境中是非常實用的。

image

RocketMQ的設定要結合業務場景,合理設定刷盤方式和主從複製方式,尤其是SYNC_FLUSH方式,由於頻繁的觸發寫磁碟動作,會明顯降低效能。通常情況下,應該把Master和Slave設定成ASYNC_FLUSH的刷盤方式,主從之間配置成SYNC_MASTER的複製方式,這樣即使有一臺機器出故障,仍然可以保證資料不丟。

總結

在微服務的構建中,永遠都逃離不了CAP理論,因為網路永遠不穩定,硬體總會老化,軟體會可能出現bug,所以分割槽容錯性在微服務中是躲不過的命題,可以這麼說,只要是分散式,只要是叢集都面臨著AP或者CP的選擇,但你很貪心的時候,既要一致性又要可用性,那隻能對一致性作出一點妥協,也就是引入了BASE理論,在業務允許的情況下實現最終一致性。

究竟是選AP還是選CP,真的在於對業務的瞭解,例如金錢,庫存相關會優先考慮CP模型,例如社群發帖相關可以優先選擇AP模型,這個說白了其實基於對業務的瞭解是一個選擇和妥協的過程。

image