zookeeper介紹

zookeeper

| 介紹

開源的分佈式協調服務,雅虎創建,基於google chubby。

可以解決的問題

  • 數據的發佈/訂閱(配置中心:disconf)
  • 負載均衡(dubbo利用了zookeeper機制實現負載均衡)
  • 命名服務
  • master選舉(kafka、hadoop、hbase)
  • 分佈式隊列
  • 分佈式鎖。

特性

  • 順序一致性

  • 原子性

  • 可靠性

  • 實時性

    一旦一個事務被成功應用,客戶端就能夠立即從服務器端讀取到事務變更後的最新數據狀態;(zookeeper僅僅保證在一定時間內,近實時)

文件系統

數據模型和文件系統類似,每一個節點爲稱znode,zk中的最小數據單元,每個node上可以保存數據和掛載子節點。構成一個層次化的屬性結構。

[外鏈圖片轉存失敗(img-jI6h3nEF-1564466530214)(.assets/20141108213344_45.png)]

節點類型

  • 持久化節點
  • 持久化有序節點
  • 臨時節點

臨時節點的生命期和會話週期相同

  • 臨時有序節點

存儲數據大小:不要超過1M

命令

create [-s] [-e] path data

get path [watch]

set path data [version]

version表示鎖的概念,樂觀鎖,數據庫裏面有一個version字段去控制數據的版本號

delete path [version]

必須從子節點開始刪除,不會立即生效,有會話重試機制,過一段時間纔會有

stat信息

名稱 說明
cversion 子節點的版本號
dataVersion 數據的版本號
aclVersion 表示acl的版本號,修改節點權限
czxid 節點被創建時的事務ID
mzxid 節點最後一次被更新的事務ID
pzxid 當前節點下的子節點最後一次被修改時的事務ID
ctime
mtime
ephemeralOwner 創建臨時節點時,會有一個sessionId
dataLength 數據長度

Watcher特性

分佈式數據發佈/訂閱。zk允許客戶端向服務器註冊一個watcher監聽,當服務器端的節點觸發指定事件(數據改變、刪除、子目錄節點增加刪除等)的時候會觸發watcher,服務端會向客戶端發送一個事件通知。

watcher的通知是一次性的,一量觸發一次通知後,該watcher就失效。

ACL

提供控制節點訪問權限的功能,用於有效的保證zk中數據的安全性,避免誤操作而導致出現重大事故。

| 集羣

角色類型

角色 說明
Leader 接收所有Follower的提案請求並統一協調發起投票,負責與所有的Follower進行內部的數據交換(同步)
Follower 直接爲客戶端服務並參與提案的投票,同時與Leader進行數據交換(同步)
Observer 直接爲客戶端服務但並不參與提案投票,同時也與Leader進行數據交換(同步)

follower不接收寫請求

| zk一致性協議 - zab工作原理

leader選舉

三種選主算法:leaderElection/AuthFastLeaderElection/FastLeaderElection(默認)

FastLeaderElection

  • serverid: 在配置server集羣時,給定服務器的標識id(myid)
  • zxid: 64位Long類型,高32位(Epoch,選舉輪數)表示當前屬於那個leader統治,低32位遞增的事務id號,zxid值越大,表示數據越新
  • server的狀態:Looking,Following,Observering,Leading

選舉流程

  1. 狀態設置爲LOOKING,初始化內部投票Vote(id, zxid),將其廣播到其它節點;首次投票都是自己作爲Leader;然後循環等待其它節點的投票信息;
  2. 每收到一個Vote,都和自己的Vote數據PK,規則爲ZXID大的優先,相等時給ID大的投票。若外部投票獲勝,將該選票覆蓋自己的Vote後再次廣播出去;同時統計是否有過半的贊同者與自己的投票數據一致,無則繼續等待Vote,有則需要判斷Leader是否在贊同者之中,在則退出循環,選舉結束,根據選舉結果及各自角色切換狀態。

