Zookeeper

Zookeeper是一个开源的分布式应用程序协调服务,用于实现分布式锁、配置维护和命名服务等。本文介绍了Zookeeper的安装、集群搭建、数据结构、特征以及API的使用,详细讲解了如何利用Zookeeper实现分布式锁和高可用性。通过选举算法确保快速恢复可用状态,提供可靠的分布式协调服务。
摘要由CSDN通过智能技术生成
Zookeeper

一.前言
  • redis课程:

  • 之前讲过redis是单实例的,内存快,但是有单点故障,访问压力,存储容量问题

    所以推出了复制集群,HA sentinel哨兵集群(不是绝对的实时同步,可能连最终一致性都谈不上),分片集群模式

    也可以用它来实现分布式锁,但是会有节点单机导致死锁,和锁超时问题,需要额外创建一个线程来监控续时间

    这种分布式锁方案不优,下面就介绍一下zookeeper,来完成分布式协调,分布式锁的讲解使用
    在这里插入图片描述

二.简介
  • 官网: https://zookeeper.apache.org

    官网介绍:
    ZooKeeper是一个分布式的、开源的分布式应用程序协调服务。它公开了一组简单的原语,分布式应用程序可以在此基础上实现更高级别的同步(临时节点)、配置维护(1M数据存储)、分组管理(path结构)和命名服务(sequence)。它的设计易于编程,并使用了一个数据模型样式后熟悉的目录树结构的文件系统。它在Java中运行,并且有Java和C的绑定 (这些高级功能需要Client代码实现) 。
    众所周知,协调服务很难做好。它们特别容易出错,例如竞争条件和死锁。ZooKeeper背后的动机是从零开始减轻分布式应用程序实现协调服务的责任。

三.安装,搭建集群
  • 安装

    • 先安装jdk

      linux jdk 安装
      1.下载安装包
      2.tar -zxvf jdk-8u251-linux-i586.tar.gz -C /home/soft
      3.vim /etc/profile #配置环境变量
      ​ export JAVA_HOME=/home/soft/jdk1.8.0_251
      export JRE_HOME= J A V A H O M E / j r e e x p o r t C L A S S P A T H = . : {JAVA_HOME}/jre export CLASSPATH=.: JAVAHOME/jreexportCLASSPATH=.:{JAVA_HOME}/lib: J R E H O M E / l i b e x p o r t P A T H = {JRE_HOME}/lib export PATH= JREHOME/libexportPATH={JAVA_HOME}/bin:$PATH
      4.source /etc/profile #执行profile
      5.sudo yum install glibc.i686

      解决版本不匹配问题

      验证
      java -version

    • 在安装zookeeper

      准备4个节点 node1~node4

      1.wget xxxxx
      2.tar xf zookeeper.tar.gz
      3.mkdir /opt/mashibing
      5.mv zookeeper/opt/mashibing
      6.vi /etc/profile
      //添加配置环境变量
      export ZOOKEEPER_HOME=/opt/mashibing/zookeeper-3.4.6
      export PATH= P A T H : PATH: PATH:ZOOKEEPER_HOME/bin
      7.cd zookeeper/conf
      8.cp zoo.zem
      .cfg zoo.cfg
      9 ci zoo.cfg
      //修改配置文件配置项
      dataDir=自己知道存放日志快照路径
      //添加节点选举和同步端口节点
      server.1=node01:2888:3888
      server.2=node02:2888:3888
      server.3=node01:2888:3888
      server.4=node01:2888:3888
      10.mkdir -p /var/mashibing/zk
      11.echo 1 > /var/mashibing/zk/myid
      12.cd /opt && scp -r ./mashibing/ node02:·pwd·
      #先配置ip互通
      #vi /etc/hosts
      #192.168.125.60 node01
      #192.168.125.61 node02
      13.node02~node04 创建myid 1,2,3,4启动
      14.zkServer.sh start-foreground
      #前台打印日志方式启动
      #默认是后台启动

      验证:
      zkCli.sh #客户端启动
      help
      ls/
      create /ooxx “”
      create -s /abc/aaa
      create -e /ooxx/xxoo
      create -s -e /ooxx/xoxo

      get /ooxx

      //查看节点的连接信息

      netstat -natp | egrep ‘(2888|3888)’

  • 选举节点连接信息
    在这里插入图片描述
    2888端口会被follower的随机端口连接,一但follower发生写操作就被通过2888端口转发到当前zookeeper进行处理,写入成功后通过2888端口分发给所有follower。

    后启动的zkserver会开启对3888端口的监听,并随机申请端口号连接已启动的zkserver的3888端口,这样达到的效果就是每台zk都有和其他三台zk有建立socket连接,这样每台机器就可以双向通信了。

  • zookeeper有2种运行状态
    主从集群,一个主leader肯定会宕机,会导致服务不可用/数据不一致,这种情况是属于不可靠集群,但是对zookeeper的集群,是高可用的,它可以快速的恢复出一个leader
    在这里插入图片描述
    组成ZooKeeper服务的服务器必须相互了解。它们维护一个内存中的状态映像,以及一个事务日志和持久性存储中的快照。只要大多数服务器可用,ZooKeeper服务就可以使用。
    客户端连接到单个ZooKeeper服务器。客户端维护一个TCP连接,通过它发送请求,获取响应,获取监视事件并发送心跳。如果与服务器的TCP连接断开,则客户端将连接到其他服务器。

