分布式协调服务ZooKeeper使用入门

一、ZK简介

在大数据技术体系内,很多技术框架都是用动物的名字命名的,比如Hadoop(大象)、Hive(蜜蜂)、Pig(小猪)。大数据服务通常都是分布式的,多个节点之间角色不同,在特殊情况下角色还会发生转换,比如HDFS中的NameNode和SidebyNameNode,YARN中的ResourceManager和Standby ResourceManager,Spark中的Mastser和 Standby Master等等,这些角色(节点)在发生故障的时候,如何确保集群能正常工作是一个很重要的问题,Zookeeper的出现就是为了解决这个问题的。

Zookeeper是Apache的开源项目,按照官网的描述来定义:Zookeeper是一个支持分布式部署的,开源的,分布式应用程序协调服务,主要提供集群管理、分布式锁、注册中心和配置中心的功能。

Zookeeper本质上是提供一个树形目录服务,类似于Linux操作系统的目录树文件系统,使得其下节点有一个层次化的结构。其中的每一个节点都被成为ZNode,每个节点都可以拥有自己的子节点,也可以在节点本身上存储少量(1MB)的数据信息。节点的分类如下:

  • 持久化节点,在ZK重启后仍然存在的节点;
  • 临时节点,存在内存中的具有有效期的节点,有效期过后或者重启后就不存在了;
  • 持久化顺序节点;顺序节点的作用在分布式锁的时候会用到;
  • 临时顺序节点;
目录树结构

临时节点常被用于心跳监控,其生命周期依赖创建它们的会话,一旦会话结束或者连接超时,那么临时节点就是失效被删除,当然也可以在会话有效的时候主动删除。临时节点是不能拥有子节点的。

二、ZK环境搭建

首先我们在阿里云上购买一台ECS云服务器,选择1C1G Linux CentOS7.5 X64的系统,然后通过本地的SSH工具连接上去。

Zookeeper需要依赖Java运行环境,所以,我们要确保目标服务器上已经安装好JDK。假设下载的JDK位于/soft目录下。

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

# 在最下面增加如下内容
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 服务端的搭建和使用

然后我们从官网下载ZK,这里假设下载的ZK位于/soft目录下,然后进行ZK的解压缩和配置。同时在该目录下创建一个数据文件夹,用来存放ZK的数据。

[root@iZuf6fkfr4guj30qo8g4woZ soft]# tar -zxvf apache-zookeeper-3.5.6-bin.tar.gz
......
# 创建ZK存放数据的目录
[root@iZuf6fkfr4guj30qo8g4woZ soft]# mkdir zkdata
[root@iZuf6fkfr4guj30qo8g4woZ soft]# cd apache-zookeeper-3.5.6-bin/conf/
# 根据示例配置复制一份正式生效的ZK配置
[root@iZuf6fkfr4guj30qo8g4woZ conf]# cp zoo_sample.cfg zoo.cfg
[root@iZuf6fkfr4guj30qo8g4woZ conf]# vim zoo.cfg

#修改ZK存放数据的目录
dataDir=/soft/zkdata

以上我们就完成了ZK服务端的配置,就可以开始启动了。

