ZooKeeper


一. zookeeper简介

Zookeeper 分布式服务框架是Apache Hadoop 的一个子项目,它主要是用来解决分布式应用中经常遇到的一些数据管理问题,如:分布式注册中心,统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等。

简而言之,zookeeper=文件系统+通知机制,简称ZK。

1)文件系统

Zookeeper维护一个类似文件系统的数据结构:

1612852814527

每个子目录项如 NameService 都被称作为 znode,和文件系统的目录一样,能够自由的增加、删除znode,在一个znode下也可以增加、删除子znode,唯一的不同在于znode是可以存储数据的。

有四种类型的znode:

  • PERSISTENT 持久化目录节点

    客户端与zookeeper断开连接后,该节点依旧存在

  • PERSISTENT_SEQUENTIAL 持久化顺序编号目录节点

    客户端与zookeeper断开连接后,该节点依旧存在;只是Zookeeper会给该节点名称进行顺序编号

  • EPHEMERAL 临时目录节点

    客户端与zookeeper断开连接后,该节点被删除

  • EPHEMERAL_SEQUENTIAL 临时顺序编号目录节点

    客户端与zookeeper断开连接后,该节点被删除;只是Zookeeper会给该节点名称进行顺序编号

2)通知机制

ZooKeeper采用了观察者设计模式,客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。

1612853382040

3)设计目的

  • 最终一致性:client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能。

  • 可靠性:具有简单、健壮、良好的性能,如果消息m被一台服务器接受,那么它将被所有的服务器接受。

  • 实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync( )接口。

  • 等待无关(wait-free):慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待。

  • 原子性:更新只能成功或者失败,没有中间状态。

  • 顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面。

二. 环境搭建与使用

2.1 windows安装

apache官网下载:zookeeper 3.6.2版本下载

下载完成后解压文件:

解压完成后默认没有data和log文件夹,需手动创建。

1612854634858

进入conf文件夹内,复制一份zoo_sample.cfg文件命名为zoo.cfg,zookeeper默认会读取conf文件夹下名为zoo.cfg的配置文件。

1612854703931

修改zoo.cfg配置文件内容:

1613133882835

dataDir:指定数据存放的文件夹,用来存放myid信息跟一些版本,日志,跟服务器唯一的ID信息等。设置为安装后创建的data文件夹

dataLogDir:指定日志存放的文件夹,设置为安装后创建的log文件夹

其它配置可无需修改。

  • tickTime CS通信心跳时间,默认值2000

    tick翻译成中文的话就是滴答滴答的意思,连起来就是滴答滴答的时间,寓意心跳间隔,单位是毫秒,系统默认是2000毫秒,也就是间隔两秒心跳一次。

    tickTime的意义:客户端与服务器或者服务器与服务器之间维持心跳,也就是每个tickTime时间就会发送一次心跳。通过心跳不仅能够用来监听机器的工作状态,还可以通过心跳来控制Flower跟Leader的通信时间,默认情况下FL的会话时常是心跳间隔的两倍。

  • initLimit 默认值10

    集群中的follower服务器(F)与leader服务器(L)之间初始连接时能容忍的最多心跳数(tickTime的数量)。

  • syncLimit 默认值5

    集群中flower服务器(F)跟leader(L)服务器之间的请求和答应最多能容忍的心跳数。

  • clientPort

    客户端连接zookeeper服务器的端口,zookeeper会监听这个端口,接收客户端的请求访问!这个端口默认是2181。

linux系统安装步骤是一样的,下载官网对应的linux版本即可。

2.2 启动zk服务

进入安装目录下的bin文件夹,双击zkServer.cmd启动服务端;日志无报错即启动成功

1612855598489

进入安装目录下的bin文件夹,双击zkCli.cmd启动客户端;日志无报错即启动成功

1612855704616

在启动的时候cmd界面可能会卡住无反应,这个时候按一下回车就好。。。

客户端cmd基本命令使用:

ls / :查看当前 ZooKeeper 中所包含的内容 (默认只有zookeeper一个节点)

create /节点名 值 :创建一个新的 znode ,并赋值

