分布式協(xié)調(diào)服務(wù)ZooKeeper使用入門

一、ZK簡介

在大數(shù)據(jù)技術(shù)體系內(nèi),很多技術(shù)框架都是用動物的名字命名的,比如Hadoop(大象)、Hive(蜜蜂)、Pig(小豬)。大數(shù)據(jù)服務(wù)通常都是分布式的,多個節(jié)點之間角色不同,在特殊情況下角色還會發(fā)生轉(zhuǎn)換,比如HDFS中的NameNode和SidebyNameNode,YARN中的ResourceManager和Standby ResourceManager,Spark中的Mastser和 Standby Master等等,這些角色(節(jié)點)在發(fā)生故障的時候,如何確保集群能正常工作是一個很重要的問題,Zookeeper的出現(xiàn)就是為了解決這個問題的。

Zookeeper是Apache的開源項目,按照官網(wǎng)的描述來定義:Zookeeper是一個支持分布式部署的,開源的,分布式應(yīng)用程序協(xié)調(diào)服務(wù),主要提供集群管理、分布式鎖、注冊中心和配置中心的功能。

Zookeeper本質(zhì)上是提供一個樹形目錄服務(wù),類似于Linux操作系統(tǒng)的目錄樹文件系統(tǒng),使得其下節(jié)點有一個層次化的結(jié)構(gòu)。其中的每一個節(jié)點都被成為ZNode,每個節(jié)點都可以擁有自己的子節(jié)點,也可以在節(jié)點本身上存儲少量(1MB)的數(shù)據(jù)信息。節(jié)點的分類如下:

  • 持久化節(jié)點,在ZK重啟后仍然存在的節(jié)點;
  • 臨時節(jié)點,存在內(nèi)存中的具有有效期的節(jié)點,有效期過后或者重啟后就不存在了;
  • 持久化順序節(jié)點;順序節(jié)點的作用在分布式鎖的時候會用到;
  • 臨時順序節(jié)點;
目錄樹結(jié)構(gòu)

臨時節(jié)點常被用于心跳監(jiān)控,其生命周期依賴創(chuàng)建它們的會話,一旦會話結(jié)束或者連接超時,那么臨時節(jié)點就是失效被刪除,當(dāng)然也可以在會話有效的時候主動刪除。臨時節(jié)點是不能擁有子節(jié)點的。

二、ZK環(huán)境搭建

首先我們在阿里云上購買一臺ECS云服務(wù)器,選擇1C1G Linux CentOS7.5 X64的系統(tǒng),然后通過本地的SSH工具連接上去。

Zookeeper需要依賴Java運(yùn)行環(huán)境,所以,我們要確保目標(biāo)服務(wù)器上已經(jīng)安裝好JDK。假設(shè)下載的JDK位于/soft目錄下。

[root@iZuf6fkfr4guj30qo8g4woZ ~]# cd /soft/
[root@iZuf6fkfr4guj30qo8g4woZ soft]# tar -zxvf jre-8u351-linux-x64.tar.gz
......
[root@iZuf6fkfr4guj30qo8g4woZ soft]# vim /etc/profile

# 在最下面增加如下內(nèi)容
export JAVA_HOME=/soft/jre1.8.0_351
export PATH=$PATH:$JAVA_HOME/bin
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar

[root@iZuf6fkfr4guj30qo8g4woZ soft]# source /etc/profile
[root@iZuf6fkfr4guj30qo8g4woZ soft]# java -version
java version "1.8.0_351"
Java(TM) SE Runtime Environment (build 1.8.0_351-b10)
Java HotSpot(TM) 64-Bit Server VM (build 25.351-b10, mixed mode)

2.1 服務(wù)端的搭建和使用

然后我們從官網(wǎng)下載ZK,這里假設(shè)下載的ZK位于/soft目錄下,然后進(jìn)行ZK的解壓縮和配置。同時在該目錄下創(chuàng)建一個數(shù)據(jù)文件夾,用來存放ZK的數(shù)據(jù)。

