分散式協調元件Zookeeper之 選舉機制與ZAB協議

wangheng1409發表於2021-09-02

Zookeeper簡介:

Zookeeper是什麼:

  Zookeeper 是⼀個分散式協調服務的開源框架。 主要⽤來解決分散式叢集中應⽤系統的⼀致性問題, 例如怎樣避免同時操作同⼀資料造成髒讀的問題。分散式系統中資料存在⼀致性的問題!!

  • ZooKeeper 本質上是⼀個分散式的⼩⽂件儲存系統。 提供基於類似於⽂件系統的⽬錄樹⽅式的數 據儲存,並且可以對樹中的節點進⾏有效管理。
  • ZooKeeper 提供給客戶端監控儲存在zk內部資料的功能,從⽽可以達到基於資料的叢集管理。 諸 如: 統⼀命名服務(dubbo)、分散式配置管理(solr的配置集中管理)、分散式訊息佇列 (sub/pub)、分散式鎖、分散式協調等功能。

架構組成:

 

 Leader

  • Zookeeper 叢集⼯作的核⼼⻆⾊
  • 叢集內部各個伺服器的排程者。
  • 事務請求(寫操作) 的唯⼀排程和處理者,保證叢集事務處理的順序性;對於 create, setData, delete 等有寫操作的請求,則需要統⼀轉發給leader 處理, leader 需要決定編號、執 ⾏操作,這個過程稱為⼀個事務。

Follower

  • 處理客戶端⾮事務(讀操作) 請求,
  • 轉發事務請求給 Leader;
  • 參與叢集 Leader 選舉投票 2n-1臺可以做叢集投票。

此外,針對訪問量⽐較⼤的 zookeeper 叢集, 還可新增觀察者⻆⾊。

Observer

  • 觀察者⻆⾊,觀察 Zookeeper 叢集的最新狀態變化並將這些狀態同步過來,其對於⾮事務請求可 以進⾏獨⽴處理,對於事務請求,則會轉發給 Leader伺服器進⾏處理。
  • 不會參與任何形式的投票只提供⾮事務服務,通常⽤於在不影響叢集事務處理能⼒的前提下提升集 群的⾮事務處理能⼒。增加了叢集增加併發的讀請求

ZK也是Master/slave架構,但是與之前不同的是zk叢集中的Leader不是指定⽽來,⽽是通過選舉產⽣。

Zookeeper 特點:

  • 1.Zookeeper:⼀個領導者(leader:⽼⼤),多個跟隨者(follower:⼩弟)組成的叢集。
  • 2. Leader負責進⾏投票的發起和決議,更新系統狀態(內部原理)
  • 3. Follower⽤於接收客戶請求並向客戶端返回結果,在選舉Leader過程中參與投票
  • 4. 叢集中只要有半數以上節點存活,Zookeeper叢集就能正常服務。
  • 5. 全域性資料⼀致:每個server儲存⼀份相同的資料副本,Client⽆論連線到哪個server,資料都是⼀ 致的。
  • 6. 更新請求順序進⾏(內部原理)
  • 7. 資料更新原⼦性,⼀次資料更新要麼成功,要麼失敗

Zookeeper 節點 之 ZNode 型別:

Zookeeper 節點型別可以分為三⼤類:

  • 永續性節點(Persistent)
  • 臨時性節點(Ephemeral)
  • 順序性節點(Sequential)

在開發中在建立節點的時候通過組合可以⽣成以下四種節點型別:持久節點、持久順序節點、臨時節 點、臨時順序節點。不同型別的節點則會有不同的⽣命週期

持久節點:是Zookeeper中最常⻅的⼀種節點型別,所謂持久節點,就是指節點被建立後會⼀直存在服 務器,直到刪除操作主動清除

持久順序節點:就是有順序的持久節點,節點特性和持久節點是⼀樣的,只是額外特性表現在順序上。 順序特性實質是在建立節點的時候,會在節點名後⾯加上⼀個數字字尾,來表示其順序。

臨時節點:就是會被⾃動清理掉的節點,它的⽣命週期和客戶端會話綁在⼀起,客戶端會話結束,節點 會被刪除掉。與永續性節點不同的是,臨時節點不能建立⼦節點。