get /节点名:获取指定节点下的数据

set /节点名 值:更新节点下的数据

delete /节点名 :删除指定节点

1612857227025

2.3 JAVA操作zookeeper

新建一个maven项目,在pom.xml文件内导入以下依赖:

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.6.2</version>
</dependency>

在zk客户端界面创建一个节点用于测试:

1612858149642

1)zk监听案例:

/**
 * @author : xsh
 * @create : 2021-02-09 - 16:07
 * @describe: 分布式配置中心监听demo
 */
public class ZooKeeperProSync implements Watcher {

    private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
    private static ZooKeeper zk = null;
    private static Stat stat = new Stat();

    public static void main(String[] args) throws Exception {
        //zookeeper配置数据存放路径
        String path = "/username";
        //连接zookeeper并且注册一个默认的监听器
        zk = new ZooKeeper("127.0.0.1:2181", 5000,
                new ZooKeeperProSync());
        //等待zk连接成功的通知
        connectedSemaphore.await();
        //获取path目录节点的配置数据,并注册默认的监听器
        System.out.println(new String(zk.getData(path, true, stat)));
        Thread.sleep(Integer.MAX_VALUE);
    }

    public void process(WatchedEvent event) {
        //zk连接成功通知事件
        if (Event.KeeperState.SyncConnected == event.getState()) {
            if (Event.EventType.None == event.getType() && null == event.getPath()) {
                connectedSemaphore.countDown();
            } else if (event.getType() == Event.EventType.NodeDataChanged) {
                //zk目录节点数据变化通知事件
                try {
                    System.out.println("配置已修改,新值为:" + new String(zk.getData(event.getPath(), true, stat)));
                } catch (Exception e) {
                }
            }
        }
    }
}

运行测试类后控制台输出:

1612858343336

运行完测试类后程序并没有立即停止,而还处于运行状态并注册了一个监听器;

在zk的客户端使用set命令更新节点值

1612858515016

测试类控制台监听到修改,并输出:

1612858540737

2)zookeeper的一些基础的api使用

import org.apache.zookeeper.*;
import java.util.concurrent.CountDownLatch;

/**
 * @author : xsh
 * @create : 2021-02-09 - 16:28
 * @describe: ZK的一些基础的api使用
 */
public class ZKJavaApiSample implements Watcher {

    private static final int SESSION_TIMEOUT = 10000;
    private static final String CONNECTION_STRING = "127.0.0.1:2181";
    private static final String ZK_PATH = "/testPath";
    private ZooKeeper zk = null;
    private CountDownLatch connectedSemaphore = new CountDownLatch(1);

    public static void main(String[] args) {

        ZKJavaApiSample sample = new ZKJavaApiSample();
        //创建zk连接
        sample.createConnection(CONNECTION_STRING, SESSION_TIMEOUT);
        //创建一个zk节点并判断是否创建成功
        if (sample.createPath(ZK_PATH, "AAA")) {
            //获取创建的节点的值
            System.out.println("数据内容: " + sample.readData(ZK_PATH) + "\n");
            //更新节点数据,将AAA更新为BBB
            sample.writeData(ZK_PATH, "BBB");
            //查看更新后节点的值
            System.out.println("数据内容: " + sample.readData(ZK_PATH) + "\n");
            //删除节点
            sample.deleteNode(ZK_PATH);
        }
        //断开zk连接
        sample.releaseConnection();
    }


    /**
     * 创建ZK连接
     *
     * @param connectString  ZK服务器地址列表
     * @param sessionTimeout Session超时时间
     */
    public void createConnection(String connectString, int sessionTimeout) {
        try {
            zk = new ZooKeeper(connectString, sessionTimeout, this);
            connectedSemaphore.await();
        } catch (Exception e) {
            System.out.println("创建zk连接失败");
            e.printStackTrace();
        }
    }

    /**
     * 关闭ZK连接
     */
    public void releaseConnection() {
        if (this.zk != null) {
            try {
                this.zk.close();
            } catch (InterruptedException e) {
                // ignore
                e.printStackTrace();
            }
        }
    }

