ZooKeeper(概念篇):ZooKeeper是如何解決數據一致性的?

你用過zk做註冊中心,你知道zk是幹什麼的嗎?

前面講了各種一致性,比如:分佈式架構之Consistency(一致性、強一致性,弱一致性,順序一致性,最終一致性)分佈式架構之CAP 定理的含義分佈式架構之Base 理論,其是都是爲了後面做鋪墊。

zookeeper(簡稱zk),顧名思義,爲動物園管理員的意思,動物對應服務節點,zk是這些節點的管理者。在分佈式場景中,zk的應用非常廣泛,如:數據發佈/訂閱、命名服務、配置中心、分佈式鎖、集羣管理、選主與服務發現等等。這不僅得益於zk類文件系統的數據模型和基於Watcher機制的分佈式事件通知,也得益於zk特殊的高容錯數據一致性協議。

一致性

       這裏的一致性,是指數據在多個副本之間保持一致的特性。分佈式環境裏,多個副本處於不同的節點上,如果對副本A的更新操作,未同步到副本B上,外界獲取數據時,A與B的返回結果會不一樣,這是典型的分佈式數據不一致情況。而強一致性,是指分佈式系統中,如果某個數據更新成功,則所有用戶都能讀取到最新的值。CAP定理告訴我們,在分佈式系統設計中,P(分區容錯性)是不可缺少的,因此只能在A(可用性)與C(一致性)間做取捨。本文主要探究zk在數據一致性方面的處理邏輯。

一致性就不多說了,前面幾篇都已經系統總結過了。

日常生活中是怎麼解決一致性的

  1. 領導說:今天下午3-4點,大會議室開會,做一下未來的規劃,收到請回復。(這領導挺好的,沒讓你下班後7-8點開會就不錯了)
  2. 看到通知的同事會在自己的日程表裏需要添加一條待開會記錄,並回復;
  3. 而領導他會看有多少人回覆了他這條通知,如果說回覆的太少,他可能會進行二次通知,如果說回覆的人比較多,他就放心了,他並不會統計是不是所有人都回復了;
  4. 等到下午3點,所有同事一起做未來的規劃。

基本概念

  • 數據節點(dataNode):zk數據模型中的最小數據單元,數據模型是一棵樹,由斜槓(/)分割的路徑名唯一標識,數據節點可以存儲數據內容及一系列屬性信息,同時還可以掛載子節點,構成一個層次化的命名空間。

  • 會話(Session):指zk客戶端與zk服務器之間的會話,在zk中,會話是通過客戶端和服務器之間的一個TCP長連接來實現的。通過這個長連接,客戶端能夠使用心跳檢測與服務器保持有效的會話,也能向服務器發送請求並接收響應,還可接收服務器的Watcher事件通知。Session的sessionTimeout,是會話超時時間,如果這段時間內,客戶端未與服務器發生任何溝通(心跳或請求),服務器端會清除該session數據,客戶端的TCP長連接將不可用,這種情況下,客戶端需要重新實例化一個Zookeeper對象。

  • 事務及ZXID:事務是指能夠改變Zookeeper服務器狀態的操作,一般包括數據節點的創建與刪除、數據節點內容更新和客戶端會話創建與失效等操作。對於每個事務請求,zk都會爲其分配一個全局唯一的事務ID,即ZXID,是一個64位的數字,高32位表示該事務發生的集羣選舉週期(集羣每發生一次leader選舉,值加1),低32位表示該事務在當前選擇週期內的遞增次序(leader每處理一個事務請求,值加1,發生一次leader選擇,低32位要清0)。

  • 事務日誌:所有事務操作都是需要記錄到日誌文件中的,可通過 dataLogDir配置文件目錄,文件是以寫入的第一條事務zxid爲後綴,方便後續的定位查找。zk會採取“磁盤空間預分配”的策略,來避免磁盤Seek頻率,提升zk服務器對事務請求的影響能力。默認設置下,每次事務日誌寫入操作都會實時刷入磁盤,也可以設置成非實時(寫到內存文件流,定時批量寫入磁盤),但那樣斷電時會帶來丟失數據的風險。

  • 數據快照:數據快照是zk數據存儲中另一個非常核心的運行機制。數據快照用來記錄zk服務器上某一時刻的全量內存數據內容,並將其寫入到指定的磁盤文件中,可通過dataDir配置文件目錄。可配置參數snapCount,設置兩次快照之間的事務操作個數,zk節點記錄完事務日誌時,會統計判斷是否需要做數據快照(距離上次快照,事務操作次數等於snapCount/2~snapCount 中的某個值時,會觸發快照生成操作,隨機值是爲了避免所有節點同時生成快照,導致集羣影響緩慢)。

  • 過半:所謂“過半”是指大於集羣機器數量的一半,即大於或等於(n/2+1),此處的“集羣機器數量”不包括observer角色節點。leader廣播一個事務消息後,當收到半數以上的ack信息時,就認爲集羣中所有節點都收到了消息,然後leader就不需要再等待剩餘節點的ack,直接廣播commit消息,提交事務。選舉中的投票提議及數據同步時,也是如此,leader不需要等到所有learner節點的反饋,只要收到過半的反饋就可進行下一步操作。

