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

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

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

CAP定理又被成爲布魯爾定理,是加州大學計算機科學家埃裏克·布魯爾提出來的猜測,後來被證實成爲分佈式計算領域公認的定理。不過布魯爾在出來CAP的時候並無對CAP三者(Consistency,Availability,Partition tolerance)進行詳細的定義,因此在網上也出現了很多對CAP不一樣解讀的聲音。github

CAP 定理

CAP定理在發展中存在過兩個版本,咱們以第二個版本爲準redis

在一個分佈式系統中(指互相鏈接並共享數據的節點集合)中,當涉及到讀寫操做時,只能保證一致性(Consistence)、可用性(Availability)、分區容錯性(Partition Tolerance)三者中的兩個,另一個必須被犧牲。算法

image

這個版本的CAP理論在探討分佈式系統,更增強調兩點是互聯和共享數據,其實也是理清楚了第一個版本中三選二的一些缺陷,分佈式系統不必定都存在互聯和共享數據,例如memcached集羣相互間就沒有存在鏈接和共享數據,因此memcached集羣這類的分佈式系統並不在CAP理論討論的範圍,而想Mysql集羣就是互聯和數據共享複製,所以mysql集羣式屬於CAP理論討論的對象。spring

一致性(Consistency)

一致性意思就是寫操做以後進行讀操做不管在哪一個節點都須要返回寫操做的值sql

可用性(Availability)

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

分區容錯性(Partition Tolerance)

當網絡出現分區後,系統依然可以繼續旅行社職責bash

在分佈式的環境下,網絡沒法作到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
相關文章
相關標籤/搜索