每一次啓動時初始化爲Looking

[外鏈圖片轉存失敗(img-oj3BwLk1-1564466530215)(.assets/20181129114824253-1564408184881.png)]

假設這些服務器從id1-5,依序啓動:

因爲一共5臺服務器,只有超過半數以上,即最少啓動3臺服務器,集羣才能正常工作。

(1)服務器1啓動,發起一次選舉。

服務器1投自己一票。此時服務器1票數一票,不夠半數以上(3票),選舉無法完成;
服務器1狀態保持爲LOOKING;

(2)服務器2啓動,再發起一次選舉。

服務器1和2分別投自己一票,此時服務器1發現服務器2的id比自己大,更改選票投給服務器2;
此時服務器1票數0票,服務器2票數2票,不夠半數以上(3票),選舉無法完成;
服務器1,2狀態保持LOOKING;

(3)服務器3啓動,發起一次選舉。

與上面過程一樣,服務器1和2先投自己一票,然後因爲服務器3id最大,兩者更改選票投給爲服務器3;
此次投票結果:服務器1爲0票,服務器2爲0票,服務器3爲3票。此時服務器3的票數已經超過半數(3票),服務器3當選Leader。
服務器1,2更改狀態爲FOLLOWING,服務器3更改狀態爲LEADING;

(4)服務器4啓動,發起一次選舉。

此時服務器1,2,3已經不是LOOKING狀態,不會更改選票信息。交換選票信息結果:服務器3爲3票,服務器4爲1票。
此時服務器4服從多數,更改選票信息爲服務器3;
服務器4並更改狀態爲FOLLOWING;

(5)服務器5啓動,同4一樣投票給3,此時服務器3一共5票,服務器5爲0票;

服務器5並更改狀態爲FOLLOWING;

最終Leader是服務器3,狀態爲LEADING;

其餘服務器是Follower,狀態爲FOLLOWING。

選主後的數據同步

二階提交:leader生成提議並廣播給followers,收到半數以上的ACK後,再廣播commit消息,同時將事務操作應用到內存中。follower收到提議後先將事務寫到本地事務日誌,然後反饋ACK,等接到leader的commit消息時,纔會將事務操作應用到內存中。

問題

假設一個事務P1在leader服務器被提交了,並且已經有過半的follower返回了ack。 在leader節點把commit消息發送給folower機器之前leader服務器掛了怎麼辦?

新生成的Leader之前是follower,未收到commit消息,內存中是沒有P1數據,ZAB協議保證選主後,P1是需要應用到集羣中的。即通過選主後的數據同步來彌補。

已被處理的消息不能丟

消息在Leader上Commit了,但是其它Server還沒有收到Commit消息已經掛了,爲了實現已經被處理的消息不能丟,Zab使用了以下策略:

  1. 選舉擁有zxid最大的節點作爲新的leader;由於所有Proposal需要過半節點ACK,必須有一個節點保存了所有被COMMIT消息的Proposal狀態;
  2. 新leader將自己事務日誌中的proposal但未commit的消息處理;
  3. 新laeder與follower建立先進先出隊列,先將自身有而follower沒有的proposal發送給follower,再將這些proposal的commit命令發送給follower,以保證所有的 follower都保存了所有的proposal並已處理。

被丟棄的消息不能再次出現

場景:當leader接收到消息請求生成proposal後就掛了,其它follower並沒有收到此proposal,重新選了leader後,這條消息是被跳過的。之前的leader重新啓動成了follower,保留的被跳過的proposal狀態,與整個系統狀態不一致,需要刪除。

Zab通過巧妙的設計zxid來實現這一目的,高32 epoch表示leader選舉的輪數,每選一次,epoch+1,低32位是消息計數器,每接收到一條消息,這個值+1,新leader選舉後這個值重置爲0。這樣,舊的leader掛了後重啓,它不會被選舉爲leader,因爲此時它的zxid肯定小於當前新的leader,當舊的leader作爲follwer接入新leader後,新leader會讓它將所有的擁有舊的epoch號的未被commit的proposal清除