保證一致性的要素

  • 領導
  • 兩階段提交
  • 過半驗證機制

領導者選舉發生的節點

  • 集羣啓動
  • Leader掛掉
  • Follower掛掉後Leader發現已經沒有過半的Follower跟隨自己了

數據模型

zk維護的數據主要有:客戶端的會話(session)狀態及數據節點(dataNode)信息。zk在內存中構造了個DataTree的數據結構,維護着path到dataNode的映射以及dataNode間的樹狀層級關係。爲了提高讀取性能,集羣中每個服務節點都是將數據全量存儲在內存中。可見,zk最適於讀多寫少且輕量級數據(默認設置下單個dataNode限制爲1MB大小)的應用場景。數據僅存儲在內存是很不安全的,zk採用事務日誌文件及快照文件的方案來落盤數據,保障數據在不丟失的情況下能快速恢復。

集羣架構

zk集羣由多個節點組成,其中有且僅有一個leader,處理所有事務請求;follower及observer統稱learner。learner需要同步leader的數據。follower還參與選舉及事務決策過程。zk客戶端會打散配置文件中的serverAddress 順序並隨機組成新的list,然後循環按序取一個服務器地址進行連接,直到成功。follower及observer會將事務請求轉交給leader處理。

要搭建一個高可用的zk集羣,我們首先需要確定好集羣規模。一般我們將節點(指leader及follower節點,不包括observer節點)個數設置爲 2*n+1 ,n爲可容忍宕機的個數。 zk使用“過半”設計原則,很好地解決了單點問題,提升了集羣容災能力。但是zk的集羣伸縮不是很靈活,集羣中所有機器ip及port都是事先配置在每個服務的zoo.cfg 文件裏的。如果要往集羣增加一個follower節點,首先需要更改所有機器的zoo.cfg,然後逐個重啓。

集羣模式下,單個zk服務節點啓動時的工作流程大體如下:

  • 統一由QuorumPeerMain作爲啓動類,加載解析zoo.cfg配置文件;

  • 初始化核心類:ServerCnxnFactory(IO操作)、FileTxnSnapLog(事務日誌及快照文件操作)、QuorumPeer實例(代表zk集羣中的一臺機器)、ZKDatabase(內存數據庫)等;

  • 加載本地快照文件及事務日誌,恢復內存數據;

  • 完成leader選舉,節點間通過一系列投票,選舉產生最合適的機器成爲leader,同時其餘機器成爲follower或是observer。關於選舉算法,就是集羣中哪個機器處理的數據越新(通過ZXID來比較,ZXID越大,數據越新),其越有可能被選中;

  • 完成leader與learner間的數據同步:集羣中節點角色確定後,leader會重新加載本地快照及日誌文件,以此作爲基準數據,再結合各個learner的本地提交數據,leader再確定需要給具體learner回滾哪些數據及同步哪些數據;

  • 當leader收到過半的learner完成數據同步的ACK,集羣開始正常工作,可以接收並處理客戶端請求,在此之前集羣不可用。