[root@iZuf6fkfr4guj30qo8g4woZ soft]# tar -zxvf apache-zookeeper-3.5.6-bin.tar.gz
......
# 創(chuàng)建ZK存放數(shù)據(jù)的目錄
[root@iZuf6fkfr4guj30qo8g4woZ soft]# mkdir zkdata
[root@iZuf6fkfr4guj30qo8g4woZ soft]# cd apache-zookeeper-3.5.6-bin/conf/
# 根據(jù)示例配置復(fù)制一份正式生效的ZK配置
[root@iZuf6fkfr4guj30qo8g4woZ conf]# cp zoo_sample.cfg zoo.cfg
[root@iZuf6fkfr4guj30qo8g4woZ conf]# vim zoo.cfg

#修改ZK存放數(shù)據(jù)的目錄
dataDir=/soft/zkdata

以上我們就完成了ZK服務(wù)端的配置,就可以開始啟動了。

[root@iZuf6fkfr4guj30qo8g4woZ conf]# cd /soft/apache-zookeeper-3.5.6-bin/bin
[root@iZuf6fkfr4guj30qo8g4woZ bin]# ./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED

ZK服務(wù)端的常見操作命令如下:

  • 啟動命令,./zkServer.sh start

  • 停止命令,./zkServer.sh stop

  • 重啟命令,./zkServer.sh restart

  • 查看狀態(tài)命令,./zkServer.sh status

    [root@iZuf6fkfr4guj30qo8g4woZ bin]# ./zkServer.sh status
    ZooKeeper JMX enabled by default
    Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
    Client port found: 2181. Client address: localhost.
    Mode: standalone
    

PS:下面客戶端的連接需要保證服務(wù)器開通了2181端口給外部,否則訪問失敗。

2.2 客戶端的搭建和使用

同樣的,我們需要在目標(biāo)客戶端服務(wù)器上下載和解壓縮下載到的Zookeeper壓縮包,如果客戶端和服務(wù)端是一起的,那就直接在/opt/zooKeeper/apache-zooKeeper-3.5.6-bin/bin/目錄下啟動客戶端就行了。常見的客戶端命令如下:

  • 連接ZK服務(wù)器,./zkCli.sh -server ip:port,本機(jī)的話就是localhost:2181;
  • 斷開連接,quit
  • 創(chuàng)建節(jié)點,create /path value;
  • 設(shè)置節(jié)點中的數(shù)據(jù)值,set /path value;
  • 獲取節(jié)點值,get /path
  • 顯示指定目錄下的節(jié)點,ls /path;
  • 刪除單個節(jié)點,delete /path,當(dāng)前節(jié)點有子節(jié)點時會刪除失敗;
  • 刪除帶有子節(jié)點的節(jié)點,deleteall /path
  • 獲取幫助,help
  • 創(chuàng)建臨時節(jié)點,create -e /path value;
  • 創(chuàng)建順序節(jié)點,create -s /path value;
  • 查詢節(jié)點詳細(xì)信息,ls -s /path;

三、使用JavaAPI Curator

Curator是Apache提供的一個操作ZK服務(wù)器的Java客戶端庫,其大大簡化了對ZK服務(wù)器節(jié)點和數(shù)據(jù)的操作,我們將在下面來介紹下其基本的使用方法。首先我們從start.spring.io網(wǎng)站上生成一個最基本的SpringBoot工程,然后在其中引入Curator的依賴。

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.0.0</version>
</dependency>

3.1 基本操作

在如下的代碼中,我們使用Curator來操作Zookeeper實現(xiàn)了開啟連接、創(chuàng)建節(jié)點、查詢節(jié)點、設(shè)置節(jié)點數(shù)據(jù)、刪除節(jié)點的操作。

@Slf4j
public class CuratorBaseTest {

    private CuratorFramework curatorClient;

    @Before
    public void testConnect(){
        /**
         * 重試策略,最多嘗試10次,每次間隔3秒
         */
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
        // 使用建造者模式創(chuàng)建ZK-Curator客戶端
        curatorClient = CuratorFrameworkFactory.builder()
                .connectString("x.x.x.x:2181")
                .sessionTimeoutMs(60*1000)
                .connectionTimeoutMs(15*1000)
                .retryPolicy(retryPolicy)
                .namespace("zhangxun")
                .build();

        curatorClient.start();
    }