主从复制:follower是leader的数据全量副本,读写操作可以发生在leader身上,读操作可以发生在任何node上(读写分离)。连接到 zookeeper 的 follower 节点的 Clien t发送 write 请求时,会被转发到 leader 上。
.
但是对于主从复制集群的第一个反应就是 leader 是一个单点,可能会发生单点故障。单点故障就会带来服务不可用的问题,服务不可用就说明 集群 不可靠,但是事实是 zookeeper 是一个 高可用集群。
.
zookeeper集群的两种状态: 1. 可用状态(leader在工作,有主状态)。2. 不可用状态(leader挂掉了,无主状态)。
两种状态的区别就是 leader 是否挂掉了。相较于不可用状态,越快恢复 可用状态 越好。
官方给出的数据:ZooKeeper只需要不到200毫秒就可以选出一个新的leader。

四.数据结构
  • zookeeper是一个目录树结构,数据是存在内存中的

    1. 统一配置节点
    2. 分组管理<- path结构
    3. 统一命名 <- sqquential
    4. 同步<- 临时节点
    • 要保证对外提供协调服务的时候很快
    • node可以存数据最大1mb
      • 序列节点
        • 每次启动或者创建数据都会消耗一次序列节点
      • 持久节点
      • 临时节点
        1. 每一个客户端连接到zookeeper一定产生一个seesion来代表这个客户端
          • 如果客户端在,session就一直在,客户端挂,session就会消失
          • 没有连接池的概念
        2. 临时节点也会消耗序列节点
  • 通过以上4点可以实现:
    1. 分布式锁(client代码实现)
    - 锁依托一个父节点,且具备持久s代表了父节点可以有多把锁
    - 队列式事物锁,可重入锁
    2. HA,选主

在这里插入图片描述

五.特征

在这里插入图片描述

  • zookeeper集群有三种角色
    1. leader
      • 领导,对外提供读写服务
    2. follower
      • 副本,对外提供读服务,可以参与选举
    3. observer
      • 副本,比follower级别低,对外提供读服务,不能参与选举

这样提高了选举主的效率,就像中国选主席,不需要每个人都去投票,有人大代表,当选主的速度快了,代表着从不可用切换到可用状态更快了

这样做的好处(扩展性):
就是随意增添新的zkserver,除了让从 非可用状态 快速 切换到可用状态,还能极限的放大 查询能力。