zookeeper一致性協議

 zookeeper實現數據一致性的核心是ZAB協議(Zookeeper原子消息廣播協議)。該協議需要做到以下幾點:

  1. 集羣在半數以下節點宕機的情況下,能正常對外提供服務;
  2. 客戶端的寫請求全部轉交給leader來處理,leader需確保寫變更能實時同步給所有follower及observer;
  3. leader宕機或整個集羣重啓時,需要確保那些已經在leader服務器上提交的事務最終被所有服務器都提交,確保丟棄那些只在leader服務器上被提出的事務,並保證集羣能快速恢復到故障前的狀態。

       Zab協議有兩種模式, 崩潰恢復(選主+數據同步)和消息廣播(事務操作)。任何時候都需要保證只有一個主進程負責進行事務操作,而如果主進程崩潰了,就需要迅速選舉出一個新的主進程。主進程的選舉機制與事務操作機制是緊密相關的。下面詳細講解這三個場景的協議規則,從細節去探索ZAB協議的數據一致性原理。

選主

leader選舉是zk中最重要的技術之一,也是保證分佈式數據一致性的關鍵所在。當集羣中的一臺服務器處於如下兩種情況之一時,就會進入leader選舉階段——服務器初始化啓動、服務器運行期間無法與leader保持連接。
選舉階段,集羣間互傳的消息稱爲投票,投票Vote主要包括二個維度的信息:ID、ZXID

  • ID   被推舉的leader的服務器ID,集羣中的每個zk節點啓動前就要配置好這個全局唯一的ID。

  • ZXID  被推舉的leader的事務ID ,該值是從機器DataTree內存中取的,即事務已經在機器上被commit過了。

節點進入選舉階段後的大體執行邏輯如下:

  1. 設置狀態爲LOOKING,初始化內部投票Vote (id,zxid) 數據至內存,並將其廣播到集羣其它節點。節點首次投票都是選舉自己作爲leader,將自身的服務ID、處理的最近一個事務請求的ZXID(ZXID是從內存數據庫裏取的,即該節點最近一個完成commit的事務id)及當前狀態廣播出去。然後進入循環等待及處理其它節點的投票信息的流程中。
  2. 循環等待流程中,節點每收到一個外部的Vote信息,都需要將其與自己內存Vote數據進行PK,規則爲取ZXID大的,若ZXID相等,則取ID大的那個投票。若外部投票勝選,節點需要將該選票覆蓋之前的內存Vote數據,並再次廣播出去;同時還要統計是否有過半的贊同者與新的內存投票數據一致,無則繼續循環等待新的投票,有則需要判斷leader是否在贊同者之中,在則退出循環,選舉結束,根據選舉結果及各自角色切換狀態,leader切換成LEADING、follower切換到FOLLOWING、observer切換到OBSERVING狀態。

       算法細節可參照FastLeaderElection.lookForLeader(),主要有三個線程在工作:選舉線程(主動調用lookForLeader方法的線程,通過阻塞隊列sendqueue及recvqueue與其它兩個線程協作)、WorkerReceiver線程(選票接收器,不斷獲取其它服務器發來的選舉消息,篩選後會保存到recvqueue隊列中。zk服務器啓動時,開始正常工作,不停止)以及WorkerSender線程(選票發送器,會不斷地從sendqueue隊列中獲取待發送的選票,並廣播至集羣)。WorkerReceiver線程一直在工作,即使當前節點處於LEADING或者FOLLOWING狀態,它起到了一個過濾的作用,當前節點爲LOOKING時,纔會將外部投票信息轉交給選舉線程處理;如果當前節點處於非LOOKING狀態,收到了處於LOOKING狀態的節點投票數據(外部節點重啓或網絡抖動情況下),說明發起投票的節點數據跟集羣不一致,這時,當前節點需要向集羣廣播出最新的內存Vote(id,zxid),落後節點收到該Vote後,會及時註冊到leader上,並完成數據同步,跟上集羣節奏,提供正常服務。