    /**
     * 創(chuàng)建節(jié)點
     * 如果創(chuàng)建節(jié)點,沒有指定數(shù)據(jù),則默認(rèn)將當(dāng)前客戶端的ip作為數(shù)據(jù)存儲
     */
    @Test
    public void testCreateNode() throws Exception {
        String path1 = curatorClient.create().forPath("/node1");
        // /node1
        log.info("{}", path1);

        String path2 = curatorClient.create().forPath("/node2", "node2".getBytes(StandardCharsets.UTF_8));
        // /node2
        log.info("{}", path2);

        // 節(jié)點的默認(rèn)類型為持久型,這里指定為臨時節(jié)點
        String path3 = curatorClient.create().withMode(CreateMode.EPHEMERAL).forPath("/node3");
        // /node3
        log.info("{}", path3);

        // 如果父節(jié)點不存在,則創(chuàng)建父節(jié)點
        String path4 = curatorClient.create().creatingParentsIfNeeded().forPath("/node4/branch1");
        // /node4/branch1
        log.info("{}", path4);
    }
    
    /**
     * [zk: localhost:2181(CONNECTED) 7] ls -R /zhangxun
     * /zhangxun
     * /zhangxun/node1
     * /zhangxun/node2
     * /zhangxun/node4
     * /zhangxun/node4/branch1
     */

    /**
     * 查詢節(jié)點
     */
    @Test
    public void testGetNode() throws Exception{
        // 查看節(jié)點中的數(shù)據(jù)
        byte[] data = curatorClient.getData().forPath("/node1");
        // x.x.x.x 默認(rèn)的IP地址
        log.info("{}", new String(data));

        // 查看某個路徑下面的所有子節(jié)點
        List<String> subNodes = curatorClient.getChildren().forPath("/node4");
        for(String node : subNodes){
            // branch1
            log.info("{}", node);
        }

        // 查看某個節(jié)點的詳細(xì)信息,ls -s
        Stat nodeStatus = new Stat();
        // 0,0,0,0,0,0,0,0,0,0,0
        log.info("{}", nodeStatus);
        curatorClient.getData().storingStatIn(nodeStatus).forPath("/node2");
        // 7,7,1677836202355,1677836202355,0,0,0,0,5,0,7
        log.info("{}", nodeStatus);
    }

    /**
     * 設(shè)置節(jié)點數(shù)據(jù)
     */
    @Test
    public void testSetNode() throws Exception {
        curatorClient.setData().forPath("/node1", "node1".getBytes(StandardCharsets.UTF_8));
        byte[] data = curatorClient.getData().forPath("/node1");
        // node1
        log.info("{}", new String(data));
    }

    /**
     * 刪除節(jié)點
     */
    @Test
    public void testDeleteNode() throws Exception {
        // 刪除指定節(jié)點,且該節(jié)點沒有子節(jié)點
        curatorClient.delete().forPath("/node1");

        // 刪除帶有子節(jié)點的指定節(jié)點,其子節(jié)點一并刪除
        curatorClient.delete().deletingChildrenIfNeeded().forPath("/node4");

        // 帶重試機(jī)制的保證刪除方法
        curatorClient.delete().guaranteed().forPath("node2");

        curatorClient.delete().guaranteed().inBackground(new BackgroundCallback() {
            @Override
            public void processResult(CuratorFramework client, CuratorEvent event) throws Exception {
                log.info("節(jié)點刪除成功:{}", event);
            }
        }).forPath("/node3");
    }

    @After
    public void close(){
        if (curatorClient != null) {
            curatorClient.close();
        }
    }
}

3.2 監(jiān)聽事件

ZK允許用戶在指定的節(jié)點或其子節(jié)點上注冊監(jiān)聽器,當(dāng)這些節(jié)點上發(fā)生變更時,ZK會將事件通知給所有注冊的客戶端,實現(xiàn)發(fā)布訂閱的功能,這也是ZK實現(xiàn)分布式應(yīng)用協(xié)調(diào)服務(wù)功能的重要特性。ZK提供了三種類型的監(jiān)聽器:

  • NodeCache,只是監(jiān)聽某個特定的節(jié)點;
  • PathChildrenCache,監(jiān)聽某個節(jié)點的所有子節(jié)點;
  • TreeCache,監(jiān)聽某個節(jié)點及其所有子節(jié)點;