[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服务端的常见操作命令如下:

  • 启动命令,./zkServer.sh start

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

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

  • 查看状态命令,./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:下面客户端的连接需要保证服务器开通了2181端口给外部,否则访问失败。

2.2 客户端的搭建和使用

同样的,我们需要在目标客户端服务器上下载和解压缩下载到的Zookeeper压缩包,如果客户端和服务端是一起的,那就直接在/opt/zooKeeper/apache-zooKeeper-3.5.6-bin/bin/目录下启动客户端就行了。常见的客户端命令如下:

  • 连接ZK服务器,./zkCli.sh -server ip:port,本机的话就是localhost:2181
  • 断开连接,quit
  • 创建节点,create /path value
  • 设置节点中的数据值,set /path value
  • 获取节点值,get /path
  • 显示指定目录下的节点,ls /path
  • 删除单个节点,delete /path,当前节点有子节点时会删除失败;
  • 删除带有子节点的节点,deleteall /path
  • 获取帮助,help
  • 创建临时节点,create -e /path value
  • 创建顺序节点,create -s /path value
  • 查询节点详细信息,ls -s /path

三、使用JavaAPI Curator

Curator是Apache提供的一个操作ZK服务器的Java客户端库,其大大简化了对ZK服务器节点和数据的操作,我们将在下面来介绍下其基本的使用方法。首先我们从start.spring.io网站上生成一个最基本的SpringBoot工程,然后在其中引入Curator的依赖。


    org.apache.curator
    curator-framework
    4.0.0

3.1 基本操作

在如下的代码中,我们使用Curator来操作Zookeeper实现了开启连接、创建节点、查询节点、设置节点数据、删除节点的操作。

@Slf4j
public class CuratorBaseTest {

    private CuratorFramework curatorClient;

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

        curatorClient.start();
    }

    /**
     * 创建节点
     * 如果创建节点,没有指定数据,则默认将当前客户端的ip作为数据存储
     */
    @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);

        // 节点的默认类型为持久型,这里指定为临时节点
        String path3 = curatorClient.create().withMode(CreateMode.EPHEMERAL).forPath("/node3");
        // /node3
        log.info("{}", path3);

        // 如果父节点不存在,则创建父节点
        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
     */

    /**
     * 查询节点
     */
    @Test
    public void testGetNode() throws Exception{
        // 查看节点中的数据
        byte[] data = curatorClient.getData().forPath("/node1");
        // x.x.x.x 默认的IP地址
        log.info("{}", new String(data));

        // 查看某个路径下面的所有子节点
        List subNodes = curatorClient.getChildren().forPath("/node4");
        for(String node : subNodes){
            // branch1
            log.info("{}", node);
        }

        // 查看某个节点的详细信息,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);
    }

    /**
     * 设置节点数据
     */
    @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));
    }

    /**
     * 删除节点
     */
    @Test
    public void testDeleteNode() throws Exception {
        // 删除指定节点,且该节点没有子节点
        curatorClient.delete().forPath("/node1");

        // 删除带有子节点的指定节点,其子节点一并删除
        curatorClient.delete().deletingChildrenIfNeeded().forPath("/node4");

        // 带重试机制的保证删除方法
        curatorClient.delete().guaranteed().forPath("node2");

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

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

3.2 监听事件

ZK允许用户在指定的节点或其子节点上注册监听器,当这些节点上发生变更时,ZK会将事件通知给所有注册的客户端,实现发布订阅的功能,这也是ZK实现分布式应用协调服务功能的重要特性。ZK提供了三种类型的监听器:

  • NodeCache,只是监听某个特定的节点;
  • PathChildrenCache,监听某个节点的所有子节点;
  • TreeCache,监听某个节点及其所有子节点;

监听事件需要Curator另一个组件的支持,因此需要在pom中引入该依赖。


    org.apache.curator
    curator-recipes
    4.0.0

如下是一个使用Curator实现对ZK某个节点或其子节点进行监听的例子:

@Slf4j
public class CuratorWatcherTest {

    private CuratorFramework curatorClient;

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

        curatorClient.start();
    }

    /**
     * 给指定的节点注册监听器
     */
    @Test
    public void testWatcher1() throws Exception {
        // 创建需要监听节点的NodeCache对象
        NodeCache nodeCache = new NodeCache(curatorClient, "/node1");
        // 注册监听
        nodeCache.getListenable().addListener(new NodeCacheListener() {
            @Override
            public void nodeChanged() throws Exception {
                log.info("监听的节点发生了变化");
                byte[] data = nodeCache.getCurrentData().getData();
                log.info("节点变化后的内容为:{}", new String(data));
            }
        });

        nodeCache.start(true);

        // 等待60秒,手动去更改/node1上的数据,触发如上回调函数
        Thread.sleep(60*1000);
    }

    /**
     * [zk: localhost:2181(CONNECTED) 9] set /zhangxun/node1 123
     *
     * 17:58:04.817 [main-EventThread] INFO com.example.zkcurator.CuratorWatcherTest - 监听的节点发生了变化
     * 17:58:04.817 [main-EventThread] INFO com.example.zkcurator.CuratorWatcherTest - 节点变化后的内容为:123
     */

    /**
     * 给指定的节点的所有子节点注册监听器
     */
    @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("监听的子节点发生了变化:{}", pathChildrenCacheEvent);
                PathChildrenCacheEvent.Type type = pathChildrenCacheEvent.getType();
                byte[] data = pathChildrenCacheEvent.getData().getData();
                log.info("监听的子节点发生了{}类型的变更,变更后的内容为:{}", type, new String(data));
            }
        });

        pathChildrenCache.start();

        // 等待60秒,手动去更改/node4的任意一个子节点上的数据,触发如上回调函数
        Thread.sleep(60*1000);
    }

    /**
     * 给指定的节点及其所有子节点注册监听器
     */
    @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("监听的子节点发生了变化:{}", treeCacheEvent);
                TreeCacheEvent.Type type = treeCacheEvent.getType();
                byte[] data = treeCacheEvent.getData().getData();
                log.info("监听的节点或其子节点发生了{}类型的变更,变更后的内容为:{}", type, new String(data));
            }
        });

        treeCache.start();

        // 等待60秒,手动去更改/node4节点或者其任意一个子节点上的数据,触发如上回调函数
        Thread.sleep(60*1000);
    }

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