選主後的數據同步

選主算法中的zxid是從內存數據庫中取的最新事務id,事務操作是分兩階段的(提出階段和提交階段),leader生成提議並廣播給followers,收到半數以上的ACK後,再廣播commit消息,同時將事務操作應用到內存中。follower收到提議後先將事務寫到本地事務日誌,然後反饋ACK,等接到leader的commit消息時,纔會將事務操作應用到內存中。可見,選主只是選出了內存數據是最新的節點,僅僅靠這個是無法保證已經在leader服務器上提交的事務最終被所有服務器都提交。比如leader發起提議P1,並收到半數以上follower關於P1的ACK後,在廣播commit消息之前宕機了,選舉產生的新leader之前是follower,未收到關於P1的commit消息,內存中是沒有P1的數據。而ZAB協議的設計是需要保證選主後,P1是需要應用到集羣中的。這塊的邏輯是通過選主後的數據同步來彌補。
       選主後,節點需要切換狀態,leader切換成LEADING狀態後的流程如下:

  1. 重新加載本地磁盤上的數據快照至內存,並從日誌文件中取出快照之後的所有事務操作,逐條應用至內存,並添加到已提交事務緩存commitedProposals。這樣能保證日誌文件中的事務操作,必定會應用到leader的內存數據庫中。
  2. 獲取learner發送的FOLLOWERINFO/OBSERVERINFO信息,並與自身commitedProposals比對,確定採用哪種同步方式,不同的learner可能採用不同同步方式(DIFF同步、TRUNC+DIFF同步、SNAP同步)。這裏是拿learner內存中的zxid與leader內存中的commitedProposals(min、max)比對,如果zxid介於min與max之間,但又不存在於commitedProposals中時,說明該zxid對應的事務需要TRUNC回滾;如果 zxid 介於min與max之間且存在於commitedProposals中,則leader需要將zxid+1~max 間所有事務同步給learner,這些內存缺失數據,很可能是因爲leader切換過程中造成commit消息丟失,learner只完成了事務日誌寫入,未完成提交事務,未應用到內存。
  3. leader主動向所有learner發送同步數據消息,每個learner有自己的發送隊列,互不干擾。同步結束時,leader會向learner發送NEWLEADER指令,同時learner會反饋一個ACK。當leader接收到來自learner的ACK消息後,就認爲當前learner已經完成了數據同步,同時進入“過半策略”等待階段。當leader統計到收到了一半已上的ACK時,會向所有已經完成數據同步的learner發送一個UPTODATE指令,用來通知learner集羣已經完成了數據同步,可以對外服務了。細節可參照Leader.lead() 、Follower.followLeader()及LearnerHandler類。

事務操作

ZAB協議對於事務操作的處理是一個類似於二階段提交過程。針對客戶端的事務請求,leader服務器會爲其生成對應的事務proposal,並將其發送給集羣中所有follower機器,然後收集各自的選票,最後進行事務提交。流程如下圖。

ZAB協議的二階段提交過程中,移除了中斷邏輯(事務回滾),所有follower服務器要麼正常反饋leader提出的事務proposal,要麼就拋棄leader服務器。follower收到proposal後的處理很簡單,將該proposal寫入到事務日誌,然後立馬反饋ACK給leader,也就是說如果不是網絡、內存或磁盤等問題,follower肯定會寫入成功,並正常反饋ACK。leader收到過半follower的ACK後,會廣播commit消息給所有learner,並將事務應用到內存;learner收到commit消息後會將事務應用到內存。

ZAB協議中多次用到“過半”設計策略 ,該策略是zk在A(可用性)與C(一致性)間做的取捨,也是zk具有高容錯特性的本質。相較分佈式事務中的2PC(二階段提交協議)的“全量通過”,ZAB協議可用性更高(犧牲了部分一致性),能在集羣半數以下服務宕機時正常對外提供服務。

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