監(jiān)聽事件需要Curator另一個組件的支持,因此需要在pom中引入該依賴。

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.0.0</version>
</dependency>

如下是一個使用Curator實現(xiàn)對ZK某個節(jié)點或其子節(jié)點進(jìn)行監(jiān)聽的例子:

@Slf4j
public class CuratorWatcherTest {

    private CuratorFramework curatorClient;

    @Before
    public void testConnect(){
        /**
         * 重試策略,最多嘗試10次,每次間隔3秒
         */
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
        // 使用建造者模式創(chuàng)建ZK-Curator客戶端
        curatorClient = CuratorFrameworkFactory.builder()
                .connectString("47.100.139.15:2181")
                .sessionTimeoutMs(60*1000)
                .connectionTimeoutMs(15*1000)
                .retryPolicy(retryPolicy)
                .namespace("zhangxun")
                .build();

        curatorClient.start();
    }

    /**
     * 給指定的節(jié)點注冊監(jiān)聽器
     */
    @Test
    public void testWatcher1() throws Exception {
        // 創(chuàng)建需要監(jiān)聽節(jié)點的NodeCache對象
        NodeCache nodeCache = new NodeCache(curatorClient, "/node1");
        // 注冊監(jiān)聽
        nodeCache.getListenable().addListener(new NodeCacheListener() {
            @Override
            public void nodeChanged() throws Exception {
                log.info("監(jiān)聽的節(jié)點發(fā)生了變化");
                byte[] data = nodeCache.getCurrentData().getData();
                log.info("節(jié)點變化后的內(nèi)容為:{}", new String(data));
            }
        });

        nodeCache.start(true);

        // 等待60秒,手動去更改/node1上的數(shù)據(jù),觸發(fā)如上回調(diào)函數(shù)
        Thread.sleep(60*1000);
    }

    /**
     * [zk: localhost:2181(CONNECTED) 9] set /zhangxun/node1 123
     *
     * 17:58:04.817 [main-EventThread] INFO com.example.zkcurator.CuratorWatcherTest - 監(jiān)聽的節(jié)點發(fā)生了變化
     * 17:58:04.817 [main-EventThread] INFO com.example.zkcurator.CuratorWatcherTest - 節(jié)點變化后的內(nèi)容為:123
     */

    /**
     * 給指定的節(jié)點的所有子節(jié)點注冊監(jiān)聽器
     */
    @Test
    public void testWatcher2() throws Exception {
        PathChildrenCache pathChildrenCache = new PathChildrenCache(curatorClient, "/node4", true);
        pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
            @Override
            public void childEvent(CuratorFramework curatorFramework, PathChildrenCacheEvent pathChildrenCacheEvent) throws Exception {
                log.info("監(jiān)聽的子節(jié)點發(fā)生了變化:{}", pathChildrenCacheEvent);
                PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType();
                byte[] data = pathChildrenCacheEvent.getData().getData();
                log.info("監(jiān)聽的子節(jié)點發(fā)生了{(lán)}類型的變更,變更后的內(nèi)容為:{}", type, new String(data));
            }
        });

        pathChildrenCache.start();

        // 等待60秒,手動去更改/node4的任意一個子節(jié)點上的數(shù)據(jù),觸發(fā)如上回調(diào)函數(shù)
        Thread.sleep(60*1000);
    }

    /**
     * 給指定的節(jié)點及其所有子節(jié)點注冊監(jiān)聽器
     */
    @Test
    public void testWatcher3() throws Exception {
        TreeCache treeCache = new TreeCache(curatorClient, "/node4");
        treeCache.getListenable().addListener(new TreeCacheListener() {
            @Override
            public void childEvent(CuratorFramework curatorFramework, TreeCacheEvent treeCacheEvent) throws Exception {
                log.info("監(jiān)聽的子節(jié)點發(fā)生了變化:{}", treeCacheEvent);
                TreeCacheEvent.Type type = treeCacheEvent.getType();
                byte[] data = treeCacheEvent.getData().getData();
                log.info("監(jiān)聽的節(jié)點或其子節(jié)點發(fā)生了{(lán)}類型的變更,變更后的內(nèi)容為:{}", type, new String(data));
            }
        });

        treeCache.start();

        // 等待60秒,手動去更改/node4節(jié)點或者其任意一個子節(jié)點上的數(shù)據(jù),觸發(fā)如上回調(diào)函數(shù)
        Thread.sleep(60*1000);
    }

    @After
    public void close(){
        if (curatorClient != null) {
            curatorClient.close();
        }
    }
}

