本文標題為《為什麼要使用zookeeper》,但是本文並不是專門介紹zookeeper原理及其使用方法的文章。如果你在網上搜索為什麼要使用zookeeper,一定能能到從zookeeper原理、適用場景到Zab算法原理等各種各樣的介紹,但是看過之後是不是還是懵懵懂懂,只是學會了一些片面的、具體的知識點,還是不能文章標題的問題。zookeeper使用一種名為Zab的共識算法實現,除了Zab算法之外還有Paxos、Multi-Paxos、Raft等共識算法,實現上也有chubby、etcd、consul等獨立的中間件和像Redis哨兵模式一樣的嵌入式實現,這些實現都是基於類似的底層邏輯為了適用於不同場景下的工程學落地,本文的重點內容是共性的底層原理而不是具體的軟件使用指導。
多線程與鎖
我以如何實現分佈式鎖為切入點,將多線程編程、鎖、分佈式系統、分佈式系統一致性模型(線性一致性、最終一致性)、CAP定理、複製冗餘、容錯容災、共識算法等一眾概念有機結合起來。採用層層遞進的方式對相關概念及其相互聯繫展開論述,不僅讓你能將零散的知識點串連成線,而且還能站在實際應用的角度對相關概念重新思考。
之所以用分佈式鎖來舉例,是因為在編程領域,鎖這個概念太普遍了,在多線程編程場景有同步鎖、共享鎖、排他所、自旋鎖和鎖升級等與鎖有關的概念,在數據庫領域也有行級鎖、表級鎖、讀鎖、寫鎖、謂詞鎖和間隙鎖等各種名詞概念。本質上鎖就是一種有一定排他性的資源佔有機制,一旦一方持有某個對象的鎖,另一方就不能持有同一對象相同的鎖。 那麼什麼是分佈式鎖呢?要回答這個問題我們需要先了解單機情況下鎖的原理。在單機多線程編程中,我們需要同步機制來保證共享變量的可見性和原子性。如何理解可見性和原子性呢?我用一個經典的計數器代碼舉例。
class Counter{
private int sum=0;
public int count(int increment){
return sum += increment
}
}
代碼很簡單,有過多線程編程經驗的人都應該知道count()方法在單線程下工作正常,但是在多線程場景下就會失效。原則上一個線程循環執行一百遍count(1)和一百個線程每個線程執行一遍count(1)結果應該都是100,但是實際執行的結果大概率是不相同,這種單線程下執行正確但是多線程下執行邏輯不正確的情況我們稱之為線程不安全。
為什麼在多線程下執行結果不正確呢?
首先當兩個線程同時執行sum=sum+1這條語句的時候,語句並不是原子性的,而是一個讀操作和一個寫操作。有可能兩個線程都同時讀取到了sum的值為0,加1操作後sum的值被兩次賦值為1,這就像第一個線程的操作被第二個線程覆蓋了一下,我們稱之為覆蓋更新(表1)。
| 時間/線程 | T1 | T2 |
|---|---|---|
| t1 | 讀取到sum值為0 | |
| t2 | 讀取到sum值為0 | |
| t3 | 執行sum=0+1操作 | |
| t4 | 執行sum=0+1操作 |
(表1)
接下來我們再説可見性,即使兩個線程不是同時讀取sum的值,也有可能當一個線程修改了sum值之後,另一個線程不能及時看到最新的修改後的值。這是因為現在的CPU為了執行效率,為每個線程分配了一個寄存器,線程對內存的賦值不是直接更新,而是先更新自己的寄存器,然後CPU異步的將寄存器的值刷新到內存。因為寄存器的讀寫性能遠遠大於內存,所以這種異步的讀寫方式可以大幅度提升CPU執行效率,讓CPU時鐘不會因為等待IO操作而暫停。
| 時間/線程 | T1 | T2 |
|---|---|---|
| t1 | 讀取到sum值為0 | |
| t2 | 執行sum=0+1操作 | |
| t3 | 讀取到sum值為0 | |
| t4 | 執行sum=0+1操作 |
(表2)
我們需要同步機制保證count()的原子性和可見性
class Counter{
private int sum=0;
public synchronized int count(int increment){
return sum += increment
}
}
如果替換為鎖的語義,這段代碼就相當於
class Counter{
private int sum=0;
public int count(int increment){
lock();
sum += increment
unlock();
return sum;
}
}
lock()和unlock()方法都是偽代碼,相當於加鎖和解鎖操作。一個線程調用了lock()方法獲取到鎖之後才可以執行後面的語句,執行完畢後調用unlock()方法釋放鎖。此時如果另一個線程也調用lock()方法就會因無法獲取到鎖而等待,直到第一個線程執行完畢後釋放鎖。鎖不但能保證代碼執行的原子性,還能保證變量的可見性,獲取到鎖之後的線程讀取的任何共享變量一定是它最新的值,不會獲取到其他線程修改後的過期值。
我們再來放大一下lock()內部細節。顯然為了保證獲取鎖的排他性,我們需要先去判斷線程是否已經獲得了鎖,如果還沒有線程獲得鎖就給當前線程加鎖,如果已經有其他線程已經獲取了鎖就等待。顯然獲取鎖本身也需要保證原子性和可見性,所以lock()方法必須是一個同步(synchronized)方法,unlock()也是一樣的道理。 在強調一下,加解鎖方法本身都要具備原子性和可見性是一個重要的概念,後面我們會用到。
public synchronized void lock(){
if(!hasLocked()){
locked();
return;
}else{
awaited();
}
}
注:以上所有代碼均為偽代碼,只為説明鎖的作用及原理,無需深究
使用鎖(同步方法)之後的count()就可以在多線程下併發執行了(表1),並且是線程安全的。
| 時間/線程 | T1 | T2 | T3 |
|---|---|---|---|
| t1 | 讀取到sum值為0 | ||
| t2 | 執行sum=0+1操作 | ||
| t3 | 讀取到sum值為1 | ||
| t4 | 執行sum=1+1操作 | ||
| t5 | 讀取到sum值為2 | ||
| t6 | 執行sum=2+1操作 |
(表3)
通過加鎖操作之後,count()方法變成一個不能被打破的原子操作,按照一定的順序依次執行,並且每個操作的執行結果都可以被後續操作立即可見。注意這裏面的順序,後面還會再講。
多進程與分佈式鎖
上文中介紹了單機多線程場景使用鎖來保證代碼線程安全的場景,分佈式鎖顧名思義就是在分佈式場景下多台機器(多個進程)間使用的鎖。這麼説還是有點抽象,我們依然用計數器舉例。假設我們的計數器併發訪問壓力非常大,單機已經不能滿足我們的性能要求了,我們需要將單機擴展為多機運行,這樣就形成了一個計數器服務集羣。(這裏只是為了舉例,我相信沒有人會為了性能而搭建這樣的集羣,其實也沒有任何理由搭建這樣的集羣。)
我們還需要對單機版計數器代碼改造為計數器服務,以適應分佈式多機場景。
class Counter{
public int count(int increment){
lock();
int sum=getSumFromDB();
sum += increment
setSumToDB(sum);
unlock();
return sum;
}
}
- 因為我們要在多台機器間共享並操作總數數據,所以不能使用只有單機可見的變量存儲,可以將這個值存儲在一個多台機器能訪問和操作的數據存儲層,代碼中使用數據庫(getSumFromDB)作為存儲目的地。無論採用何種方式實現,數據存儲中間件都必須保證數據的可見性,即數據變更後可以讀取到最新的值。
- count()方法還是讀取-寫回模式,所以依然要使用鎖模式來阻止多台機器多台機器(多個進程)間併發操作,保證計數操作在分佈式場景下的正確性。這裏的鎖就是分佈式鎖,lock()和unlcok()就是分佈式鎖的加鎖和解鎖方法。
- 分佈式鎖的加解鎖操作需要在多台機器(或多個進程)間被調用。所以編程語言中沒有原生的方法供我們使用,通常需要我們基於各種中間件自己實現。
注:以上分佈式計數器代碼僅為示例,只為説明相關概念,無需深究。
分佈式鎖實現原理
鎖的實現原理並不複雜(注意我説額是基本原理,實際還是比較複雜的),鎖本身可以理解為一個標識,加鎖解鎖就是改變這個標識的狀態,當然因為要滿足排他性要求,加鎖前要判斷鎖是否已經存在。判斷標識和改變狀態操作必須是一個原子單元(原子性),並且鎖的狀態一旦改變就立刻可見(可見性),這樣才能保證在多方(多線程或多進程)同時獲取鎖的時候,只有唯一一方可以得到。
synchronized{
if(flag==null){
flag=locked;
}else{
print("Flag is locked");
}
}
要實現分佈式鎖,我們可以將鎖的狀態存儲在數據庫中,每個進程通過讀取寫數據庫中鎖的狀態值來完成加解鎖操作。這樣就相當於把加鎖操作原子性和鎖狀態數據可見性的要求轉移給了數據庫。這種原子性和可見性的要求在數據庫領域是由數據一致性模型來定義並保證的。針對分佈式鎖這個場景,我們需要數據庫提供線性一致性(linearizability)保證。線性一致性也稱強一致性或原子一致性,它的理論定義比較複雜,這裏就不展開了,我們只需要知道線性一致性提供額一下三點保證:
- 就近性:一旦一個新值被寫入或者讀取,所有後續的對該值讀取看到的都是最新的值,直到它被再次修改。
- 原子性:所有操作都是原子操作,沒有併發行為。
- 順序性:所有操作都可以按照全局時間順序排序,並且所有客户端看到的順序一致。這也就保證了所有客户端看到的數據狀態變化是一致性的。
顯然線性一致性確實可以滿足我們對於實現分佈式鎖的全部要求。那麼接下來的問題就是哪些數據庫可以提供線性一致性保證?
就近性看似是一個公理,似乎數據庫都應該支持(其實不然,可見最新值數據對於分佈式系統其實是一個很嚴格的要求。即使是單機場景,受限於性能及使用場景也需有不同實現,後面我們會介紹。)。提到原子性你應該能夠想到我們很熟悉的一款緩存數據庫Redis,因為Redis本身是以單線程方式執行而聞名,所以所有針對Redis的操作都是原子性的並且按照到達Redis的服務端的順序依次順序執行。那麼接下來我們就嘗試用Redis實現上文代碼中加鎖邏輯。
使用Redis實現分佈式鎖
Redis中有一個原子命令SETNX KEY VALUE,命令的意思是當指定的key不存在時,為key設置指定的值。我們可以通過SETNX flag locked完成加鎖操作,通過判斷命令的返回值(成功為1,失敗為0)確定自己是否得到了鎖。
這麼簡單嗎?我們前面做了這麼多鋪墊,從線程安全開始一直講到數據庫一致性模型,最後就用一條Redis命令實現了。但是這僅僅是開始,作為開發人員你一定知道很多時候正常的業務邏輯實現起來很簡單,但是如何處理異常才是難點所在。上面這段代碼正確的前提是我們的Redis部署為單一節點。而單點就意味着一旦出現故障,我們分佈式鎖服務就不可用,單點故障就是我們要處理的異常場景。為了提升系統整體的可用性,就必須避免單點部署,一旦我們的Redis就從單機升級為集羣,問題就會趨於複雜。在分佈式場景下如何既能提供一致性保證又能在異常時保證系統可用性,將是我們接下來的重點。
CAP定理
一説到一致性和可用性關係,你應該能想到一個廣為人知的分佈式理論——CAP定理。CAP定理説的是在一個布式系統中分區容錯性、可用性和一致性最多隻能實現兩個,由於分佈式系統網絡故障一定會發生,網絡分區場景不可避免,所以分區容錯性我們必須保證,只能在一致性和可用性中二選一,最終系統要麼選擇分區容錯性和一致性(CP),要麼選擇分區容錯性和可用性(AP)。而這裏所説的一致性就是線性一致性。CP系統要求要麼不返回,返回一定是最新的值。AP系統要求每個請求必須有響應,但是可以返回過期值。
CAP定理本身限制條件比較多。首先分佈式系統除了網絡分區之外還有很多故障場景,如網絡延時、機器故障等、進程崩潰等。其次定理本身並沒有將系統性能考慮在內,一致性不僅需要和可用性做權衡,也需要在性能上做取捨,上文中提到的每個線程都先更新自己的寄存器後異步更新內存顯然就是為了性能考量而不是為了容錯。雖然CAP定理在工程落地中指導意義略顯不足,但是作為一個簡化模型,為我們理解分佈式系統在發生網絡分區故障時如何在一致性和可用性間平衡取捨提供了參考。
集羣下困境
補充完分佈式理論知識,讓我們回到分佈式鎖場景。為了規避單點故障,我們使用兩台機服務器搭建了一個Redis集羣,集羣有兩個節點,一個主節點,一個從節點。只有主節點可以承接客户端寫操作,並且將負責將數據異步複製到從節點中(Redis只支持異步複製),從節點只能接受讀請求,這樣我們就搭建了一個一主一從的Redis集羣。那麼這個Redis集羣以整體的方式對外提供服務是否可以提供線性一致性呢?
異步複製
因為主從間為異步複製,所以會出現複製延遲情況。也就是採用讀寫分離方式(圖2),客户端在主節點寫入數據後,在從節點不一定讀取到最新的數據(此時滿足最終一致性,即當沒有數據寫入操作後,經過一段時間後主從節點數據最終將達成一致)。如果所有讀寫操作均在主節點進行(圖1),此時似乎和單節點一樣可以滿足線性一致性的,但是一旦發生故障導致主節點不能訪問,為保證系統可用性集羣會進行主從切換將從節點提升為主節點,而此時未複製完成的數據就會丟失,客户端也有可能讀取到舊數據。所以無論採取什麼樣的讀數據模式,在Redis主從異步複製的架構下,均不滿足線性一致性要求,不能用於分佈式鎖場景。從CAP定理角度看,Redis集羣優先保證可用性,集羣具備一定的容錯能力,出現故障後集羣依然可以對外提供服務,但是不保證獲取到最新的數據。
(圖1)
(圖2)
同步複製
讓我們假設Redis支持同步複製再分析以上讀寫的場景。同步複製就是主節點接收到寫數據請求後,除了完成自身的寫入操作外必須要等待所有從節點完成複製操作後才算操作完成並返回客户端(異步複製則不需要等待)(圖3)。此時主節點數據和從節點數據沒有複製延遲問題,無論從主節點或者從節點讀取數據都可以獲取到最新的值(主節點寫入操作和所有從節點寫入操作不是發生在同一時間點,而如何讓主節點和從節點新寫入數據在同一時間點對外可見還是有很多需要考慮的地方)。而且主從切換後也不會丟失數據。但是同步複製模式也會帶來新的問題,首先因為寫操作要等待所有從節點完成,對於系統性能有比較大的影響。其次,一旦某個從節點故障或者網絡故障,系統就無法寫入數據了。顯然在同步複製模式下,系統用降低可用性和性能為代價,換取數據一致性。這不僅符合CAP定理兩者選其一的要求,也再一次體現了線性一致性對於性能的影不容小覷。所以對於Redis來説,選擇性能和可用性更加符合它的使用場景和自身定位。
(圖3)
腦裂
對於一個分佈式系統由網絡分區的等原因造成系統分割成不同的部分且都對外提供服務就稱之為腦裂。對應到Redis集羣場景就是一旦發生腦裂,會有兩個Redis主節點同時接受客户端的寫請求(圖4),這會導致併發寫入衝突而造成數據不一致現象。可以引起腦裂的場景很多,例如主從間網絡延時、主節點故障後恢復、錯誤的自動/人工主從切換行為等。顯然對於分佈式集羣腦裂是一個我們不得不解決的問題。
(圖4)
本節小結
我們做個小結,單機版的Redis滿足線性一致性要求,可以用來實現分佈式鎖,但是有單點故障問題。為了提高可用性,我們構建了一個Redis集羣,但是集羣並不能滿足線性一致性要求,所以也無法來實現分佈式鎖。似乎我們又陷入了一個CAP定理二選一的難題中,那麼有沒有一個分佈式存儲系統,即可以實現一致性,又可以保證可用性呢。
使用zookeeper實現分佈式鎖
我想你已經猜到答案了,接下來我們正式介紹zookeeper。zookeeper被定義為一個高可靠的分佈式協調服務。這個定義不是很直觀,其實我更願意將zookeeper理解為數據庫,只不多zookeeper的讀寫方式更像是對文件系統操作,而不是傳統關係數據庫中SQL語句形式或者key-value數據庫的get/set方式。協調服務也不難理解,分佈式鎖不就是對各個爭搶鎖的進程由誰獲得鎖這個行為進行協調,還有主從切換也是對各個候選節點誰可以晉升為主節點這個行為進行協調。只不過這些協調操作以操作zookeeper中ZNode(類似於文件系統裏的目錄)的方式實現。
zookeeper有一個原子級命令create可以用創建一個節點(ZNode)。ZooKeeper中的節點的路徑必須是唯一的,這意味着在同一級目錄下,你不能創建同名的節點。所以客户端可以通這條命令過創建同一級目錄下的同名節點並根據返回的結果來確定是否加鎖成功,如果創建成功説明加鎖成功,否者加鎖失敗。和Redis的實現一樣簡單。
(注:此處的實現方式只是示例説明,並非常用的實現方法)
全序關係廣播
zookeeper是以名為Zab的分佈式共識算法為基礎實現的。“共識”的意思就是在所有分佈式節點中達成一致。和Redis主從複製類似,zookeeper也由一個主節點(leader節點)用來接收客户端寫操作,並且將數據複製個所有的從節點(follower節點)。這種由一個主節點接受數據寫入請求,再將數據有序複製到從節點的方式我們稱之為全序關係廣播。對全序關係廣播是一種節點之間的數據交換協議,它要求滿足下面兩個基本屬性:
- 數據可靠性:複製數據的消息必須被髮送到所有節點
- 數據有序性:消息發送到各個節點的順序與主節點操作順序完全相同
故障及容錯
單從這兩個屬性上看,主從複製的Redis也實現了全序關係廣播。但是就像前文所屬,如何處理系統運行過程中產生的異常邏輯才是關鍵。
- 首先我們要保證系統在任何時期都只能有一個主節點(這點很重要,否則會出現“腦裂”,破壞數據一致性)。
- 其次當主節點故障的情況下系統系統可以自己選舉一個新的主節點繼續提供服務。選舉過程也要保證主節點唯一性,並且新的主節點不能丟失數據(參見Redis異步複製場景)。顯然讓讓新的主節點信息在所有節點間達成一致也是一個共識問題。
- 最後我們還要能處理從節點發生髮生故障的情況,不能出現從節點故障造成系統不可用的情況(參見Redis同步複製假設場景)。
和Redis主從間異步複製數據不同,zookeeper採用類似半同步複製的方式。zookeeper寫入操作需要等待大多數從節點完成複製後才算完成,這裏的大多數為集羣的所有節點數N除以2在加1(N/2+1)。假設集羣中有三個節點,zookeeper的寫入操作就需要同步等待兩個節點完成複製操作。這樣為集羣提供了一定的容錯性,最多允許1-(N/2+1)個節點故障,系統依然可以對外提供服務。
zookeeper主、節點間通過網絡心跳的方式監測並確定主節點正常的狀態,心跳中斷一段時候後從節點認為主節點故障,就會發起新的主節點選舉過程,從節點向集羣中的其他節點發送一個投票的提案,聲明自己希望成為主節點。其他節點根據情況同意或反對。這裏有三個關鍵點:
- 只有收到大多數節點同意選主投票的情況下,選舉的過程才算完成。也就是説選取的主節點的行為在集羣中達成了共識。
- 每一個主節點的任期內都有一個全局唯一、單調遞增任期編號,從節點發起選主提案的時候會帶着自己的任期編號遞增後的新編號,其他節點只對大於自己已知最大的任期編號的選主提案投贊成票。任期編號不僅在選主過程中使用,主節點向從節點的複製數據的消息中也攜帶這個編號,只有消息的任期編號不小於從節點已知的任期編號,也就是從節點上次參與投票達成共識的主節點地位沒有變化,消息才可被接受。通過任期編號,我們就保證系統在同一時期內只能只有唯一一個主節點。
- zookeeper每一次寫操作主節點都會生成一個全局唯一的遞增的zxid,並將zxid通過複製消息傳播給所有的從節點。選主的提案也會包含zxid,從節點不會給一個zxid小於自己zxid的選主提案投票。這樣就能保證不會出現有不完成數據的從節點被選取為主節點,避免主從切換後數據丟失。
本節小結
我們再小結一下,像Zab這類分佈式識算法通常有如下特點:
- 只有一個主節點承擔所有的寫操作並採用全序關係廣播向從節點複製數據。以此保證複製消息的可靠性和有序性,並且可以進一步保證操作的原子性。
- 寫操作需要等待大多數從節點(N/2+1)完成複製,可以容忍節點小部分節點(1-(N/2+1))故障。保證一定程度上的可用性
- 可以監測到主節點故障並以投票的方式選取新的主節點。選取主節點的提案需要獲得集羣中大多數節點的同意,並且算法通過主節點任期編碼和全局操作順序編碼(zxid)規避了腦裂和主從切換後數據丟失問題。
可能你已經注意到了,上面提到共識算法的特點中提到了原子性、順序性和一定程度的可用性,而線性一致性原則中就近性原則沒有涉及。不同的共識算法對寫入數據的一致性要求比較統一,但是對讀取數據一致性要求各不相同,具體落地實現的時候會根據使用場景進行取捨(zookeeper提供最終一致性讀,etcd提供串行一致性和線性一致性讀),所以使用前一定要閲讀説明文檔,明確他們所提供的一致性保證,否則很可能錯誤使用。zookeeper文檔中明確説明了自己不滿足讀數據線性一致性要求,只保證寫數據的線性一致性。因為zookeeper為了性能將讀操作交由從節點完成,所以有可能讀取到舊的數據。那麼上文提到的使用create()創建分佈式鎖的方法還能生效嗎。答案是肯定的,因為create()方法作為一個寫方法只能在Redis主節點執行,主節點數據為最新可以保證就近性原則。這裏要囉嗦一句,實際使用場景中不會使用create()方法創建分佈式鎖(存在鎖釋放後通知、鎖無法釋放等問題),而是採用創建並監聽臨時順序節點的方式實現,在不滿足讀數據的線性一致性場景下,zookeeper依然可以實現一個分佈式鎖,這就是zookeeper的精妙之處,如果有同學對這方面感興趣,有機會我將撰文介紹。
總結
最後讓我們來做個總結吧。本文從多線程下鎖的原理開始,一步一步介紹到共識算法。 對於鎖的實現來説,無論是單機線程鎖還是多機分佈式説,都必須要求鎖操作具備原子性和可見性——即線性一致性一致性,單機情況下編程語言級就可以支持,但是分佈式場下必須通過能滿足線性一致性的中間件支持。在單節點場景下,很多數據庫都可以保證某種意義上的線性一致性,顯然在一個節點上更好確定操作的順序和保證操作的原子性,也更好實現數據就近性。但是單節點無法避免單點故障,如果增加節點數組成分佈式潔羣,我們又會受制於CAP定理的限制,只能在一致性和可用性中兩者選其一。
共識算法本質上是為了在多個節點間達成一致,這就可以成為實現線性一致性的基礎,所以理論上共識算法是可以實現線性一致性的。並且共識算法通過在複製操作和選主行為上使用“大多數”原則,保證了一定程度上的可用性。但是這裏要注意,共識算法也不能完全違背CAP定理,使用共識算法的分佈式系統理論上還是CP系統,即使通過主從切換和半同步複製提供一定程度的系統容錯能力,但是這些容錯都有限制條件(小於半數節點故障),一旦超過容錯極限,系統還是不可用的。
共識算法也並非“銀彈”,使用共識算法構建的系統也會有諸多影響及限制。首先系統寫入和讀取的性能影響我們就不能小覷。寫入數據通常只能在單節點進行,所以單節點的吞吐能力會成為整個集羣的瓶頸,而且等待大多數節點完成複製操作也遠比異步複製來的慢些,這些都會影響寫入性能。不同的共識算法實現系統有不同的選擇選擇策略,但是要保證線性一致性讀取數據(常見的方式就是隻從主節點讀取數據),對系統的性能和吞吐量影響我們也是不能忽略的。其次共識算法對節點數量有要求,因為“大多數”原則,所以集羣起始節點數最少為3台,而且為了避免網絡分區為同數量集羣集羣造成選主效率下降問題,集羣節點數量通常建議為奇數,這就為集羣部署提出了要求。還有共識算法通常依賴超時來判斷主節點故障情況,在網絡延時較高的場景下可能出現無意義的主從切換,所以共識算法對網絡性能和穩定性更加敏感。最後共識算法本身比想象的要複雜許多,需要精巧的設計和工程化落地,所以我們常見的共識算法和適用組件並不多。也不要去挑戰設計並實現一個全新的共識算法,而是從已有的、經歷過大規模場景使用驗證過的成熟中間件中選擇,這樣就導致共識算法和已有工程的集成性和改造性欠佳(參見kafka依賴zookeeper和Redis的哨兵模式)。
鑑於以上對共識算法理解,像zookeeper這類實現了共識算法的中間件更適用於沒有大量的數據寫入或者變更,並且對數據一致性有較高要求(讀取最新數據和故障恢復後不丟失數據),對性能沒有太高要求,但是希望系統有一定的容錯能力的場景。所以zookeeper這類服務雖然也能提供數據存儲服務但是通常不作為業務數據庫使用(業務數據庫通常有較高的讀寫吞吐量和性能要求),而是作為服務組件或中間件的基礎組件,用於選主、加鎖、服務註冊發現等這類對數據一致性有較強要求且希望系統能提供一定的可用性的場景。希望通過我上面的介紹,你能有所收穫。如有你有問題也可以在文章後留言評論,我們一起討論。
作者簡介
Jerry Tse:紫光云云平台開發部架構組架構師,擁有十餘年分佈式系統設計及開發經驗,現從事雲計算、企業上雲及企業數字化轉型相關架構設計工作。