神一樣的CAP理論如何應用到分佈式中去

突然在刷博客的時候看到這篇文章,可以說幫助實在是巨大的,但是其原文訪問量並不高,頗有一種這麼好的文章被埋沒的感覺,遂轉載希望能夠讓更多的人能看到這篇文章,原文在這裏SpringCloud(6)之CAP理論在微服務的運用

引言

對於開發或設計分佈式系統的架構師工程師來說,CAP 是必須要掌握的理論,這個文章的重點並不是討論 CAP 理論和細節,重點是說說 CAP 在微服務中的開發怎麼起到一個指引作用,會通過幾個微服務開發的例子說明,儘量的去貼近開發。

CAP 定理又被稱爲布魯爾定理,是加州大學計算機科學家埃裏克·布魯爾提出來的猜想,後來被證明成爲分佈式計算領域公認的定理。

不過布魯爾在出來 CAP 的時候並沒有對 CAP 三者(Consistency,Availability,Partition tolerance)進行詳細的定義,所以在網上也出現了不少對 CAP 不同解讀的聲音。

一、CAP定理

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

在這裏插入圖片描述

這個版本的 CAP 理論在探討分佈式系統,更加強調兩點是互聯和共享數據,其實也是理清楚了第一個版本中三選二的一些缺陷。

分佈式系統不一定都存在互聯和共享數據,例如 Memcached 集羣相互間就沒有存在連接和共享數據。

所以 Memcached 集羣這類的分佈式系統並不在 CAP 理論討論的範圍,而像 MySQL 集羣就是互聯和數據共享複製,因此 MySQL 集羣是屬於 CAP 理論討論的對象。

1.1一致性(Consistency)

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

1.2 可用性(Availability)

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

1.3分區容錯性(Partition Tolerance)

當網絡出現分區後,系統依然能夠繼續履行職責。

在分佈式的環境下,網絡無法做到 100% 可靠,有可能出現故障,因此分區是一個必須的選項。

如果選擇了 CA 而放棄了 P,若發生分區現象,爲了保證 C,系統需要禁止寫入,此時就與 A 發生衝突;如果是爲了保證 A,則會出現正常的分區可以寫入數據,有故障的分區不能寫入數據,則與 C 就衝突了。

因此分佈式系統理論上不可能選擇 CA 架構,而必須選擇 CP 或 AP 架構。

1.4分佈式事務 BASE 理論

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

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

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

這裏着重講解的是神一樣的 CAP 在我們的微服務中怎麼去指導和應用起來,大概會舉幾個平時常見的例子。
在這裏插入圖片描述

2.1服務註冊中心解決的問題

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

  • 服務註冊:實例將自身服務信息註冊到註冊中心,這部分信息包括服務的主機 IP 和服務的 Port,以及暴露服務自身狀態和訪問協議信息等。
  • 服務發現:實例請求註冊中心所依賴的服務信息,服務實例通過註冊中心,獲取到註冊到其中的服務實例的信息,通過這些信息去請求它們提供的服務。

在這裏插入圖片描述

目前作爲註冊中心的一些組件大致有:

  • Dubbo 的 Zookeeper
  • Spring Cloud 的 Eureka,Consul
  • RocketMQ 的 nameServer
  • HDFS 的 nameNode

目前微服務主流是 Dubbo 和 Spring Cloud,使用最多是 Zookeeper 和 Eureka,我們就來看看應該根據 CAP 理論怎麼去選擇註冊中心。(Spring Cloud 也可以用 ZK,不過不是主流不討論)。

2.1.1Zookeeper 選擇 CP

Zookeeper 保證 CP,即任何時刻對 Zookeeper 的訪問請求能得到一致性的數據結果,同時系統對網絡分割具備容錯性,但是它不能保證每次服務的可用性。

從實際情況來分析,在使用 Zookeeper 獲取服務列表時,如果 ZK 正在選舉或者 ZK 集羣中半數以上的機器不可用,那麼將無法獲取數據。所以說,ZK 不能保證服務可用性。

2.1.2Eureka 選擇 AP

Eureka 保證 AP,Eureka 在設計時優先保證可用性,每一個節點都是平等的。

一部分節點掛掉不會影響到正常節點的工作,不會出現類似 ZK 的選舉 Leader 的過程,客戶端發現向某個節點註冊或連接失敗,會自動切換到其他的節點。

只要有一臺 Eureka 存在,就可以保證整個服務處在可用狀態,只不過有可能這個服務上的信息並不是最新的信息。

2.1.3ZK 和 Eureka 的數據一致性問題