那么ZK是如何利用監(jiān)聽機(jī)制和臨時節(jié)點來實現(xiàn)分布式應(yīng)用協(xié)調(diào)服務(wù)功能的呢?

假設(shè)客戶端集群HDFS或者Kafaka,它們都連上了ZK服務(wù)器,各個節(jié)點都注冊了臨時節(jié)點在ZK服務(wù)器上,同時對其它節(jié)點注冊了監(jiān)聽機(jī)制。這樣如果集群中有服務(wù)器節(jié)點上下線,其它節(jié)點都能及時收到變更通知,從而再利用ZK集群進(jìn)行選舉,保證了客戶端自身集群的穩(wěn)定和可用。

3.3 分布式鎖實現(xiàn)

分布式鎖不同于線程鎖,線程鎖是為了解決在一個Java進(jìn)程內(nèi),多個線程并發(fā)訪問共享資源時同步的問題,而分布式鎖則是解決集群環(huán)境下,多個Java進(jìn)程間訪問分布式共享資源的同步問題。

同樣的,要實現(xiàn)分布式鎖也需要引入curator-recipes依賴。下面是一個分布式場景下(用多線程來模擬分布式場景)搶購書籍的例子,你也可以不使用多線程,而是啟動多個應(yīng)用進(jìn)程來模擬。

@Slf4j
public class BookSale implements Runnable{
    /**
     * 假設(shè)書本售賣總量為10本,模擬分布式搶購資源
     */
    private int count = 10;
    /**
     * 定義分布式鎖變量
     */
    private InterProcessLock lock;

    /**
     * 在構(gòu)造函數(shù)中進(jìn)行ZK連接的獲取及分布式鎖的初始化
     */
    public BookSale(){
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(3000, 10);
        CuratorFramework curatorClient = CuratorFrameworkFactory.builder()
                .connectString("47.100.139.15:2181")
                .sessionTimeoutMs(60*1000)
                .connectionTimeoutMs(15*1000)
                .retryPolicy(retryPolicy)
                .build();
        curatorClient.start();

        lock = new InterProcessMutex(curatorClient, "/lock");
    }


    @Override
    public void run() {
        while (true) {
            // 無限循環(huán)進(jìn)行搶購
            try {
                // 第一步先上鎖確保了下面操作的同步執(zhí)行,當(dāng)獲取不到鎖時最多等待3秒
                lock.acquire(3, TimeUnit.SECONDS);
                if(count <= 0){
                    log.info("{}-您來晚了,所有書本都被搶完!", Thread.currentThread());
                    return;
                }
                count--;
                log.info("{}-恭喜您搶到了1本,還剩{}本,繼續(xù)加油!", Thread.currentThread(), count);
            } catch (Exception e) {
                log.warn("{}-本次搶購失敗,還剩{}本,繼續(xù)加油: {}", Thread.currentThread() ,count, e);
            } finally {
                try {
                    lock.release();
                } catch (Exception e) {
                    log.error("釋放鎖失敗:{}", e);
                }
            }
        }
    }

    /**
     * Thread[Thread-3,5,main]-恭喜您搶到了1本,還剩9本,繼續(xù)加油!
     * Thread[Thread-1,5,main]-恭喜您搶到了1本,還剩8本,繼續(xù)加油!
     * Thread[Thread-2,5,main]-恭喜您搶到了1本,還剩7本,繼續(xù)加油!
     * Thread[Thread-3,5,main]-恭喜您搶到了1本,還剩6本,繼續(xù)加油!
     * Thread[Thread-1,5,main]-恭喜您搶到了1本,還剩5本,繼續(xù)加油!
     * Thread[Thread-2,5,main]-恭喜您搶到了1本,還剩4本,繼續(xù)加油!
     * Thread[Thread-3,5,main]-恭喜您搶到了1本,還剩3本,繼續(xù)加油!
     * Thread[Thread-1,5,main]-恭喜您搶到了1本,還剩2本,繼續(xù)加油!
     * Thread[Thread-2,5,main]-恭喜您搶到了1本,還剩1本,繼續(xù)加油!
     * Thread[Thread-3,5,main]-恭喜您搶到了1本,還剩0本,繼續(xù)加油!
     * Thread[Thread-1,5,main]-您來晚了,所有書本都被搶完!
     * Thread[Thread-2,5,main]-您來晚了,所有書本都被搶完!
     * Thread[Thread-3,5,main]-您來晚了,所有書本都被搶完!
     */
}
public class BookMain {