    /**
     * 创建节点
     * 节点类型:
     *         1. CreateMode.PERSISTENT :持久节点,一旦创建就保存到硬盘上面
     *       2.  CreateMode.SEQUENTIAL : 顺序持久节点
     *       3.  CreateMode.EPHEMERAL :临时节点,创建以后如果断开连接则该节点自动删除
     *       4.  CreateMode.EPHEMERAL_SEQUENTIAL :顺序临时节点
     * @param path 节点path
     * @param data 初始数据内容
     * @return
     */
    public boolean createPath(String path, String data) {
        try {
            //创建一个CreateMode.EPHEMERA零时节点
            System.out.println("节点创建成功, Path: "
                    + this.zk.create(path, data.getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL) 
                    + ", content: " + data);
        } catch (Exception e) {
            System.out.println("节点创建失败");
            e.printStackTrace();
        }
        return true;
    }

    /**
     * 读取指定节点数据内容
     *
     * @param path 节点path
     * @return
     */
    public String readData(String path) {
        try {
            System.out.println("获取数据成功,path:" + path);
            return new String(this.zk.getData(path, false, null));
        } catch (Exception e) {
            System.out.println("读取数据失败,path: " + path);
            e.printStackTrace();
            return "";
        }
    }

    /**
     * 更新指定节点数据内容
     *
     * @param path 节点path
     * @param data 数据内容
     * @return
     */
    public boolean writeData(String path, String data) {
        try {
            System.out.println("更新数据成功,path:" + path + ", stat: " +
                    this.zk.setData(path, data.getBytes(), -1));
        } catch (Exception e) {
            System.out.println("更新数据失败,path: " + path);
            e.printStackTrace();
        }
        return false;
    }

    /**
     * 删除指定节点
     *
     * @param path 节点path
     */
    public void deleteNode(String path) {
        try {
            this.zk.delete(path, -1);
            System.out.println("删除节点成功,path:" + path);
        } catch (Exception e) {
            System.out.println("删除节点失败,path: " + path);
            e.printStackTrace();
        }
    }

    /**
     * 收到来自zk Server的Watcher通知后的处理。
     */
    public void process(WatchedEvent event) {
        System.out.println("收到事件通知:" + event.getState() + "\n");
        if (Event.KeeperState.SyncConnected == event.getState()) {
            connectedSemaphore.countDown();
        }
    }
}

运行main方法控制台打印:

1612860721980

三. ZK的使用场景

3.1 命名服务

这个是最简单的使用,在zookeeper的文件系统里创建一个目录,即有唯一的一个path。在使用时无法确定上游程序的部署机器时即可与下游程序约定好一个path,通过path即能互相发现并监听

3.2 配置管理

程序总是需要配置的,如果程序分散部署在多台机器上,要逐个改变配置就变得困难。现在可以把这些配置全部放到zookeeper上去,保存在 Zookeeper 的某个目录节点中,然后所有相关应用程序对这个目录节点进行监听,一旦配置信息发生变化,每个应用程序就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中就好。

1612861858001

3.3 集群管理

集群管理在于两点:1)是否有服务退出和加入 2)如何选举master

对于第一点,所有服务约定在父目录GroupMembers下创建临时目录节点,然后监听父目录节点的子节点变化消息。一旦有服务挂掉,该服务与 zookeeper的连接断开,其所创建的临时目录节点被删除,所有其他服务都收到通知:某个服务的目录被删除,于是,所有服务都知道:它挂了。新服务加入也是类似,所有服务收到通知:新目录被创建意味着有新服务加入。

对于第二点,则可所有服务创建临时顺序编号目录节点,每次选取编号最小的机器作为master就好。

1612862147600

3.4 分布式锁

有了zookeeper的一致性文件系统,锁的问题变得容易。

锁服务可以分为两类:1)保持独占 2)控制时序。

对于第一类,可以将zookeeper上的一个znode节点看作是一把锁,通过createznode的方式来实现创建锁。所有客户端都去创建 /distribute_lock 节点,最终只会有一个客户端创建成功,而最后成功创建的那个客户端也即拥有了这把锁,然后用完立即删除掉自己创建的distribute_lock节点就释放出锁。