zab協議,一定需要保證已經被leader提交的事務也能夠被所有follower提交

zab協議需要保證,在崩潰恢復過程中跳過哪些已經被丟棄的事務

事務操作

二階提交,針對client的請求,leader服務器會爲其生成對應的事務proposal,並將其發送給其它follower,然後收集各自的選票,最後進行事務提交。

[外鏈圖片轉存失敗(img-rl2lu3Om-1564466530215)(.assets/1523632294541695.png)]

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

什麼情況下zab協議會進入崩潰恢復模式

  • 當服務器啓動時
  • 當leader服務器出現網絡中斷、崩潰或者重啓的情況
  • 集羣中已經不存在過半的服務器與該leader保持正常通信

zab協議進入崩潰恢復模式會做什麼

  • 當leader出現問題,zab協議進入崩潰恢復模式,並且選舉出新的leader。當新的leader選舉出來以後,如果集羣中已經有過半機器完成了leader服務器的狀態同(數據同步),退出崩潰恢復,進入消息廣播模式
  • 當新的機器加入到集羣中的時候,如果已經存在leader服務器,那麼新加入的服務器就會自覺進入數據恢復模式,找到leader進行數據同步

[外鏈圖片轉存失敗(img-JUvIJkpS-1564466530215)(.assets/1564408409704.png)]

| 安裝

單機安裝

http://apache.fayea.com/zookeeper中下載

zkCli.sh -server ip:port

zoo.cfg配置說明

tickTime:時間單位,默認值是2000ms
initLimit:10*2000,leader服務器等待follow啓動並完成同步的時間
syncLimit:5*2000,leader節點和follower節點進行心跳檢測的最大延時時間
dataDir:zk服務器存儲快照文件的目錄
clientPort:客戶端訪問的端口
dataLogDir:表示配置zk事務日誌的存儲路徑,默認在dataDir目錄下

集羣安裝

[外鏈圖片轉存失敗(img-tQ4V5Qtu-1564466530216)(.assets/1564197131345.png)]

第三步:啓動每個zookeeper

如何增加observer節點

zoo.cfg中 增加 peerType=observer

server.1=192.168.11.129:2888:3181  
server.2=192.168.11.135:2888:3181   
server.3=192.168.11.136:2888:3181:observer

三個端口:

2181: 對Client端提供服務

2888:集羣內機器通訊使用

3888:選舉leader,leader掛掉時使用

使用docker安裝,直接使用zookeeper:latest版本,最新版本爲3.5.5

使用docker-compose啓動zk集羣

官方安裝文檔

version: '3.1'

services:
  zoo1:
    image: zookeeper
    restart: always
    container_name: zoo1
    ports:
      - 2181:2181
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=0.0.0.0:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181
    networks:
      
  zoo2:
    image: zookeeper
    restart: always
    container_name: zoo2
    ports:
      - 2182:2181
    environment:
      ZOO_MY_ID: 2
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=0.0.0.0:2888:3888;2181 server.3=zoo3:2888:3888;2181

  zoo3:
    image: zookeeper
    restart: always
    container_name: zoo3
    ports:
      - 2183:2181
    environment:
      ZOO_MY_ID: 3
      ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=0.0.0.0:2888:3888;2181

docker-compose up -d

[外鏈圖片轉存失敗(img-zqNFwUNn-1564466530216)(.assets/1563270840302.png)]

分別將本地的2181、2182、2183端口映射到對應容器的2181端口上。

ZOO_MY_ID:表示zk服務的id

ZOO_SERVERS:表示zk集羣的主機列表

查看各個節點的狀態

[root@ceos03 zookeeper-svc]# docker exec -it zoo1 zkServer.sh status