那么ZK是如何利用监听机制和临时节点来实现分布式应用协调服务功能的呢?

假设客户端集群HDFS或者Kafaka,它们都连上了ZK服务器,各个节点都注册了临时节点在ZK服务器上,同时对其它节点注册了监听机制。这样如果集群中有服务器节点上下线,其它节点都能及时收到变更通知,从而再利用ZK集群进行选举,保证了客户端自身集群的稳定和可用。

3.3 分布式锁实现

分布式锁不同于线程锁,线程锁是为了解决在一个Java进程内,多个线程并发访问共享资源时同步的问题,而分布式锁则是解决集群环境下,多个Java进程间访问分布式共享资源的同步问题。

同样的,要实现分布式锁也需要引入curator-recipes依赖。下面是一个分布式场景下(用多线程来模拟分布式场景)抢购书籍的例子,你也可以不使用多线程,而是启动多个应用进程来模拟。

@Slf4j
public class BookSale implements Runnable{
    /**
     * 假设书本售卖总量为10本,模拟分布式抢购资源
     */
    private int count = 10;
    /**
     * 定义分布式锁变量
     */
    private InterProcessLock lock;

    /**
     * 在构造函数中进行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) {
            // 无限循环进行抢购
            try {
                // 第一步先上锁确保了下面操作的同步执行,当获取不到锁时最多等待3秒
                lock.acquire(3, TimeUnit.SECONDS);
                if(count <= 0){
                    log.info("{}-您来晚了,所有书本都被抢完!", Thread.currentThread());
                    return;
                }
                count--;
                log.info("{}-恭喜您抢到了1本,还剩{}本,继续加油!", Thread.currentThread(), count);
            } catch (Exception e) {
                log.warn("{}-本次抢购失败,还剩{}本,继续加油: {}", Thread.currentThread() ,count, e);
            } finally {
                try {
                    lock.release();
                } catch (Exception e) {
                    log.error("释放锁失败:{}", e);
                }
            }
        }
    }

    /**
     * Thread[Thread-3,5,main]-恭喜您抢到了1本,还剩9本,继续加油!
     * Thread[Thread-1,5,main]-恭喜您抢到了1本,还剩8本,继续加油!
     * Thread[Thread-2,5,main]-恭喜您抢到了1本,还剩7本,继续加油!
     * Thread[Thread-3,5,main]-恭喜您抢到了1本,还剩6本,继续加油!
     * Thread[Thread-1,5,main]-恭喜您抢到了1本,还剩5本,继续加油!
     * Thread[Thread-2,5,main]-恭喜您抢到了1本,还剩4本,继续加油!
     * Thread[Thread-3,5,main]-恭喜您抢到了1本,还剩3本,继续加油!
     * Thread[Thread-1,5,main]-恭喜您抢到了1本,还剩2本,继续加油!
     * Thread[Thread-2,5,main]-恭喜您抢到了1本,还剩1本,继续加油!
     * Thread[Thread-3,5,main]-恭喜您抢到了1本,还剩0本,继续加油!
     * 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();

        /**
         * 模拟三个独立的人同时抢购书籍
         */
        Thread tom = new Thread(bookSale);
        Thread jack = new Thread(bookSale);
        Thread lucy = new Thread(bookSale);

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

}