    public static void main(String[] args) {

        BookSale bookSale = new BookSale();

        /**
         * 模擬三個獨(dú)立的人同時搶購書籍
         */
        Thread tom = new Thread(bookSale);
        Thread jack = new Thread(bookSale);
        Thread lucy = new Thread(bookSale);

        tom.start();
        jack.start();
        lucy.start();
    }

}

如上案例中,使用curator來實現(xiàn)分布式鎖非常的簡單,那它是如何做到的呢,背后的工作機(jī)制又是怎樣的呢?

  • 1.客戶端在獲取鎖時,會在指定目錄下創(chuàng)建臨時順序節(jié)點;
  • 2.然后獲取該目錄下所有子節(jié)點,判斷自己創(chuàng)建的子節(jié)點是否是最小的,如果是就代表獲取到了鎖,執(zhí)行業(yè)務(wù)邏輯,結(jié)束時刪除自己創(chuàng)建的節(jié)點;
  • 3.如果發(fā)現(xiàn)自己創(chuàng)建的子節(jié)點不是最小的,那代表當(dāng)前沒有獲取到鎖,同時注冊監(jiān)聽器到最小的那個子節(jié)點上,監(jiān)聽其刪除事件;
  • 4.當(dāng)最小的那個子節(jié)點被刪除時,代表有人釋放了鎖,重復(fù)如上步驟2;

在如上步驟中,客戶端創(chuàng)建的節(jié)點有兩個特點:

  • 臨時的,確??蛻舳嗽诰W(wǎng)絡(luò)失?。〞捠В┣闆r下,不會長時間占用鎖,使得其他客戶端可以繼續(xù)競爭鎖;
  • 順序的,保證一次只有一個客戶端能獲取到鎖;

當(dāng)然,在學(xué)習(xí)線程鎖的時候,我們有講過鎖的種類,Curator中實現(xiàn)的分布式鎖也有多種,供了解。

  • InterProcessSemaphoreMutex,分布式非可重入排它鎖;意思是在任何時刻不會有兩個客戶端同時持有鎖,該客戶端在擁有鎖的同時,也不能多次獲取鎖;
  • InterProcessMutex,分布式可重入排它鎖;意思是在任何時刻不會有兩個客戶端同時持有鎖,和JDK的ReentrantLock類似, 意味著該客戶端在擁有鎖的同時,可以多次獲取鎖,不會被阻塞;
  • InterProcessReadWriteLock,分布式可重入讀寫鎖;參考JDK中的讀寫鎖;
  • InterProcessSemaphoreV2,共享信號量;參考JDK中的信號量;
  • InterProcessMultiLock,多共享分布式鎖,多個鎖作為一個鎖,可以同時在多個資源上加鎖。一個維護(hù)多個鎖對象的容器。當(dāng)調(diào)用 acquire()時,獲取容器中所有的鎖對象,請求失敗時,釋放所有鎖對象。同樣調(diào)用release()也會釋放所有的鎖;

四、ZK集群搭建和使用

4.1 集群搭建

ZK中的角色總共分為三種:

  • 領(lǐng)導(dǎo)者,Leader,負(fù)責(zé)處理事務(wù)類的請求,是集群內(nèi)部各個服務(wù)器的調(diào)度者,一個集群只能有一個Leader;
  • 跟隨著,F(xiàn)ollower,負(fù)責(zé)處理非事務(wù)類的請求,收到事務(wù)類請求時會轉(zhuǎn)發(fā)給Leader進(jìn)行處理;同時參與集群Leader的選舉投票,有可能成為新的Leader;
  • 觀察者,Observer,負(fù)責(zé)處理非事務(wù)類的請求,不參加投票,不是必須的;