先要明確一點,Eureka 的創建初心就是爲一個註冊中心,但是 ZK 更多是作爲分佈式協調服務的存在。

只不過因爲它的特性被 Dubbo 賦予了註冊中心,它的職責更多是保證數據(配置數據,狀態數據)在管轄下的所有服務之間保持一致。

所以這個就不難理解爲何 ZK 被設計成 CP 而不是 AP,ZK 最核心的算法 ZAB,就是爲了解決分佈式系統下數據在多個服務之間一致同步的問題。

更深層的原因,ZK 是按照 CP 原則構建,也就是說它必須保持每一個節點的數據都保持一致。

如果 ZK 下節點斷開或者集羣中出現網絡分割(例如交換機的子網間不能互訪),那麼 ZK 會將它們從自己的管理範圍中剔除,外界不能訪問這些節點,即使這些節點是健康的可以提供正常的服務,所以導致這些節點請求都會丟失。

而 Eureka 則完全沒有這方面的顧慮,它的節點都是相對獨立,不需要考慮數據一致性的問題,這個應該是 Eureka 的誕生就是爲了註冊中心而設計。

相對 ZK 來說剔除了 Leader 節點選取和事務日誌機制,這樣更有利於維護和保證 Eureka 在運行的健壯性。

在這裏插入圖片描述

再來看看,數據不一致性在註冊服務中會給 Eureka 帶來什麼問題,無非就是某一個節點被註冊的服務多,某個節點註冊的服務少,在某一個瞬間可能導致某些 IP 節點被調用數多,某些 IP 節點調用數少的問題。

也有可能存在一些本應該被刪除而沒被刪除的髒數據。
在這裏插入圖片描述

2.1.4服務註冊應該選擇 AP 還是 CP

對於服務註冊來說,針對同一個服務,即使註冊中心的不同節點保存的服務註冊信息不相同,也並不會造成災難性的後果。

對於服務消費者來說,能消費纔是最重要的,就算拿到的數據不是最新的數據,消費者本身也可以進行嘗試失敗重試。總比爲了追求數據的一致性而獲取不到實例信息整個服務不可用要好。

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

2.2分佈式鎖,是選擇 CA 還是選擇 CP?

對於分佈式鎖我在上一篇博客中也已經介紹到了,這裏實現分佈式鎖的方式選取了三種:

  • 基於數據庫實現分佈式鎖
  • 基於 Redis 實現分佈式鎖
  • 基於 Zookeeper 實現分佈式鎖

2.2.1基於數據庫實現分佈式鎖

構建表結構:

在這裏插入圖片描述

利用表的 UNIQUE KEY idx_lock(method_lock)作爲唯一主鍵,當進行上鎖時進行 Insert 動作,數據庫成功錄入則以爲上鎖成功,當數據庫報出 Duplicate entry 則表示無法獲取該鎖。
在這裏插入圖片描述

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

2.2.2基於 Redis 實現分佈式鎖

Redis 單線程串行處理天然就是解決串行化問題,用來解決分佈式鎖是再適合不過。

實現方式:

在這裏插入圖片描述

爲了解決數據庫鎖的無主從切換的問題,可以選擇 Redis 集羣,或者是 Sentinel 哨兵模式,實現主從故障轉移,當 Master 節點出現故障,哨兵會從 Slave 中選取節點,重新變成新的 Master 節點。

在這裏插入圖片描述

哨兵模式故障轉移是由 Sentinel 集羣進行監控判斷,當 Maser 出現異常即複製中止,重新推選新 Slave 成爲 Master,Sentinel 在重新進行選舉並不在意主從數據是否複製完畢具備一致性。

所以 Redis 的複製模式是屬於 AP 的模式。保證可用性,在主從複製中“主”有數據,但是可能“從”還沒有數據。

這個時候,一旦主掛掉或者網絡抖動等各種原因,可能會切換到“從”節點,這個時候可能會導致兩個業務線程同時獲取得兩把鎖。

在這裏插入圖片描述

這個過程如下:

  • 業務線程 -1 向主節點請求鎖
  • 業務線程 -1 獲取鎖
  • 業務線程 -1 獲取到鎖並開始執行業務
  • 這個時候 Redis 剛生成的鎖在主從之間還未進行同步
  • Redis 這時候主節點掛掉了
  • Redis 的從節點升級爲主節點
  • 業務線程 -2 想新的主節點請求鎖
  • 業務線程 -2 獲取到新的主節點返回的鎖
  • 業務線程 -2 獲取到鎖開始執行業務
  • 這個時候業務線程 -1 和業務線程 -2 同時在執行任務