如上案例中,使用curator来实现分布式锁非常的简单,那它是如何做到的呢,背后的工作机制又是怎样的呢?

  • 1.客户端在获取锁时,会在指定目录下创建临时顺序节点;
  • 2.然后获取该目录下所有子节点,判断自己创建的子节点是否是最小的,如果是就代表获取到了锁,执行业务逻辑,结束时删除自己创建的节点;
  • 3.如果发现自己创建的子节点不是最小的,那代表当前没有获取到锁,同时注册监听器到最小的那个子节点上,监听其删除事件;
  • 4.当最小的那个子节点被删除时,代表有人释放了锁,重复如上步骤2;

在如上步骤中,客户端创建的节点有两个特点:

  • 临时的,确保客户端在网络失败(会话失效)情况下,不会长时间占用锁,使得其他客户端可以继续竞争锁;
  • 顺序的,保证一次只有一个客户端能获取到锁;

当然,在学习线程锁的时候,我们有讲过锁的种类,Curator中实现的分布式锁也有多种,供了解。

  • InterProcessSemaphoreMutex,分布式非可重入排它锁;意思是在任何时刻不会有两个客户端同时持有锁,该客户端在拥有锁的同时,也不能多次获取锁;
  • InterProcessMutex,分布式可重入排它锁;意思是在任何时刻不会有两个客户端同时持有锁,和JDK的ReentrantLock类似, 意味着该客户端在拥有锁的同时,可以多次获取锁,不会被阻塞;
  • InterProcessReadWriteLock,分布式可重入读写锁;参考JDK中的读写锁;
  • InterProcessSemaphoreV2,共享信号量;参考JDK中的信号量;
  • InterProcessMultiLock,多共享分布式锁,多个锁作为一个锁,可以同时在多个资源上加锁。一个维护多个锁对象的容器。当调用 acquire()时,获取容器中所有的锁对象,请求失败时,释放所有锁对象。同样调用release()也会释放所有的锁;

四、ZK集群搭建和使用

4.1 集群搭建

ZK中的角色总共分为三种:

  • 领导者,Leader,负责处理事务类的请求,是集群内部各个服务器的调度者,一个集群只能有一个Leader;
  • 跟随着,Follower,负责处理非事务类的请求,收到事务类请求时会转发给Leader进行处理;同时参与集群Leader的选举投票,有可能成为新的Leader;
  • 观察者,Observer,负责处理非事务类的请求,不参加投票,不是必须的;
集群工作示意图

客户端在设置连接ZK集群的时候,可以将集群所有服务器的IP+端口都配置进去,比如Curator就会任意选择一个ZK服务器进行连接,任何一台服务器上的数据都是相同的,当前连接的服务器如果宕机的话,Curator会自动帮我们重连寻找集群中其它可用的服务器。

