為什麼要使用zookeeper

發表於2023-09-26

本文標題為《為什麼要使用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)。

時間/執行緒T1T2
t1讀取到sum值為0
t2 讀取到sum值為0
t3執行sum=0+1操作
t4 執行sum=0+1操作

(表1)

接下來我們再說可見性,即使兩個執行緒不是同時讀取sum的值,也有可能當一個執行緒修改了sum值之後,另一個執行緒不能及時看到最新的修改後的值。這是因為現在的CPU為了執行效率,為每個執行緒分配了一個暫存器,執行緒對記憶體的賦值不是直接更新,而是先更新自己的暫存器,然後CPU非同步的將暫存器的值重新整理到記憶體。因為暫存器的讀寫效能遠遠大於記憶體,所以這種非同步的讀寫方式可以大幅度提升CPU執行效率,讓CPU時鐘不會因為等待IO操作而暫停。

時間/執行緒T1T2
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),並且是執行緒安全的。

時間/執行緒T1T2T3
t1讀取到sum值為0
t2執行sum=0+1操作
t3 讀取到sum值為1
t4 執行sum=1+1操作
t5 讀取到sum值為2
t6 執行sum=2+1操作

(表3)
透過加鎖操作之後,count()方法變成一個不能被打破的原子操作,按照一定的順序依次執行,並且每個操作的執行結果都可以被後續操作立即可見。注意這裡面的順序,後面還會再講。

多程式與分散式鎖

上文中介紹了單機多執行緒場景使用鎖來保證程式碼執行緒安全的場景,分散式鎖顧名思義就是在分散式場景下多臺機器(多個程式)間使用的鎖。這麼說還是有點抽象,我們依然用計數器舉例。假設我們的計數器併發訪問壓力非常大,單機已經不能滿足我們的效能要求了,我們需要將單機擴充套件為多機執行,這樣就形成了一個計數器服務叢集。(這裡只是為了舉例,我相信沒有人會為了效能而搭建這樣的叢集,其實也沒有任何理由搭建這樣的叢集。)

image.png

我們還需要對單機版計數器程式碼改造為計數器服務,以適應分散式多機場景。

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叢集優先保證可用性,叢集具備一定的容錯能力,出現故障後叢集依然可以對外提供服務,但是不保證獲取到最新的資料。

image.png
(圖1)
image.png

(圖2)

同步複製

讓我們假設Redis支援同步複製再分析以上讀寫的場景。同步複製就是主節點接收到寫資料請求後,除了完成自身的寫入操作外必須要等待所有從節點完成複製操作後才算操作完成並返回客戶端(非同步複製則不需要等待)(圖3)。此時主節點資料和從節點資料沒有複製延遲問題,無論從主節點或者從節點讀取資料都可以獲取到最新的值(主節點寫入操作和所有從節點寫入操作不是發生在同一時間點,而如何讓主節點和從節點新寫入資料在同一時間點對外可見還是有很多需要考慮的地方)。而且主從切換後也不會丟失資料。但是同步複製模式也會帶來新的問題,首先因為寫操作要等待所有從節點完成,對於系統效能有比較大的影響。其次,一旦某個從節點故障或者網路故障,系統就無法寫入資料了。顯然在同步複製模式下,系統用降低可用性和效能為代價,換取資料一致性。這不僅符合CAP定理兩者選其一的要求,也再一次體現了線性一致性對於效能的影不容小覷。所以對於Redis來說,選擇效能和可用性更加符合它的使用場景和自身定位。
image.png
(圖3)

腦裂

對於一個分散式系統由網路分割槽的等原因造成系統分割成不同的部分且都對外提供服務就稱之為腦裂。對應到Redis叢集場景就是一旦發生腦裂,會有兩個Redis主節點同時接受客戶端的寫請求(圖4),這會導致併發寫入衝突而造成資料不一致現象。可以引起腦裂的場景很多,例如主從間網路延時、主節點故障後恢復、錯誤的自動/人工主從切換行為等。顯然對於分散式叢集腦裂是一個我們不得不解決的問題。
image.png
(圖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:紫光云云平臺開發部架構組架構師,擁有十餘年分散式系統設計及開發經驗,現從事雲端計算、企業上雲及企業數字化轉型相關架構設計工作。

相關文章