2. zookeeper - 选主原理,数据一致性原理,watch,简单API

zookeeper - 选主原理,数据一致性原理,watch,简单API

    • 扩展性
    • 可靠性
    • 有序性
    • watch
    • 简单API

扩展性

zookeeper是一个分布式协调的工具,具有扩展性,可靠性,时序性。
站在微服务架构的角度上来说,每种服务都是不同的角色负责不同的功能,zookeeper有三种角色,分别是 leader,follower,observer。

其中leader和follower是主从模型,而主从模型可以做读写分离。leader可以触发增删改查,follower和observer可以触发read操作和转发write操作给leader。但是为什么触发read操作需要分为follower和observer两类呢?

因为zookeeper有一个特性就是响应快,响应快可以从两方面来讲:

  1. zookeeper正常运转,读写操作响应快。
  2. zookeeper 从 非可用状态 恢复到 可用状态 快(leader挂了 -> 选举出新的leader)。

那么zookeeper是何如从非可用状态 快速 恢复到可用状态呢?
举个例子:从1000人中选择出1个领导 对比 3个人中选出1个领导,哪个更快?换句话说就是 从1000台follower中选出一个leader 和 3台follower中选出1个 leader 哪个更快?答案一定是从3台follower中选出一台leader快。
假如现在有1000台zk做读写分离,如果leader挂了,从剩下的999 台 zk 中选取leader就需要999台zk进行投票,必然会影响性能,因此在设计上引入了observer的概念。
在zk集群中只有follower才能参与投票(选举投票的速度 和 follower 的数量 成正比),observer其实是比follower更低的级别,只负责read操作和转发write操作给leader 并不参与选主。leader挂掉的时候,只需要followers进行选举leader,选举成功后observer只需要追随leader同步数据和接收Client查询即可。

例如: 30台zkserver,可以配置leader + follower = 5台,其余zkserver全部为observer。

如何把一台zk更改为observer呢? 需要把zoo.cfg文件中指定的zk更改为:

server.1=192.168.116.135:2888:3888			
server.2=192.168.116.131:2888:3888
server.3=192.168.116.132:2888:3888
server.4=192.168.116.133:2888:3888:observer			// 把这台zk修改为observer服务器

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

可靠性

可靠性可以总结成一句话: 攘其外 必先 安其内。

其中 “攘其外” 表示对外提供数据的可靠性(数据的可靠 可用 一致性),“安其内” 则表示 服务的可靠性 (快速选leader)。简单的讲,就是服务能够在不可用状态 快速恢复 可用状态的前提下 ,并且对外提供可靠的数据。

zookeeper是如何保证数据的可靠性呢?
zk集群是一个分布式集群,在分布式环境中如何做到数据一致性?其中涉及Paxso分布式数据一致性算法。
参考:https://www.douban.com/note/208430424/

zk对paxos算法做了一个更简单的实现:ZAB协议(原子广播协议),ZAB协议作用在zk集群可用状态。

2. zookeeper - 选主原理,数据一致性原理,watch,简单API_第1张图片
ZAB协议(假设有leader和followers一共三台):

  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,该操作会被拒绝。

zookeeper是如何保证服务的可靠性呢?

快速选举leader分两种场景:

  1. 集群第一次启动(没有数据、版本、历史状态)。

  2. 重启集群,或者leader挂掉重选leader(之前运行时产生过数据,可能有的szkser数据多,有的数据少)。

并且每台zkserver都有自己的myid,和Zxid(事务ID)。

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

  1. 数据最全的(Zxid最高的,一定是过半通过的)。
  2. myid最大的。

比较规则:收到其他zkserver的数据后,都是先比较zxid,如果zxid相同,再比较myid。

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

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

算法分析:

参考:https://blog.csdn.net/Alyson_han/article/details/80044047

   在3.4.0后的Zookeeper的版本只保留了TCP版本的FastLeaderElection选举算法。当一台机器进入Leader选举时,当前集群可能会处于以下两种状态:
  · 集群中已经存在Leader。
  · 集群中不存在Leader。
   
   对于集群中已经存在Leader而言,此种情况一般都是某台机器启动得较晚,在其启动之前,集群已经在正常工作,对这种情况,该机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器而言,仅仅需要和Leader机器建立起连接,并进行状态同步即可。而在集群中不存在Leader情况下则会相对复杂,其步骤如下:

   (1) 第一次投票。 无论哪种导致进行Leader选举,集群的所有机器都处于试图选举出一个Leader的状态,即LOOKING状态,LOOKING机器会向所有其他机器发送消息,该消息称为投票。投票中包含了SID(服务器的唯一标识)和ZXID(事务ID),(SID, ZXID)形式来标识一次投票信息。假定Zookeeper由5台机器组成,SID分别为1、2、3、4、5,ZXID分别为9、9、9、8、8,并且此时SID为2的机器是Leader机器,某一时刻,1、2所在机器出现故障,因此集群开始进行Leader选举。在第一次投票时,每台机器都会将自己作为投票对象,于是SID为3、4、5的机器投票情况分别为(3, 9),(4, 8), (5, 8)。

    (2) 变更投票。 每台机器发出投票后,也会收到其他机器的投票,每台机器会根据一定规则来处理收到的其他机器的投票,并以此来决定是否需要变更自己的投票,这个规则也是整个Leader选举算法的核心所在,其中术语描述如下

   · vote_sid:接收到的投票中所推举Leader服务器的SID。

   · vote_zxid:接收到的投票中所推举Leader服务器的ZXID。

   · self_sid:当前服务器自己的SID。

   · self_zxid:当前服务器自己的ZXID。

每次对收到的投票的处理,都是对(vote_sid, vote_zxid)和(self_sid, self_zxid)对比的过程。

   规则一:如果vote_zxid大于self_zxid,就认可当前收到的投票,并再次将该投票发送出去。

   规则二:如果vote_zxid小于self_zxid,那么坚持自己的投票,不做任何变更。

   规则三:如果vote_zxid等于self_zxid,那么就对比两者的SID,如果vote_sid大于self_sid,那么就认可当前收到的投票,并再次将该投票发送出去。

   规则四:如果vote_zxid等于self_zxid,并且vote_sid小于self_sid,那么坚持自己的投票,不做任何变更。

结合上面规则,给出下面的集群变更过程。

   (3) 确定Leader。 经过第二轮投票后,集群中的每台机器都会再次接收到其他机器的投票,然后开始统计投票,如果一台机器收到了超过半数的相同投票,那么这个投票对应的SID机器即为Leader。此时Server3将成为Leader。

由上面规则可知,通常那台服务器上的数据越新(ZXID会越大),其成为Leader的可能性越大,也就越能够保证数据的恢复。如果ZXID相同,则SID越大机会越大。

有序性

zookeeper是有序的。 更新操作无论发送到哪个 zkserver 都会被转发到 leader 节点,leader单机有利于维护执行顺序会很容易,ZooKeeper为每个更新加盖一个zxid,zxid反映了所有ZooKeeper事务的顺序。后续操作可以使用该顺序实现更高级别的抽象,比如同步原语。

watch

watch是监控和观察的意思,如果想要了解watch需要先知道:

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

2. zookeeper - 选主原理,数据一致性原理,watch,简单API_第2张图片

有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

新建Maven项目,导入zookeeper客户端jar包:


  org.apache.zookeeper
  zookeeper
  3.4.6

代码:

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
         */
        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秒消失)
         */
        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);

    }
}

运行后对比项目日志的session和Linux里节点的Session:

2. zookeeper - 选主原理,数据一致性原理,watch,简单API_第3张图片

[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper, ooxx]
[zk: localhost:2181(CONNECTED) 1] get /ooxx
newdata01
cZxid = 0xa00000004
ctime = Wed Jun 17 14:14:32 CST 2020
mZxid = 0xa00000006
mtime = Wed Jun 17 14:14:32 CST 2020
pZxid = 0xa00000004
cversion = 0
dataVersion = 2
aclVersion = 0
ephemeralOwner = 0x172c0e6cd0a0000
dataLength = 9
numChildren = 0
[zk: localhost:2181(CONNECTED) 2] 

然后把连接的zkserver stop,Client程序会连接到健康的zkserver:

2. zookeeper - 选主原理,数据一致性原理,watch,简单API_第4张图片
使用异步的方式回调获得数据:


        /**
         * 异步的方式是没有返回值的,获取值之后会回调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

你可能感兴趣的:(zookeeper)