在这里插入图片描述

  • 可靠性和快速恢复
    • 其中 “攘其外” 表示对外提供数据的可靠性(数据的可靠 可用 一致性),“安其内” 则表示 服务的可靠性 (快速选leader)。简单的讲,就是服务能够在不可用状态 快速恢复 可用状态的前提下 ,并且对外提供可靠的数据。
    • Paxos 分布式数据一致算法
      可看这篇文件了解:
      https://www.douban.com/note/208430424/?_i=0317593WV7tBkz
      在这里插入图片描述
  • ZAB
    • zk对paxos算法做了一个更简单的实现:ZAB协议(原子广播协议),ZAB协议作用在zk集群可用状态。
    1. Client1 对着 follower1 发起写操作, create /ooxx 节点
    2. follower把 create /ooxx 转发给leader
    3. leader 生成 Zxid(事务ID),leader里会维护着对应follower数量的FIFO队列。
    4. (a): leader开启事务,通过所有FIFO队列里广播log给所有follower,follower1收到log后会回复给leader一个ok。follower2如果因为网络延迟没有及时给leader回复ok也没关系。因为follower1的ok + leader自身的ok 已经在集群数目中 过半。
      (b): leader通过FIFO队列通知所有followers(包含follower2) create /ooxx 生效。虽然follower2没有对之前的log回复ok,但是只要follower2没有挂掉,最终能消费掉FIFO的消息,那么最终follower2的数据和其他zkserver的数据是一致的(最终一致性)
    5. leader返回给follower1一个写入成功ok
    6. follower1再返回给Client1一个写入成功ok

    以上涉及的要点:原子广播(原子 + 广播),FIFO队列。
    原子:要么全部成功,要么要不失败,没有中间态(基于队列实现)。
    广播:分布式多节点,不一定所有人都能接收到广播,超过一半就可以生效。
    FIFO队列:push和pop的顺序性。如果follower收到的Zxid小于自身的Zxid,该操作会被拒绝。

  • 选举过程
    • 快速选举leader分两种场景:

      1. 集群第一次启动(没有数据、版本、历史状态)。
      2. 重启集群,或者leader挂掉重选leader(之前运行时产生过数据,可能有的szkser数据多,有的数据少)。
    • 并且每台zkserver都有自己的myid,和Zxid(事务ID)。

      那么根据上述条件,要选取出leader要满足什么条件呢?

      数据最全的(Zxid最高的,一定是过半通过的)。
      myid最大的。
      比较规则:收到其他zkserver的数据后,都是先比较zxid,如果zxid相同,再比较myid。

      集群第一次启动:启动zkserver数量的达到 最大数的一半 + 1 就可以根据myid最大来选出 leader,后启动的zkserver只能追随已经选举出的leader。

      重启集群或者leader挂掉了:无论谁先发现了leader挂掉了触发了投票包,投票包一定会发送到node02(zxid最大 的zkserver && myid最大) ,node02收到投票后就一定会触发自身发起投票 ,只要node02发起了投票,那么其他zkserver一定会选择投它。

    • 选举算法
      参考:http://t.csdn.cn/Sh41U

在这里插入图片描述

  • watch监控
    • ZooKeeper支持watch的概念。客户端可以在znode上设置watch。当znode改变时,watch将被触发并移除。当一个监视被触发时,客户端会收到一个数据包,表示znode已经改变。如果客户机和ZooKeeper服务器之一之间的连接中断,客户机将收到一个本地通知。

      新版本:客户端还可以在znode上设置永久的、递归的监视,这些监视在触发时不会被删除,并且会递归地触发对已注册的znode以及任何子znode的更改。

    zookeeper集群可以做到统一视图,Client访问zk集群中的任何节点,都可以使用sync进行同步,所取回的数据都是一样的。
    同时zookeeper还是一个目录树结构,有层次结构节点的概念。

在这里插入图片描述
有Client1和Client2两个客户端,Client2想要动态的发现Client1的服务。
就可以在zookeeper统一视图和目录树的模型的条件下,有一个/ooxx的节点,如果Client1在/ooxx节点下创建一个节点/a数据为自身IP来代表自己(节点结构为/ooxx/a),Client2连接这个zookeeper集群,一定能通过统一视图拿到/a节点的信息,就可以动态的发现Client2服务了。
但是如果Client1挂掉,Client2想要依赖Client1挂掉的事件关闭自身的服务,就需要Client1和Client2手动开启一个连接的socket发送心跳,同样也有更便捷高效的方式:向zookeeper注册watch。

发送心跳和zookeeper的watch相比区别在于方向性时效性

方向性
自己手动实现心跳需要其中一个Client按一定频率发送心跳。

而watch只需要Client1在zookeeper上创建/a节点时设定为临时节点和session绑定即可,Client2在获取/a节点时注册watch删除事件,当Client1挂了/a节点就会被清理而产生事件,zookeeper就会触发回调Client2的watch的方法。

时效性
zookeeper的实效性必然高于手动建立心跳的实效性,因为一旦Client1挂了,Client的watch方法会立马被回调。
心跳方式 最长需要要等一个心跳的间隔才能发现Client挂了。

六.API
  • 简单API
    • 导入maven坐标
<dependency>
  <groupId>org.apache.zookeeper</groupId>
  <artifactId>zookeeper</artifactId>
  <version>3.4.6</version>
</dependency>
  • API代码