ZooKeeper JMX enabled by default
Using config: /conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower

[root@ceos03 zookeeper-svc]# docker exec -it zoo2 zkServer.sh status

ZooKeeper JMX enabled by default
Using config: /conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower

[root@ceos03 zookeeper-svc]# docker exec -it zoo3 zkServer.sh status

ZooKeeper JMX enabled by default
Using config: /conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader

| 應用 - 分佈式鎖

鎖分爲兩類,一類是保持獨佔,另一種是控制時序。

獨佔鎖

所有client去創建同一個節點,如/Lock,最終成功創建的那個client擁有了這把鎖,用完成之後再刪除。

控制時序

所有client在/LOCKs下創建臨時有序節點,編號最小的獲得鎖,用完刪除,其它client依次使用,未獲得鎖的監控相臨較小編號節點即可。

控制時序代碼實現

  1. 導入jar包

查找zk包的版本,http://www.mvnrepository.com/search?q=zookeeper

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.5.5</version>
</dependency>
  1. zk連接
public class ZkClient {

    private final static String CONNECTSTRING="192.168.10.13:2181";

    private static int sessionTimeout=5000;

    //獲取連接
    public static ZooKeeper getInstance() throws IOException, InterruptedException {

        final CountDownLatch conectStatus=new CountDownLatch(1);

        ZooKeeper zooKeeper=new ZooKeeper(CONNECTSTRING, sessionTimeout, new Watcher() {
            public void process(WatchedEvent event) {
                if(event.getState()== Event.KeeperState.SyncConnected){
                    conectStatus.countDown();
                }
            }
        });

        conectStatus.await();
        return zooKeeper;
    }

    public static int getSessionTimeout() {
        return sessionTimeout;
    }
}
  1. 分佈式鎖實現
public class DistributeLock {

    private static final String ROOT_LOCKS="/LOCKS";//根節點

    private ZooKeeper zooKeeper;

    private int sessionTimeout; //會話超時時間

    private String lockID; //記錄鎖節點id

    private final static byte[] data={1,2}; //節點的數據

    private CountDownLatch countDownLatch=new CountDownLatch(1);

    public DistributeLock() throws IOException, InterruptedException {
        this.zooKeeper=ZookeeperClient.getInstance();
        this.sessionTimeout=ZookeeperClient.getSessionTimeout();
    }