臨時順序節點:就是有順序的臨時節點,和持久順序節點相同,在其建立的時候會在名字後⾯加上數字 字尾

事務ID:

  在ZooKeeper中,事務是指能夠改變ZooKeeper伺服器狀態的操作,我們也稱之為事務操作或更新 操作,⼀般包括資料節點建立與刪除、資料節點內容更新等操作。對於每⼀個事務請求,ZooKeeper都 會為其分配⼀個全域性唯⼀的事務ID,⽤ ZXID 來表示,通常是⼀個 64 位的數字。每⼀個 ZXID 對應⼀次 更新操作,從這些ZXID中可以間接地識別出ZooKeeper處理這些更新操作請求的全域性順序 zk中的事務指的是對zk伺服器狀態改變的操作(create,update data,更新位元組點);zk對這些事務操作都 會編號,這個編號是⾃增⻓的被稱為ZXID。

ZNode 的狀態資訊:

#使⽤bin/zkCli.sh 連線到zk叢集
[zk: localhost:2181(CONNECTED) 2] get /zookeeper
cZxid = 0x0
ctime = Wed Dec 31 19:00:00 EST 1969
mZxid = 0x0
mtime = Wed Dec 31 19:00:00 EST 1969
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1

含義:

 cZxid 就是 Create ZXID,表示節點被建立時的事務ID。
 ctime 就是 Create Time,表示節點建立時間。
 mZxid 就是 Modified ZXID,表示節點最後⼀次被修改時的事務ID。
 mtime 就是 Modified Time,表示節點最後⼀次被修改的時間。
 pZxid 表示該節點的⼦節點列表最後⼀次被修改時的事務 ID。只有⼦節點列表變更才會更新 pZxid,
⼦節點內容變更不會更新。
 cversion 表示⼦節點的版本號。
 dataVersion 表示內容版本號。
 aclVersion 標識acl版本
 ephemeralOwner 表示建立該臨時節點時的會話 sessionID,如果是永續性節點那麼值為 0
 dataLength 表示資料⻓度。
 numChildren 表示直系⼦節點數。

Watcher 機制:

  在 ZooKeeper 中,引⼊了 Watcher 機制來實現這種分散式的通知功能。ZooKeeper 允許客戶端向服務 端註冊⼀個 Watcher 監聽,當服務端的⼀些指定事件觸發了這個 Watcher,那麼Zk就會向指定客戶端 傳送⼀個事件通知來實現分散式的通知功能。

 Zookeeper的Watcher機制主要包括客戶端執行緒、客戶端WatcherManager、Zookeeper伺服器三部 分。

具體⼯作流程為:

  • 客戶端在向Zookeeper伺服器註冊的同時,會將Watcher物件儲存在客戶端的WatcherManager當 中
  • 當Zookeeper伺服器觸發Watcher事件後,會向客戶端傳送通知
  • 客戶端執行緒從WatcherManager中取出對應的Watcher物件來執⾏回撥邏輯

注: 客戶端負責watch的註冊 和回撥,zk伺服器負責處理watch。

命令列使用:

   建立順序節點:create -s /zk-test 123
  建立臨時節點:create -e /zk-temp 123
  建立永久節點:create /zk-permanent 123
  讀取節點:ls path   其中,path表示的是指定資料節點的節點路徑
  獲取內容:get path
  更新節點:set path data
  刪除節點: delete path

JAVA 操作Zookeeper:

<dependency>
 <groupId>org.apache.zookeeper</groupId>
 <artifactId>zookeeper</artifactId>
 <version>3.4.14</version>
</dependency> 
<dependency>
 <groupId>com.101tec</groupId>
 <artifactId>zkclient</artifactId>
 <version>0.2</version>
 </dependency>

建立會話:

package com.hust.grid.leesf.zkclient.examples;
import java.io.IOException;
import org.I0Itec.zkclient.ZkClient;
public class CreateSession {
/*
 建立⼀個zkClient例項來進⾏連線
 */
 public static void main(String[] args) {
   ZkClient zkClient = new ZkClient("127.0.0.1:2181");
   System.out.println("ZooKeeper session created.");
 }
}