public class App 
{
    public static void main( String[] args ) throws Exception {
        System.out.println( "Hello World!" );

        //zk有session的概念,但是没有线程池的概念。因为每一个连接会得到一个独立的session,监控watch时就会出现混乱。

        /**
         * 在new zk的时候注册session级别的watch是异步的,
         * 如果想要成功连接后再查看zk状态,需要先阻塞。
         */
        CountDownLatch latch=new CountDownLatch(1);

        /**
         * 参数1:zookeeper集群的所有IP地址
         * 参数2:Client程序停止运行后,session的保留时间。
         * 参数3:在zookeeper里面watch分为两类(观察和回调):
         *          第一类:new zk的时候,传入的watch,这个watch是session级别的,和path没有关系。(当前参数)
         *          第二类:path级别的,并且watch的注册 只发生在读类型,调用get,exites
         *  
         * 另外watch事件的注册,只发生在读类型的调用:get、exeits.
         */
        ZooKeeper zk = new ZooKeeper("192.168.116.135:2181,192.168.116.131:2181,192.168.116.132:2181,192.168.116.133:2181",
                3000, new Watcher() {

            /**
             * 回调方法
             * @param event
             */
            @Override
            public void process(WatchedEvent event) {

                //事件状态
                Event.KeeperState state = event.getState();
                //事件类型
                Event.EventType type = event.getType();
                String path = event.getPath();

                System.out.println("new zk watch::"+event.toString());

                switch (state) {
                    case Unknown:
                        break;
                    case Disconnected:
                        break;
                    case NoSyncConnected:
                        break;
                    case SyncConnected:
                        System.out.println("connected...");
                        latch.countDown();
                        break;
                    case AuthFailed:
                        break;
                    case ConnectedReadOnly:
                        break;
                    case SaslAuthenticated:
                        break;
                    case Expired:
                        break;
                }

                switch (type) {
                    case None:
                        break;
                    case NodeCreated:
                        break;
                    case NodeDeleted:
                        break;
                    case NodeDataChanged:
                        break;
                    case NodeChildrenChanged:
                        break;
                }
            }
        });

        latch.await();
        ZooKeeper.States state = zk.getState();
        //根据zk状态不同,打印不同的字符串
        switch (state) {
            case CONNECTING:
                System.out.println("ing....");
                break;
            case ASSOCIATING:
                break;
            case CONNECTED:
                System.out.println("ed....");
                break;
            case CONNECTEDREADONLY:
                break;
            case CLOSED:
                break;
            case AUTH_FAILED:
                break;
            case NOT_CONNECTED:
                break;
        }

        /**
         * 增加节点
         * create创建有两种形式,一种是阻塞,一种是非阻塞回调。(这里使用传统阻塞)
         * 参数1:节点名称。  参数2:节点数据(二进制)     参数3:权限
         * 参数4:节点类型(临时节点,因为在new zk的时候设定3000毫秒,所以程序运行结束后3秒消失)
         *  
         * 返回的pathName(nodeName)还是很有必要的,因为有可能是顺序节点
         */
        String nodeName = zk.create("/ooxx", "olddata".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);

        Stat stat = new Stat();

        /**
         * 查询节点
         * getDate方法分为两大类(同步异步),四种方式。
         * 参数1:查询数据的节点。
         * 参数2:get方法注册的watch(第二类型,监控级别是path,是一次性的)
         * 参数3:全量数据。
         */
        byte[] data = zk.getData("/ooxx", new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                System.out.println("getdata event ::"+event.toString());

                try {
                    /**
                     * 回调处理完业务逻辑,可以直接在逻辑后添加再次注册
                     * 注意:第二个参数如果是true,则代表修改的时候调用的是default watch(new zk的watch)。
                     * false表示不注册。
                     * 写this代表path级别的watch
                     */
                    zk.getData("/ooxx",this,stat);
                } catch (KeeperException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }

        },stat);
        System.out.println("stat::"+stat.getVersion()+"::"+stat.getCzxid());
        System.out.println("查询节点获得结果 ::"+ new String(data));

        /**
         * 修改节点,
         * 一旦修改节点,就会触发之前getData方法中注册的回调。
         */
        Stat newdata = zk.setData("/ooxx", "newdata".getBytes(), 0);
        //第二次修改不会触发getData的watch回调,因为注册watch回调是一次性的,需要从新注册才能再次触发
        Stat newdata01 = zk.setData("/ooxx", "newdata01".getBytes(), newdata.getVersion());
        