集群工作示意圖

客戶端在設(shè)置連接ZK集群的時候,可以將集群所有服務(wù)器的IP+端口都配置進(jìn)去,比如Curator就會任意選擇一個ZK服務(wù)器進(jìn)行連接,任何一臺服務(wù)器上的數(shù)據(jù)都是相同的,當(dāng)前連接的服務(wù)器如果宕機(jī)的話,Curator會自動幫我們重連尋找集群中其它可用的服務(wù)器。

我們現(xiàn)在已經(jīng)有了一臺云主機(jī)了,為了配置成為ZK集群,再購買兩臺(需要在同一個局域網(wǎng)內(nèi)),同時按照2.1節(jié)中的步驟配置好java環(huán)境和ZK的配置。需要額外配置的內(nèi)容有如下:

  • 在每臺機(jī)器的zk數(shù)據(jù)目錄下,也就是我們創(chuàng)建的zkdata目錄下,創(chuàng)建myid文件,指定各自的服務(wù)器ID,比如1、2、3;

    # node1(172.24.38.213)
    echo 1 > /soft/zkdata/myid
    # node2(172.24.38.214)
    echo 2 > /soft/zkdata/myid
    # node3(172.24.38.212)
    echo 3 > /soft/zkdata/myid
    
  • 修改每臺機(jī)器的zk配置文件,將集群中所有的機(jī)器地址配置在里面;

    vim /soft/apache-zookeeper-3.5.6-bin/conf/zoo.cfg
    
    # server.id=服務(wù)器IP地址:服務(wù)器之間的通信端口:服務(wù)器之間的選舉投票端口
    server.1=172.24.38.213:2881:3881
    server.2=172.24.38.214:2881:3881
    server.3=172.24.38.212:2881:3881
    

做好如上配置后,我們就可以開始啟動我們的ZK集群了:

cd /soft/apache-zookeeper-3.5.6-bin/bin

# node1(172.24.38.213)
./zkServer.sh start
# node2(172.24.38.214)
./zkServer.sh start
# node3(172.24.38.212)
./zkServer.sh start

4.2 集群的使用

在剛才的集群中node1是Leader,node2和node3是Follower,我們模擬node1異常服務(wù)終止,那么會發(fā)生什么呢?

# node1(172.24.38.213)
./zkServer.sh stop
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED
# node2(172.24.38.214)
./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower
# node3(172.24.38.212)
./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader

可以看到,經(jīng)過ZAB協(xié)議的重新選舉,node3成為了新的Leader,此時如果再停掉任意一個節(jié)點,整個集群將不可用:

# node2(172.24.38.214)
./zkServer.sh stop
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Stopping zookeeper ... STOPPED
# node3(172.24.38.212)
./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Error contacting service. It is probably not running.

這是因為集群中正常節(jié)點的數(shù)量沒有超過總節(jié)點的一半,此時我們重啟node1,會發(fā)現(xiàn)集群會恢復(fù)正常,且產(chǎn)生了新的Leader:

# node1(172.24.38.213)
./zkServer.sh start
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Starting zookeeper ... STARTED
# node1(172.24.38.213)
./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: follower
# node3(172.24.38.212)
./zkServer.sh status
ZooKeeper JMX enabled by default
Using config: /soft/apache-zookeeper-3.5.6-bin/bin/../conf/zoo.cfg
Client port found: 2181. Client address: localhost.
Mode: leader

五、算法原理介紹

文本是ZK的入門介紹,這些算法理論方面的知識了解即可,暫不做展開描述,后面會有專門的介紹。

5.1 拜占庭將軍問題

什么是拜占庭將軍問題 - 知乎 (zhihu.com)

拜占庭將軍問題 (The Byzantine Generals Problem) - 知乎 (zhihu.com)