建立節點:

package com.hust.grid.leesf.zkclient.examples;
import org.I0Itec.zkclient.ZkClient;
public class Create_Node_Sample {
 public static void main(String[] args) {
 ZkClient zkClient = new ZkClient("127.0.0.1:2181");
 System.out.println("ZooKeeper session established.");
 //createParents的值設定為true,可以遞迴建立節點
 zkClient.createPersistent("/lg-zkClient/lg-c1",true);
 System.out.println("success create znode.");
 }
}

刪除節點:

package com.hust.grid.leesf.zkclient.examples;
import org.I0Itec.zkclient.ZkClient;
public class Del_Data_Sample {
 public static void main(String[] args) throws Exception {
 String path = "/lg-zkClient/lg-c1";
 ZkClient zkClient = new ZkClient("127.0.0.1:2181", 5000);
 zkClient.deleteRecursive(path);
 System.out.println("success delete znode.");
 }
}

監聽節點變化:

import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.ZkClient;
import org.apache.zookeeper.client.ZooKeeperSaslClient;
import java.util.List;
/*
演示zkClient如何使⽤監聽器
*/
public class Get_Child_Change {
 public static void main(String[] args) throws InterruptedException {
 //獲取到zkClient
 final ZkClient zkClient = new ZkClient("linux121:2181");
 //zkClient對指定⽬錄進⾏監聽(不存在⽬錄:/lg-client),指定收到通知之後的邏輯
 //對/lag-client註冊了監聽器,監聽器是⼀直監聽
 zkClient.subscribeChildChanges("/lg-client", new IZkChildListener() {
 //該⽅法是接收到通知之後的執⾏邏輯定義
 public void handleChildChange(String path, List<String> childs)
throws Exception {
 //列印節點資訊
 System.out.println(path + " childs changes ,current childs " +
childs);
 }
 });
 //使⽤zkClient建立節點,刪除節點,驗證監聽器是否運⾏
 zkClient.createPersistent("/lg-client");
 Thread.sleep(1000); //只是為了⽅便觀察結果資料
 zkClient.createPersistent("/lg-client/c1");
 Thread.sleep(1000);
 zkClient.delete("/lg-client/c1");
 Thread.sleep(1000);
 zkClient.delete("/lg-client");
 Thread.sleep(Integer.MAX_VALUE);
 /*
 1 監聽器可以對不存在的⽬錄進⾏監聽
 2 監聽⽬錄下⼦節點發⽣改變,可以接收到通知,攜帶資料有⼦節點列表
 3 監聽⽬錄建立和刪除本身也會被監聽到
 */
 }
}

執行結果:

/lg-zkClient 's child changed, currentChilds:[]
/lg-zkClient 's child changed, currentChilds:[c1]
/lg-zkClient 's child changed, currentChilds:[]
/lg-zkClient 's child changed, currentChilds:null

注:

  • 客戶端可以對⼀個不存在的節點進⾏⼦節點變更的監聽。
  • ⼀旦客戶端對⼀個節點註冊了⼦節點列表變更監聽之後,那麼當該節點的⼦節點列表發⽣變更時,服務 端都會通知客戶端,並將最新的⼦節點列表傳送給客戶端  
  • 該節點本身的建立或刪除也會通知到客戶端。

監聽節點資料變化:

import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
//使⽤監聽器監聽節點資料的變化
public class Get_Data_Change {
 public static void main(String[] args) throws InterruptedException {
 // 獲取zkClient物件
 final ZkClient zkClient = new ZkClient("linux121:2181");
 //設定⾃定義的序列化型別,否則會報錯!!
 zkClient.setZkSerializer(new ZkStrSerializer());
 //判斷節點是否存在,不存在建立節點並賦值
 final boolean exists = zkClient.exists("/lg-client1");
 if (!exists) {
 zkClient.createEphemeral("/lg-client1", "123");
 }
 //註冊監聽器,節點資料改變的型別,接收通知後的處理邏輯定義
 zkClient.subscribeDataChanges("/lg-client1", new IZkDataListener() {
 public void handleDataChange(String path, Object data) throws
Exception {
 //定義接收通知之後的處理邏輯
 System.out.println(path + " data is changed ,new data " +
data);
 }
 //資料刪除--》節點刪除
 public void handleDataDeleted(String path) throws Exception {
 System.out.println(path + " is deleted!!");
 }
 });
 //更新節點的資料,刪除節點,驗證監聽器是否正常運⾏
 final Object o = zkClient.readData("/lg-client1");
 System.out.println(o);
 zkClient.writeData("/lg-client1", "new data");
 Thread.sleep(1000);
 //刪除節點
 zkClient.delete("/lg-client1");
 Thread.sleep(Integer.MAX_VALUE);
 }
}