        /**
         * 在zookeeper安装目录的conf目录下有log4j的文件,可以拿出来放到项目的resources目录下打印日志
         * 观察:
         *      连接的节点
         *      生成sessionID和临时节点归属匹配
         *
         *  在连接过程中如果zkserver如果挂掉了,程序会从新连接其他可用的zkserver,并且sessionID不会断。
         */
        Thread.sleep(7777777);

    }
}


  • 异步方式获取

        /**
         * 异步的方式是没有返回值的,获取值之后会回调processResult方法
         */
        System.out.println("-------async start--------");
        zk.getData("/ooxx", false, new AsyncCallback.DataCallback() {
            /**
             *
             * @param rc        状态码
             * @param path      路径
             * @param ctx       上下文,其实就是getData时自己定义的abc
             * @param data      数据
             * @param stat      元数据
             */
            @Override
            public void processResult(int rc, String path, Object ctx, byte[] data, Stat stat) {
                System.out.println("-------async call back--------");
                System.out.println("async getdata::"+new String(data));

            }
        },"abc");
        System.out.println("-------async over--------");

输出结果:

-------async start--------
-------async over--------
-------async call back--------
async getdata::newdata01
  • 注册配置
    在不同的主机里都启动了service,但是启动的时候需要配置需要从哪里来呢?我们可以在本地写配置文件,但是随着集群的规模越来越大,如果有某一个配置项需要修改的话,那么运维人员需要登录每一台主机修改每一个配置文件。
       如果有一个地方,是所有service都能去独立访问的,那么只需要修改这一个位置其他人就都可以读取到,这个独立的位置可以是数据库、redis等任何一个可以共享的存储位置。但是为什么选择zookeeper呢? 就是因为zookeeper的回调机制,客户端除了能get到zookeeper中的节点数据,还能watch节点。只要有人更新zookeeper的节点数据了,并且这个客户端 watch 这个节点了,节点的修改一定会回调这个客户端的方法,这样客户端就会第一时间知道节点被修改了。任何使用zookeeper的客户端都不需要去轮询它是否被修改,成本低。

在这里插入图片描述

  • 分布式锁
    分布式做 镜像 负载均衡service的时候,要求service集群不能同时执行相同的业务逻辑,只要有一台service正在处理业务,其他service就必须处于阻塞状态。或者要求多个service不能同时访问同一个资源。
      像JVM提供的锁只能解决service内的线程同步问题,确不能解决分布式service之间的同步问题。这个时候就需要使用到分布式锁了。分布式锁是把锁的概念抽离到service服务外边,放到外边就可以用很多别的技术来实现,比如redis,数据库等能被多service同时访问即可,但是这些方式的实现方式都很繁琐并且准确性不高。
    在这里插入图片描述
    目前来说最方便的实现方式是使用zookeeper,zookeeper是高可用的并且视图统一。那么zookeeper实现分布式锁之前 需要注意的问题有哪些?
  1. 争抢所,只有一台service能获得锁。
  2. 获得锁的service突然挂掉了形成死锁。
  3. 获得锁的service成功处理完请求,释放锁。
  4. 锁被释放,如何让其他service知道。如果没有watch和call back怎么实现?
    (a) service主动轮询,发送心跳。但是如果service有1000台,同一时间只能有一台获得锁,则代表每个心跳都要有999台service主动轮询锁服务。 弊端:延迟,压力大。
    (b)使用zookeeper的wacth支持。watch监控也分为两种使用:
      · watch ParentNode。虽然能解决延迟问题,但是也有弊端,锁释放会回调 999 个service,999和节点会发生新一轮争抢锁,通信上会有一些压力。
      · sequence(序列化节点) + temporary节点,在parentNode下创建临时顺序节点,watch的时候watch跟当前service相关联的节点的前一个节点。一旦最小的节点释放了,只会给第二个节点发送事件。无延迟,压力小。

代码:
实现思路:主要方法通过watch和事件回调,参数回调机制(需要搭配CountDownLatch来实现),当服务创建节点设置锁时,会返回对于的序列化节点,把节点排序,做判断,节点所在集合里面的索引是不是第一个(注意集合里面的值不带/符号):

  1. 是第一个节点

    • 实现业务逻辑
    • CountDownLatch减一
    • 在CountDownLatch.await之间把对应节点删除,方便触发事件
  2. 不是

    • 通过exsits监听自己的上一个节点
    • 如果触发了事件,在调用获取方法进入判断是否为第一个节点
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值