拜占庭將軍問題是用來為描述分布式系統(tǒng)一致性問題而情景化的一個著名例子,該問題是分布式領(lǐng)域最為復(fù)雜的問題,通常解決該問題的算法分為兩類:

  • 故障容錯算法,解決的是分布式系統(tǒng)中存在故障, 但不存在惡意攻擊的場景下的共識問題。也就是說, 在該場景下可能存在消息丟失, 消息重復(fù), 但不存在消息被篡改或偽造的場景。一般用于局域網(wǎng)場景下的分布式系統(tǒng), 屬于此類的常見算法有Paxos算法, Raft算法, ZAB協(xié)議等;
  • 拜占庭容錯算法,可以解決分布式系統(tǒng)中既存在故障, 又存在惡意攻擊場景下的共識問題。 一般用于互聯(lián)網(wǎng)場景下的分布式系統(tǒng), 如在數(shù)字貨幣的區(qū)塊鏈技術(shù)中,屬于此類的常見算法有PBFT算法, PoW算法等。

5.2 Paxos算法

Paxos算法詳解 - 知乎 (zhihu.com)

Paxos算法是用來解決分布式系統(tǒng)中信息一致性問題的算法之一,其實現(xiàn)比較復(fù)雜,在實際中很少直接使用。

5.3 ZAB協(xié)議

分布式共識算法 Zab 簡解 - 知乎 (zhihu.com)

ZAB協(xié)議是基于Multi-Paxos改造和優(yōu)化后的算法,Zookeeper集群的選舉過程就是該算法的體現(xiàn);

5.3 Raft協(xié)議

Raft算法詳解 - 知乎 (zhihu.com)

Raft協(xié)議是將Paxos優(yōu)化和簡化之后的算法,Nacos集群的選舉過程就是該算法的體現(xiàn);

5.5 CAP原理

談?wù)劮植际较到y(tǒng)的CAP理論 - 知乎 (zhihu.com)

CAP分別代表一致性、可用性、分區(qū)容錯性,在分布式系統(tǒng)中,P代表的分區(qū)容錯性是必須的,所以一般都是在CP和AP之間做出選擇。Zookeeper因為ZAB協(xié)議的關(guān)系,不能保證可用性,因此屬于CP陣營。

六、注冊中心和配置中心

阿里一面:Zookeeper用作注冊中心的原理,你知道嗎? - 知乎 (zhihu.com)

現(xiàn)在主流的注冊中心有很多,比如SpringCloud Eureka,Nacos,Apollo,Consul等,為什么Zookeeper明明也可以作為注冊中心和配置中心,卻鮮有人用呢?

Zookeeper作為注冊中心的注冊流程大致如下:

  • 服務(wù)提供者啟動時,會將其服務(wù)名稱、ip地址等信息注冊到Zookeeper的某個路徑上,相同的服務(wù)使用同一個路徑,不同的服務(wù)有各自的路徑;
  • 服務(wù)消費(fèi)者在第一次調(diào)用服務(wù)時,會通過Zookeeper找到相應(yīng)的服務(wù)的IP地址列表,并緩存到本地,以供后續(xù)使用。當(dāng)消費(fèi)者調(diào)用服務(wù)時,不會再去請求注冊中心,而是直接通過負(fù)載均衡算法從IP列表中取一個服務(wù)提供者的服務(wù)器調(diào)用服務(wù)。
  • 當(dāng)服務(wù)提供者的某臺服務(wù)器宕機(jī)、下線、上線時,相應(yīng)的ip會從服務(wù)提供者IP列表中移除或者新增,此時,Zookeeper會依靠其自身的Watcher機(jī)制將新的服務(wù)IP地址列表發(fā)送給服務(wù)消費(fèi)者機(jī)器,緩存在消費(fèi)者本機(jī);
  • Zookeeper自身具有心跳檢測機(jī)制,會定期向服務(wù)提供者發(fā)送請求,如果長時間沒有回應(yīng),就認(rèn)為該服務(wù)已經(jīng)下線,會將其剔除;

可以看到Zookeeper的工作機(jī)制和主流的注冊中心沒啥大的區(qū)別,其不太適合作為注冊中心的原因是其底層核心算法ZAB協(xié)議導(dǎo)致的,ZAB是CP陣營的算法,雖然提供了強(qiáng)一致性,但是在選舉的過程中無法保證可用性(其重新選舉和數(shù)據(jù)同步將耗費(fèi)一定的時間,在此期間服務(wù)不可用),對于微服務(wù)體系來說是不可接收的。

同理,Zookeeper作為配置中心也存在同樣的問題,因此不是不能用,而是不太適合,有其它更好的方案可供選擇。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容