对于第二类, /distribute_lock 已经预先存在,所有客户端在它下面创建临时顺序编号目录节点,和选master一样,编号最小的获得锁,用完删除。

1612862483435

3.5 队列管理

两种类型的队列:

1、 同步队列,当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达。

2、 队列按照 FIFO 方式进行入队和出队操作。

第一类,在约定目录下创建临时目录节点,监听节点数目是否是我们要求的数目。

第二类,和分布式锁服务中的控制时序场景基本原理一致,入列有编号,出列也按编号出。

3.6 注册中心

Registry(注册中心): 服务注册与发现的注册中心。

dubbo:一个远程服务调用的分布式框架。

Provider(生产者): 暴露服务的服务提供方。

Consumer(消费者): 调用远程服务的服务消费方。

zookeeper可作为dubbo的注册中心,可以理解为zookeeper就是个中介,卖楼的(生产者)把楼盘信息放在中介(注册中心)那里,想买楼的(消费者)去中介那里获得楼盘资源清单。

四. 分布式集群与数据复制

单点维护一个文件系统没有什么难度,可是如果是一个集群维护一个文件系统保持数据的一致性就非常困难了。

Zookeeper作为一个集群提供一致的数据服务,自然有在机器间做数据复制的功能。

数据复制的好处:

  • 容错

    一个节点出错,不致于让整个系统停止工作,别的节点可以接管它的工作;

  • 提高系统的扩展能力

    把负载分布到多个节点上,或者增加节点来提高系统的负载能力;

  • 提高性能

    让客户端本地访问就近的节点,提高用户访问速度。

数据复制集群分下面两种:

  • 写主(WriteMaster)

    对数据的修改提交给指定的节点。读无此限制,可以读取任何一个节点。这种情况下客户端需要对读与写进行区别,俗称读写分离;

  • 写任意(Write Any)

    对数据的修改可提交给任意的节点,跟读一样。这种情况下,客户端对集群节点的角色与变化透明。

对zookeeper来说,它采用的方式是写任意。通过增加机器,它的读吞吐能力和响应能力扩展性非常好;而写,随着机器的增多吞吐能力肯定下降,而响应能力则取决于具体实现方式,是延迟复制保持最终一致性,还是立即复制快速响应。

而如何保证数据在集群所有机器的一致性,这就涉及到paxos算法。

4.1 数据一致性与paxos算法

在一个分布式数据库系统中,如果各节点的初始状态一致,每个节点都执行相同的操作序列,那么他们最后能得到一个一致的状态。

而Paxos算法解决的就是保证每个节点执行相同的操作序列。master维护一个全局写队列,所有写操作都必须放入这个队列编号,那么无论写多少个节点,只要写操作是按编号来的,就能保证一致性。

而如果master挂了,Paxos算法通过投票来对写操作进行全局编号,同一时刻,只有一个写操作被批准,同时并发的写操作要去争取选票,只有获得过半数选票的写操作才会被批准(所以永远只会有一个写操作得到批准),其他的写操作竞争失败只好再发起一轮投票,就这样,在日复一日年复一年的投票中,所有写操作都被严格编号排 序。编号严格递增,当一个节点接受了一个编号为100的写操作,之后又接受到编号为99的写操作(因为网络延迟等很多不可预见原因),写入编号的顺序不是顺序递增时,它马上能意识到自己 数据不一致了,自动停止对外服务并重启同步过程。任何一个节点挂掉都不会影响整个集群的数据一致性(总服务2n+1台,除非挂掉大于n台,所以zookeeper服务一般部署数量为奇数)。

4.2 zookeeper节点奇数位

zookeeper 集群通常是用来对用户的分布式应用程序提供协调服务的,为了保证数据的一致性,对 zookeeper 集群进行了这样三种角色划分:leader、follower、observer分别对应着总统、议员和观察者。

  • 总统(leader):负责进行投票的发起和决议,更新系统状态。
  • 议员(follower):用于接收客户端请求并向客户端返回结果以及在选举过程中参与投票。
  • 观察者(observer):也可以接收客户端连接,将写请求转发给leader节点,但是不参与投票过程,只同步leader的状态。通常对查询操作做负载。