zk 自定義字串序列化:

import org.I0Itec.zkclient.exception.ZkMarshallingError;
import org.I0Itec.zkclient.serialize.ZkSerializer;
public class ZkStrSerializer implements ZkSerializer {
 //序列化,資料--》byte[]
 public byte[] serialize(Object o) throws ZkMarshallingError {
  return String.valueOf(o).getBytes();
 }
 //反序列化,byte[]--->資料
 public Object deserialize(byte[] bytes) throws ZkMarshallingError {
 return new String(bytes);
 }
}

 

Leader選舉:

選舉機制:

  • 半數機制:叢集中半數以上機器存活,叢集可⽤。所以Zookeeper適合安裝奇數臺伺服器。
  • Zookeeper雖然在配置⽂件中並沒有指定Master和Slave。但是,Zookeeper⼯作時,是有⼀個節 點為Leader,其它為Follower,Leader是通過內部的選舉機制產⽣的。
  • 只有當機器減少或叢集初次啟動才會選舉leader。

 

 

 詳細步驟:

  • (1)伺服器1啟動,此時只有它⼀臺伺服器啟動了,它發出去的報⽂沒有任何響應,所以它的選舉狀態 ⼀直是LOOKING狀態。
  • (2)伺服器2啟動,它與最開始啟動的伺服器1進⾏通訊,互相交換⾃⼰的選舉結果,由於兩者都沒有 歷史資料,所以id值較⼤的伺服器2勝出,但是由於沒有達到超過半數以上的伺服器都同意選舉它(這個 例⼦中的半數以上是3),所以伺服器1、2還是繼續保持LOOKING狀態。 return String.valueOf(o).getBytes(); } //反序列化,byte[]--->資料 public Object deserialize(byte[] bytes) throws ZkMarshallingError { return new String(bytes); } } 123 /lg-client1 data is changed ,new data new data /lg-client1 is deleted!!
  • (3)伺服器3啟動,根據前⾯的理論分析,伺服器3成為伺服器1、2、3中的⽼⼤,⽽與上⾯不同的 是,此時有三臺伺服器選舉了它,所以它成為了這次選舉的Leader。
  • (4)伺服器4啟動,根據前⾯的分析,理論上伺服器4應該是伺服器1、2、3、4中最⼤的,但是由於前 ⾯已經有半數以上的伺服器選舉了伺服器3,所以它只能接收當⼩弟的命了。
  • (5)伺服器5啟動,同4⼀樣稱為follower

叢集首次啟動:

  半數前選myid 最大的機器。

非首次啟動:

  優先選擇zxid值⼤的節點稱為Leader!!

ZAB⼀致性協議:

  ZAB 協議是為分散式協調服務 Zookeeper 專⻔設計的⼀種⽀持崩潰恢復和原⼦⼴播協議

原子廣播:

  

 

 

 

 具體流程:

 

 

 

 

 

 

 

總結: 第一步傳送提議,如果提議獲得半數以上機器的ack,然後傳送commit給follower,同時自己commit。

崩潰恢復:

Leader當機後,被選舉的新Leader需要解決的問題:

  • ZAB 協議確保那些已經在 Leader 提交的事務最終會被所有伺服器提交。
  • ZAB 協議確保丟棄那些只在 Leader 提出/複製,但沒有提交的事務。

選舉演算法的關鍵點:保證選舉出的新Leader擁有叢集中所有節點最⼤編號(ZXID)的事務!!

總結:leader崩潰,新的leader必須擁有最大的事務id,這樣才能保證資料最新。

 

 

相關文章