上述的問題其實並不是 Redis 的缺陷,只是 Redis 採用了 AP 模型,它本身無法確保我們對一致性的要求。

Redis 官方推薦 Redlock 算法來保證,問題是 Redlock 至少需要三個 Redis 主從實例來實現,維護成本比較高。

相當於 Redlock 使用三個 Redis 集羣實現了自己的另一套一致性算法,比較繁瑣,在業界也使用得比較少。

能不能使用 Redis 作爲分佈式鎖?這個本身就不是 Redis 的問題,還是取決於業務場景。我們先要自己確認我們的場景是適合 AP 還是 CP , 如果在社交發帖等場景下,我們並沒有非常強的事務一致性問題,Redis 提供給我們高性能的 AP 模型是非常適合的。

但如果是交易類型,對數據一致性非常敏感的場景,我們可能要尋找一種更加適合的 CP 模型。

2.2.3基於 Zookeeper 實現分佈式鎖

剛剛也分析過,Redis 其實無法確保數據的一致性,先來看 Zookeeper 是否適合作爲我們需要的分佈式鎖。

首先 ZK 的模式是 CP 模型,也就是說,當 ZK 鎖提供給我們進行訪問的時候,在 ZK 集羣中能確保這把鎖在 ZK 的每一個節點都存在。
在這裏插入圖片描述

這個實際上是 ZK 的 Leader 通過二階段提交寫請求來保證的,這個也是 ZK 的集羣規模大了的一個瓶頸點。

①ZK 鎖實現的原理

說 ZK 的鎖問題之前先看看 Zookeeper 中幾個特性,這幾個特性構建了 ZK 的一把分佈式鎖。

ZK 的特性如下:

  • 有序節點:當在一個父目錄下如 /lock 下創建 有序節點,節點會按照嚴格的先後順序創建出自節點 lock000001,lock000002,lock0000003,以此類推,有序節點能嚴格保證各個自節點按照排序命名生成。
  • 臨時節點:客戶端建立了一個臨時節點,在客戶端的會話結束或會話超時,Zookepper 會自動刪除該節點 ID。
  • 事件監聽:在讀取數據時,我們可以對節點設置監聽,當節點的數據發生變化(1 節點創建 2 節點刪除 3 節點數據變成 4 自節點變成)時,Zookeeper 會通知客戶端。

在這裏插入圖片描述

結合這幾個特點,來看下 ZK 是怎麼組合分佈式鎖:

業務線程 -1,業務線程 -2 分別向 ZK 的 /lock 目錄下,申請創建有序的臨時節點。

業務線程 -1 搶到 /lock0001 的文件,也就是在整個目錄下最小序的節點,也就是線程 -1 獲取到了鎖。

業務線程 -2 只能搶到 /lock0002 的文件,並不是最小序的節點,線程 2 未能獲取鎖。

業務線程 -1 與 lock0001 建立了連接,並維持了心跳,維持的心跳也就是這把鎖的租期。

當業務線程 -1 完成了業務,將釋放掉與 ZK 的連接,也就是釋放了這把鎖。

②ZK 分佈式鎖的代碼實現

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

在這裏插入圖片描述

2.2.4究竟該用 CP 還是 AP 的分佈式鎖

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

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

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

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

如果說到事務,ACID 是傳統數據庫常用的設計理念,追求強一致性模型,關係數據庫的 ACID 模型擁有高一致性+可用性,所以很難進行分區。

在微服務中 ACID 已經是無法支持,我們還是回到 CAP 去尋求解決方案,不過根據上面的討論,CAP 定理中,要麼只能 CP,要麼只能 AP。

如果我們追求數據的一致性而忽略可用性這個在微服務中肯定是行不通的,如果我們追求可用性而忽略一致性,那麼在一些重要的數據(例如支付,金額)肯定出現漏洞百出,這個也是無法接受。所以我們既要一致性,也要可用性。

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

2.3.1實現最終一致性

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

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

2.3.2BASE 模型

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

BASE 模型反 ACID 模型,完全不同 ACID 模型,犧牲高一致性,獲得可用性或可靠性:Basically Available 基本可用。

支持分區失敗(e.g. sharding碎片劃分數據庫)Soft state 軟狀態,狀態可以有一段時間不同步,異步。

Eventually consistent 最終一致,最終數據是一致的就可以了,而不是時時一致。

2.3.3分佈式事務

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

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

①兩階段提交(2PC)

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