1612937842579

在每台机器数据保持一致的情况下,zookeeper集群可以保证客户端发起的每次查询操作,集群节点都能返回同样的结果。

但是对于客户端发起的修改、删除等能改变数据的操作,集群中那么多台机器,各个机器间各修改各的,最后返回集群中是哪台机器的数据呢?

这就是一盘散沙,需要一个领导,于是在zookeeper集群中,leader的作用就体现出来了,只有leader节点才有权利发起修改数据的操作,而follower节点即使接收到了客户端发起的修改操作,也要将其转交给leader来处理,leader接收到修改数据的请求后,会向所有follower广播一条消息,让他们执行某项操作,follower 执行完后,便会向 leader 回复执行完毕。当 leader 收到半数以上的 follower 的确认消息,便会判定该操作执行完毕,然后向所有 follower 广播该操作已经生效。

所以zookeeper集群中leader是不可缺少的,而 leader 节点其实就是由所有follower 节点选举产生的,并且leader节点只能有一个。

而zookeeper 节点数是奇数的原因:

  • 容错率(需要保证集群服务中能够有半数进行投票,即集群中正常的机器过半则表示服务可以使用)

    • 2台服务器,至少2台正常运行才行(2的半数为1,半数以上最小整数为2),正常运行1台服务器都不允许挂掉,但是相对于单节点服务器,2台服务器还有两个单点故障,所以直接排除了。

    • 3台服务器,至少2台正常运行才行(3的半数为1.5,半数以上最小整数为2),正常运行可以允许1台服务器挂掉

    • 4台服务器,至少3台正常运行才行(4的半数为2,半数以上最小整数为3),正常运行可以允许1台服务器挂掉

    • 5台服务器,至少3台正常运行才行(5的半数为2.5,半数以上最小整数为3),正常运行可以允许2台服务器挂掉

    2-> 0,3->1,4->1,5->2;可以理解为2n-1和2n的容忍度是一样的,都是n-1,所以没必要弄成2n台,多一台浪费;一般为大于三台的奇数位。

  • 防脑裂

4.3 Zookeeper集群中的脑裂场景

脑裂(split-brain)就是“大脑分裂”,也就是本来一个“大脑”被拆分了两个或多个“大脑”.

而如果一个人有多个大脑,并且相互独立的话,那么会导致人体“手舞足蹈”,“不听使唤”。

集群的脑裂通常是发生在节点之间通信不可达的情况下(某些原因导致节点故障),集群会分裂成不同的小集群,小集群各自选出自己的leader节点,导致原有的集群出现多个leader节点的情况,这就是脑裂。

而集群环境比如ElasticSearch或Zookeeper集群,都有一个统一的特点,就是它们只有一个大脑,比如ElasticSearch集群中有Master节点,Zookeeper集群中有Leader节点。

对于一个集群,想要提高这个集群的可用性,通常会采用多机房部署,比如现在有一个由6台zkServer所组成的一个集群,部署在了两个机房:

1612926573362

正常情况下,此集群只会有一个Leader,那么如果机房之间的网络断了之后,两个机房内的zkServer还是可以相互通信的,如果不考虑过半机制,那么就会出现每个机房内部都将选出一个Leader。

1612926704283

这就相当于原本一个集群,被分成了两个集群,出现了两个“大脑”,这就是脑裂。

对于这种情况,可以看出,原本应该是统一的一个集群对外提供服务的,现在变成了两个集群同时对外提供服务,如果过了一会,断了的网络突然联通了,那么此时就会出现问题了,两个集群刚刚都对外提供服务了,数据该怎么合并,数据冲突怎么解决等等问题。

刚刚在说明脑裂场景时,有一个前提条件就是没有考虑过半机制,所以实际上Zookeeper集群中是不会出现脑裂问题的,而不会出现的原因就跟过半机制有关。

4.4 过半机制

在领导者选举的过程中,如果某台zkServer获得了超过半数的选票,则此zkServer就可以成为Leader了。

过半机制的源码实现在QuorumMaj这个类中:

1612934465865

其中LearnerType内有两个枚举:

1612934559550

LearnerType.PARTICIPANT代表参与者; LearnerType.OBSERVER代表观察者节点

在QuorumMaj这个类中的构造方法内可以看到,如果zkServer的类型为PARTICIPANT才会被放入votingMembers中,所以this.votingMembers.size()的数量即为集群中zkServer的个数(准确的说是参与者的个数,参与者不包括观察者节点),然后将参与者Server的总数量除以2并赋值给half

this.half = this.votingMembers.size() / 2;

而在QuorumMaj类中containsQuorum方法,则是验证是否符合过半机制

1612935582541

ackSet.size( )表示的是某台ZKServer获得的票数,当得票数大于参与者Server总数量的一半时,就返回true;这个就是过半机制.

而在zookeeper选举过程中之所以会有过半机制的验证,是因为这样即能保证只选举出一个Leader,又可以在短时间内快速选出;这样不需要等待所有zkServer都投了同一个zkServer就可以选举出来一个Leader了,比较快所以又叫快速领导者选举算法;

比如:

现在集群中有11台zkServer,那么half=11/2=5,大于5的最小整数为6;所以在领导者选举的过程中至少要有6台zkServer投了同一个zkServer,这个zkServer获得了6票,6>half返回true才会符合过半机制,才能选出来一个Leader。而当选举出了Leader之后就可以立刻停止选举,而不用等到全部的zkServer投完票.

在过半机制的验证方法containsQuorum内之所以是大于而不是大于等于,是因为如果判断等于的话就会出现脑裂结果,而去掉等于就不会出现脑裂结果.

在4.3节中描述的脑裂场景:

1612926573362

当机房中间的网络断掉之后,机房1内的三台服务器会进行领导者选举,但是此时过半机制的条件是ackSet.size() > 3,也就是说至少要4台zkServer才能选出来一个Leader,所以对于机房1来说它不能选出一个Leader,同样机房2也不能选出一个Leader,这种情况下整个集群宕机,房间的网络断掉后,整个集群将没有Leader。

而如果过半机制的条件是set.size() >= 3,那么机房1和机房2都会选出一个Leader,这样就出现了脑裂。所以,为什么过半机制中是大于,而不是大于等于。就是为了防止脑裂。

而假设现在只有5台机器,也部署在两个机房:

1612936933887

此时过半机制的条件是set.size( ) > 2,也就是至少要3台服务器才能选出一个Leader,此时机房间的网络断开了,对于机房1来说是没有影响的,可以重新选举Leader; 对于机房2来说因为只有俩台zkServer不满足过半机制所以是选不出来Leader的,此时整个集群中任然只有一个Leader。

所以,可以总结得出,有了过半机制,对于一个Zookeeper集群,要么没有Leader,要么只有1个Leader,这样就避免了脑裂问题的出现。

4.5 搭建zk集群

在zookeeper安装目录下的conf文件夹下新建三个配置文件:zoo1.cfg、zoo2.cfg、zoo3.cfg。(不要直接在原zoo.cfg上修改)

1613131082817

在zookeeper安装目录下新建三个文件夹:zk1,zk2,zk3;并分别在这三个文件夹内再建两个文件夹:data和log

1613131211683

1613131276201

修改zoo1.cfg、zoo2.cfg、zoo3.cfg配置文件。

zoo1.cfg:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=E:/software/zookeeper/apache-zookeeper-3.6.2-bin/zk1/data
dataLogDir=E:/software/zookeeper/apache-zookeeper-3.6.2-bin/zk1/log
clientPort=2182


server.1=localhost:2287:3387
server.2=localhost:2288:3388
server.3=localhost:2289:3389

1613133789365

红色框住的地方为三个配置文件zoo1.cfg、zoo2.cfg、zoo3.cfg内相同的配置,需保持一样;

clientPort修改成不同的:三个配置文件分别配置2182,2183,2184(默认是2181)

dataDir和dataLogDir配置成对应的数据存放文件和日志文件。

tickTime,initLimit,syncLimit保持默认配置即可。