    //獲取鎖的方法
    public boolean lock(){
        try {
            //1. create LOCKS/00000001
            lockID=zooKeeper.create(ROOT_LOCKS+"/",data, ZooDefs.Ids.
                    OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            System.out.println(Thread.currentThread().getName()+"->成功創建了lock節點["+lockID+"], 開始去競爭鎖");
			//2. get all node
            List<String> childrenNodes=zooKeeper.getChildren(ROOT_LOCKS,true);//獲取根節點下的所有子節點
            //3. sort and get minist
            SortedSet<String> sortedSet=new TreeSet<String>();
            for(String children:childrenNodes){
                sortedSet.add(ROOT_LOCKS+"/"+children);
            }
            String first=sortedSet.first(); //拿到最小的節點
            if(lockID.equals(first)){
                //表示當前就是最小的節點
                System.out.println(Thread.currentThread().getName()+"->成功獲得鎖,lock節點爲:["+lockID+"]");
                return true;
            }
            SortedSet<String> lessThanLockId=sortedSet.headSet(lockID);
            // 4. watch close less No. node
            if(!lessThanLockId.isEmpty()){
                String prevLockID=lessThanLockId.last();//拿到比當前LOCKID這個幾點更小的上一個節點
                zooKeeper.exists(prevLockID,new LockWatcher(countDownLatch));
                countDownLatch.await(sessionTimeout, TimeUnit.MILLISECONDS);
                //上面這段代碼意味着如果會話超時或者節點被刪除(釋放)了
                System.out.println(Thread.currentThread().getName()+" 成功獲取鎖:["+lockID+"]");
            }
            return true;
        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return false;
    }

    public boolean unlock(){
        System.out.println(Thread.currentThread().getName()+"->開始釋放鎖:["+lockID+"]");
        try {
            zooKeeper.delete(lockID,-1);
            System.out.println("節點["+lockID+"]成功被刪除");
            return true;
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
        return false;
    }
}
  1. main
    public static void main(String[] args) {
        final CountDownLatch latch=new CountDownLatch(10);
        Random random=new Random();
        for(int i=0;i<10;i++){
            new Thread(()->{
                DistributeLock lock=null;
                try {
                    lock=new DistributeLock();
                    latch.countDown();
                    latch.await();
                    lock.lock();
                    Thread.sleep(random.nextInt(500));
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    if(lock!=null){
                        lock.unlock();
                    }
                }
            }).start();
        }
    }

| 應用 - master選舉

master-slave模式

資源:https://www.cnblogs.com/sky-sql/p/6804467.html

[外鏈圖片轉存失敗(img-6zPbMhnm-1564466530216)(.assets/393620-20160627154234109-1109968833.png)]

zookeeper進行master選舉使用場景是什麼?

分佈式,Master往往用來協調集羣中的其他系統單元,具有對分佈式狀態變更的決定權。如:Master負責處理一些複雜的邏輯,並將結果同步給集羣中其他系統單元。

多個client同時去創建相同的節點,創建成功的即是master;

創建失敗的需要獲取節點上的數據,即具體的master信息;

代碼實現

1. jar依賴

    <dependency>
      <groupId>org.apache.zookeeper</groupId>
      <artifactId>zookeeper</artifactId>
      <version>3.5.5</version>
    </dependency>
    <dependency>
      <groupId>com.101tec</groupId>
      <artifactId>zkclient</artifactId>
      <version>0.11</version>
    </dependency>

2. MasterSelector

package com.wjg.master_slave;

import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class MasterSelector {

    private ZkClient zkClient;

    private final static String MASTER_PATH = "/master"; //需要爭搶的節點

    private IZkDataListener dataListener; //註冊節點內容變化

    private UserCenter server;  //其他服務器

    private UserCenter master;  //master節點

    private boolean isRunning = false;

    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

    public MasterSelector(UserCenter server, ZkClient zkClient) {

        System.out.println("[" + server + "] 去爭搶master權限");
        this.server = server;
        this.zkClient = zkClient;

        this.dataListener = new IZkDataListener() {
            @Override
            public void handleDataChange(String s, Object o) throws Exception {

            }

            @Override
            public void handleDataDeleted(String s) throws Exception {
                //節點如果被刪除, 發起選主操作,這裏可以處理,判斷之前的master是否爲本機,若是,則立即進行選舉,否則 ,等待5s再進行選舉
                if(master.getMc_id() != server.getMc_id())
                    TimeUnit.SECONDS.sleep(4);
                chooseMaster();

            }
        };
    }

    public void start() {
        //開始選舉
        if (!isRunning) {
            isRunning = true;
            zkClient.subscribeDataChanges(MASTER_PATH, dataListener); //註冊節點事件
            chooseMaster();
        }
    }


    public void stop() {
        //停止
        if (isRunning) {
            isRunning = false;
            scheduledExecutorService.shutdown();
            zkClient.unsubscribeDataChanges(MASTER_PATH, dataListener);
            releaseMaster();
        }
    }


    //具體選master的實現邏輯
    private void chooseMaster() {
        if (!isRunning) {
            System.out.println("當前服務沒有啓動");
            return;
        }
        try {
            zkClient.createEphemeral(MASTER_PATH, server);
            master = server; //把server節點賦值給master
            System.out.println(master + "->我現在已經是master,你們要聽我的");

            //定時器
            //master釋放(master 出現故障),5秒後釋放一次
            scheduledExecutorService.schedule(() -> {
                releaseMaster();//釋放鎖
            }, 2, TimeUnit.SECONDS);
        } catch (ZkNodeExistsException e) {
            //表示master已經存在
            UserCenter userCenter = zkClient.readData(MASTER_PATH, true);
            if (userCenter == null) {
                System.out.println("啓動操作:");
                chooseMaster(); //再次獲取master
            } else {
                master = userCenter;
            }
        }
    }

    private void releaseMaster() {
        //釋放鎖(故障模擬過程)
        //判斷當前是不是master,只有master才需要釋放
        if (checkIsMaster()) {
            System.out.println(server+" release master!");
            zkClient.delete(MASTER_PATH); //刪除
        }
    }


    private boolean checkIsMaster() {
        //判斷當前的server是不是master
        UserCenter userCenter = zkClient.readData(MASTER_PATH);
        if (userCenter.getMc_name().equals(server.getMc_name())) {
            master = userCenter;
            return true;
        }
        return false;
    }

}

3. UserCenter

package com.wjg.master_slave;

import java.io.Serializable;

public class UserCenter implements Serializable {

    private static final long serialVersionUID = -1776114173857775665L;
    private int mc_id; //機器信息

    private String mc_name;//機器名稱

    public int getMc_id() {
        return mc_id;
    }

    public void setMc_id(int mc_id) {
        this.mc_id = mc_id;
    }

    public String getMc_name() {
        return mc_name;
    }

    public void setMc_name(String mc_name) {
        this.mc_name = mc_name;
    }

    @Override
    public String toString() {
        return "UserCenter{" +
                "mc_id=" + mc_id +
                ", mc_name='" + mc_name + '\'' +
                '}';
    }
}

4. main

public class App 
{
    private final static String CONNECTSTRING="192.168.10.13:2181";


    public static void main(String[] args) throws IOException {

        List<MasterSelector> selectorLists = new ArrayList<>();

        for (int i = 0; i < 10; i++) {
            int id = i;
            Thread th = new Thread(() -> {
                System.out.println(id+" started!");
                ZkClient zkClient = new ZkClient(CONNECTSTRING, 5000,
                        10000,
                        new SerializableSerializer());

                UserCenter userCenter = new UserCenter();
                userCenter.setMc_id(id);
                userCenter.setMc_name("客戶端:" + id);

                MasterSelector selector = new MasterSelector(userCenter, zkClient);
                selectorLists.add(selector);
                selector.start();//觸發選舉操作
            });
            th.start();
        }



        System.out.println("press any key to stop!");
        System.in.read();

        for (MasterSelector selector : selectorLists) {
            selector.stop();
        }


    }
}

說明:在實際生產環境中,可能會由於插拔網線等導致網絡短時的不穩定,也就是網絡抖動。由於正式生產環境中可能server在zk上註冊的信息是比較多的,而且server的數量也是比較多的,那麼每一次切換主機,每臺server要同步的數據量(比如要獲取誰是master,當前有哪些salve等信息,具體視業務不同而定)也是比較大的。那麼我們希望,這種短時間的網絡抖動最好不要影響我們的系統穩定,也就是最好選出來的master還是原來的機器,那麼就可以避免發現master更換後,各個salve因爲要同步數據等導致的zk數據網絡風暴。所以在搶主的時候,如果之前主機是本機,則立即搶主,否則延遲5s搶主。這樣就給原來主機預留出一定時間讓其在新一輪選主中佔據優勢,從而利於環境穩定。

| 應用 - 分佈隊列

先進先出隊列

  1. 通過getchildren獲取指定根節點下的所有子節點,子節點就是任務
  2. 確定自己節點在子節點中的順序
  3. 如果自己不是最小的節點,那麼監控比自己小的上一個子節點,否則處於等待
  4. 接收watcher通知,重複流程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章