在這裏插入圖片描述

在 XA 協議中分爲兩階段:

事務管理器要求每個涉及到事務的數據庫預提交(Precommit)此操作,並反映是否可以提交。

事務協調器要求每個數據庫提交數據,或者回滾數據。

說一下,爲何在互聯網的系統中沒被改造過的兩階段提交基本很少被業界應用,最大的缺點就是同步阻塞問題。

在資源準備就緒之後,資源管理器中的資源就一直處於阻塞,直到提交完成之後,才進行資源釋放。這個在互聯網高併發大數據的今天,兩階段的提交是不能滿足現在互聯網的發展。還有就是兩階段提交協議雖然爲分佈式數據強一致性所設計,但仍然存在數據不一致性的可能。

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

②補償事務(TCC)

TCC 是服務化的兩階段編程模型,每個業務服務都必須實現 Try,Confirm,Cancel 三個方法,這三個方式可以對應到 SQL 事務中 Lock,Commit,Rollback。

在這裏插入圖片描述

相比兩階段提交,TCC 解決了幾個問題:同步阻塞,引入了超時機制,超時後進行補償,並不會像兩階段提交鎖定了整個資源,將資源轉換爲業務邏輯形式,粒度變小。

因爲有了補償機制,可以由業務活動管理器進行控制,保證數據一致性。Try 階段:Try 只是一個初步的操作,進行初步的確認,它的主要職責是完成所有業務的檢查,預留業務資源。Confirm 階段:Confirm 是在 Try 階段檢查執行完畢後,繼續執行的確認操作,必須滿足冪等性操作,如果 Confirm 中執行失敗,會有事務協調器觸發不斷的執行,直到滿足爲止。Cancel 是取消執行:在 Try 沒通過並釋放掉 Try 階段預留的資源,也必須滿足冪等性,跟 Confirm 一樣有可能被不斷執行。

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

在這裏插入圖片描述

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

在這裏插入圖片描述

在 Try 的時候,會讓庫存服務預留 N 個庫存給這個訂單使用,讓訂單服務產生一個“未確認”訂單,同時產生這兩個預留的資源。

在 Confirm 的時候,會使用在 Try 預留的資源,在 TCC 事務機制中認爲,如果在 Try 階段能正常預留的資源,那麼在 Confirm 一定能完整的提交。

在這裏插入圖片描述

在 Try 的時候,有任務一方爲執行失敗,則會執行 Cancel 的接口操作,將在 Try 階段預留的資源進行釋放。這個並不是重點要論 TCC 事務是怎麼實現,重點還是討論分佈式事務在 CAP+BASE 理論的應用。

實現可以參考:

https://github.com/changmingxie/tcc-transaction
③本地消息表

本地消息表這個方案最初是 eBay 提出的,eBay 的完整方案:

https://queue.acm.org/detail.cfm?id=1394128
本地消息表這種實現方式應該是業界使用最多的,其核心思想是將分佈式事務拆分成本地事務進行處理。

在這裏插入圖片描述

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

當我們去創建訂單的時候,我們新增一個本地消息表,把創建訂單和扣減庫存寫入到本地消息表,放在同一個事務(依靠數據庫本地事務保證一致性)。

配置一個定時任務去輪詢這個本地事務表,掃描這個本地事務表,把沒有發送出去的消息,發送給庫存服務,當庫存服務收到消息後,會進行減庫存,並寫入服務器的事務表,更新事務表的狀態。

庫存服務器通過定時任務或直接通知訂單服務,訂單服務在本地消息表更新狀態。

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

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

④MQ 事務

RocketMQ 在 4.3 版本已經正式宣佈支持分佈式事務,在選擇 RokcetMQ 做分佈式事務請務必選擇 4.3 以上的版本。

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

在這裏插入圖片描述

事務消息作爲一種異步確保型事務, 將兩個事務分支通過 MQ 進行異步解耦,RocketMQ 事務消息的設計流程同樣借鑑了兩階段提交理論。

整體交互流程如下圖所示:

在這裏插入圖片描述

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

三、總結

在微服務的構建中,永遠都逃離不了 CAP 理論,因爲網絡永遠不穩定,硬件總會老化,軟件可能出現 Bug,所以分區容錯性在微服務中是躲不過的命題。

可以這麼說,只要是分佈式,只要是集羣都面臨着 AP 或者 CP 的選擇,但你很貪心的時候,既要一致性又要可用性,那隻能對一致性作出一點妥協,也就是引入了 BASE 理論,在業務允許的情況下實現最終一致性。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章