红色框住的参数说明: 格式server.myid=IP:Port1:Port2

  • myid是服务器的编号,一个正整数
  • port1表示的是服务器与集群中的Leader服务器交换信息的端口,一般用2288,
  • Port2表示的是万一集群中的Leader服务器宕机了,需要一个端口来重新进行选举,选出一个新的Leader,一般用3388

zoo2.cfg:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=E:/software/zookeeper/apache-zookeeper-3.6.2-bin/zk2/data
dataLogDir=E:/software/zookeeper/apache-zookeeper-3.6.2-bin/zk2/log
clientPort=2183


server.1=localhost:2287:3387
server.2=localhost:2288:3388
server.3=localhost:2289:3389

zoo3.cfg:

tickTime=2000
initLimit=10
syncLimit=5
dataDir=E:/software/zookeeper/apache-zookeeper-3.6.2-bin/zk3/data
dataLogDir=E:/software/zookeeper/apache-zookeeper-3.6.2-bin/zk3/log
clientPort=2184


server.1=localhost:2287:3387
server.2=localhost:2288:3388
server.3=localhost:2289:3389

完成以上配置后,还需要在每个data文件夹内新建一个myid文件,内容为对应的zoo.cfg里server.后的数字;

注:myid不是txt文本文件,是一个没有后缀名的文件,可以新建一个文本文件再把后缀名去掉;

1613134576920

1613134710944

1613134731872

启动配置的集群服务:

复制三个zkServer.cmd文件并分别命名为zkServer-1.cmd,zkServer-2.cmd,zkServer-3.cmd

1613133345962

修改配置文件zkServer-1.cmd,在配置文件内加入这一行并保存:

set ZOOCFG=../conf/zoo1.cfg

1613135635366

zkServer-2.cmd文件内加入:set ZOOCFG=../conf/zoo2.cfg

zkServer-3.cmd文件内加入:set ZOOCFG=../conf/zoo3.cfg

分别双击这三个cmd服务启动文件。

启动zkServer-1.cmd时会报以下错误:

1613135030211

这个是正常的,因为配置文件内配置了集群server,server2和server3此时还未启动,server1找不到这俩服务就拒绝连接报错了;当三个server都启动就不报错了。

三个集群zk服务都成功启动后,就可以使用客户端连接对应的服务端了。

zookeeper 的监控工具:ZooInspector下载

下载后解压,解压文件内build目录下有一个zookeeper-dev-ZooInspector.jar文件,双击即可运行

1613148863959

点击左上角绿色图标连接zookeeper服务,端口填写前面配置clientPort属性2182端口

1613148957062

连接后前面配置的集群server配置文件也会显示在config内

1613149042156

ZooInspector这个工具可以为连接的zk服务增加删除和编辑节点。

五. ZK的工作原理

Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和 leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态。

为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于哪个leader的统治时期。低32位用于递增计数。

zxid:

  • ZooKeeper状态的每一次改变, 都对应着一个递增的Transaction id, 该id称为zxid. 由于zxid的递增性质, 如果zxid1小于zxid2, 那么zxid1肯定先于zxid2发生.

创建任意节点, 或者更新任意节点的数据, 或者删除任意节点, 都会导致Zookeeper状态发生改变, 从而导致zxid的值增加.

每个Server在工作过程中有三种状态:

  • LOOKING:当前Server不知道leader是谁,正在搜寻
  • LEADING:当前Server即为选举出来的leader
  • FOLLOWING:leader已经选举出来,当前Server与之同步

5.1 选主流程

当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的 Server都恢复到一个正确的状态。Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。

  • basic paxos流程:
    • 1 ) 选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server;
    • 2 ) 选举线程首先向所有Server发起一次询问(包括自己);
    • 3 ) 选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息( id,zxid),并将这些信息存储到当次选举的投票记录表中;
    • 4 ) 收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server;
    • 5 ) 线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 + 1的Server票数, 设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来。

通过流程分析可以得出:要使Leader获得多数Server的支持,则Server总数必须是奇数2n+1,且存活的Server的数目不得少于n+1.

每个Server启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的server还会从磁盘快照中恢复数据和会话信息,zk会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。