我们现在已经有了一台云主机了,为了配置成为ZK集群,再购买两台(需要在同一个局域网内),同时按照2.1节中的步骤配置好java环境和ZK的配置。需要额外配置的内容有如下:

  • 在每台机器的zk数据目录下,也就是我们创建的zkdata目录下,创建myid文件,指定各自的服务器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
    
  • 修改每台机器的zk配置文件,将集群中所有的机器地址配置在里面;

    vim /soft/apache-zookeeper-3.5.6-bin/conf/zoo.cfg
    
    # server.id=服务器IP地址:服务器之间的通信端口:服务器之间的选举投票端口
    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异常服务终止,那么会发生什么呢?

# 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

可以看到,经过ZAB协议的重新选举,node3成为了新的Leader,此时如果再停掉任意一个节点,整个集群将不可用:

# 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.

这是因为集群中正常节点的数量没有超过总节点的一半,此时我们重启node1,会发现集群会恢复正常,且产生了新的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)

拜占庭将军问题是用来为描述分布式系统一致性问题而情景化的一个著名例子,该问题是分布式领域最为复杂的问题,通常解决该问题的算法分为两类:

  • 故障容错算法,解决的是分布式系统中存在故障, 但不存在恶意攻击的场景下的共识问题。也就是说, 在该场景下可能存在消息丢失, 消息重复, 但不存在消息被篡改或伪造的场景。一般用于局域网场景下的分布式系统, 属于此类的常见算法有Paxos算法, Raft算法, ZAB协议等;
  • 拜占庭容错算法,可以解决分布式系统中既存在故障, 又存在恶意攻击场景下的共识问题。 一般用于互联网场景下的分布式系统, 如在数字货币的区块链技术中,属于此类的常见算法有PBFT算法, PoW算法等。

5.2 Paxos算法

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

Paxos算法是用来解决分布式系统中信息一致性问题的算法之一,其实现比较复杂,在实际中很少直接使用。

5.3 ZAB协议

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

ZAB协议是基于Multi-Paxos改造和优化后的算法,Zookeeper集群的选举过程就是该算法的体现;

5.3 Raft协议

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

Raft协议是将Paxos优化和简化之后的算法,Nacos集群的选举过程就是该算法的体现;

5.5 CAP原理

谈谈分布式系统的CAP理论 - 知乎 (zhihu.com)

CAP分别代表一致性、可用性、分区容错性,在分布式系统中,P代表的分区容错性是必须的,所以一般都是在CP和AP之间做出选择。Zookeeper因为ZAB协议的关系,不能保证可用性,因此属于CP阵营。

六、注册中心和配置中心

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

现在主流的注册中心有很多,比如SpringCloud Eureka,Nacos,Apollo,Consul等,为什么Zookeeper明明也可以作为注册中心和配置中心,却鲜有人用呢?

Zookeeper作为注册中心的注册流程大致如下:

  • 服务提供者启动时,会将其服务名称、ip地址等信息注册到Zookeeper的某个路径上,相同的服务使用同一个路径,不同的服务有各自的路径;
  • 服务消费者在第一次调用服务时,会通过Zookeeper找到相应的服务的IP地址列表,并缓存到本地,以供后续使用。当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从IP列表中取一个服务提供者的服务器调用服务。
  • 当服务提供者的某台服务器宕机、下线、上线时,相应的ip会从服务提供者IP列表中移除或者新增,此时,Zookeeper会依靠其自身的Watcher机制将新的服务IP地址列表发送给服务消费者机器,缓存在消费者本机;
  • Zookeeper自身具有心跳检测机制,会定期向服务提供者发送请求,如果长时间没有回应,就认为该服务已经下线,会将其剔除;

可以看到Zookeeper的工作机制和主流的注册中心没啥大的区别,其不太适合作为注册中心的原因是其底层核心算法ZAB协议导致的,ZAB是CP阵营的算法,虽然提供了强一致性,但是在选举的过程中无法保证可用性(其重新选举和数据同步将耗费一定的时间,在此期间服务不可用),对于微服务体系来说是不可接收的。

同理,Zookeeper作为配置中心也存在同样的问题,因此不是不能用,而是不太适合,有其它更好的方案可供选择。

你可能感兴趣的:(分布式协调服务ZooKeeper使用入门)