选主的具体流程图如下所示:

1612938890171

fast paxos流程则是在选举过程中,某ZKServer首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和 zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。其流程图如下所示:

1612938972336

5.2 同步流程

选完leader以后,zk就进入状态同步过程。

​ 1. leader等待server连接;

​ 2 . Follower连接leader,将最大的zxid发送给leader;

​ 3 . Leader根据follower的zxid确定同步点;

​ 4 . 完成同步后通知follower 已经成为uptodate状态;

​ 5 . Follower收到uptodate消息后,又可以重新接受client的请求进行服务了。

1613127325046

5.3 工作流程

Leader工作流程

Leader主要有三个功能:

​ 1 .恢复数据;

​ 2 .维持与Learner的心跳,接收Learner请求并判断Learner的请求消息类型;

​ 3 .Learner的消息类型主要有PING消息、REQUEST消息、ACK消息、REVALIDATE消息,根据不同的消息类型,进行不同的处理。

  • PING消息是指Learner的心跳信息;
  • REQUEST消息是Follower发送的提议信息,包括写请求及同步请求;
  • ACK消息是 Follower的对提议的回复,超过半数的Follower通过,则commit该提议;
  • REVALIDATE消息是用来延长SESSION有效时间。

Leader的工作流程简图如下所示,在实际实现中,流程要比下图复杂得多,启动了三个线程来实现功能。

1613127457051

Follower工作流程

Follower主要有四个功能:

​ 1. 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);

​ 2 .接收Leader消息并进行处理;

​ 3 .接收Client的请求,如果为写请求,发送给Leader进行投票;

​ 4 .返回Client结果。

Follower的消息循环处理如下几种来自Leader的消息:

​ 1 .PING消息: 心跳消息;

​ 2 .PROPOSAL消息:Leader发起的提案,要求Follower投票;

​ 3 .COMMIT消息:服务器端最新一次提案的信息;

​ 4 .UPTODATE消息:表明同步完成;

​ 5 .REVALIDATE消息:根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息;

​ 6 .SYNC消息:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新。

Follower的工作流程简图如下所示,在实际实现中,Follower是通过5个线程来实现功能的。

1613127571092

注:observer流程和Follower的唯一不同的地方就是observer不会参加leader发起的投票,其它地方与Follower相同。

5.4 observer

因为Zookeeper需保证高可用和强一致性,所以为了支持更多的客户端,需要增加更多的Server,而Server增多,投票阶段延迟增大,影响性能;所以权衡伸缩性和高吞吐率,引入Observer。

  • Observer不参与投票;

  • Observers接受客户端的连接,并将写请求转发给leader节点;

  • 加入更多Observer节点,提高伸缩性,同时不影响吞吐率

5.5 zk的数据模型

» 层次化的目录结构,命名符合常规文件系统规范

» 每个节点在zookeeper中叫做znode,并且其有一个唯一的路径标识

» 节点Znode可以包含数据和子节点,但是EPHEMERAL类型的节点不能有子节点

» Znode中的数据可以有多个版本,比如某一个路径下存有多个数据版本,那么查询这个路径下的数据就需要带上版本

» 客户端应用可以在节点上设置监视器

» 节点不支持部分读写,而是一次性完整读写

5.6 zk的节点

» Znode有两种类型,短暂的(ephemeral)和持久的(persistent)

» Znode的类型在创建时确定并且之后不能再修改

» 短暂znode的客户端会话结束时,zookeeper会将该短暂znode删除,短暂znode不可以有子节点

» 持久znode不依赖于客户端会话,只有当客户端明确要删除该持久znode时才会被删除

» Znode有四种形式的目录节点:

  • PERSISTENT(持久的)
  • EPHEMERAL(暂时的)
  • PERSISTENT_SEQUENTIAL(持久化顺序编号目录节点)
  • EPHEMERAL_SEQUENTIAL(暂时化顺序编号目录节点)
springCloud
springBoot
  • 作者:管理员(联系作者)
  • 发表时间:2021-02-12 20:17
  • 版权声明:自由转载-非商用-非衍生-保持署名(null)
  • undefined
  • 评论