ZooKeeper学习与总结

zookeeper:

—该总结来自于JavaGuide以及自己的扩充

1.1 概念:

zookeeper是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于zookeeper实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master选举、分布式锁和分布式队列等功能。eg:Dubbo架构中zookeeper中担任注册中心这一角色。

2.1重要概念:

  • zookeeper本身就是一个分布式程序(只要半数以上节点存活,zookeeper就能正常服务)
  • 为了保证高可用,最好是以集群来部署zk,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么zk本身任然是可用的。
  • zk将数据存在内存中,这也就保证了搞吞吐量和低延迟,但是内存限制了能够存储的容量不太大,此限制也是保持znode中存储的数据量较小的进一步原因。
  • zk是高性能的。在读多于写的应用程序中性能很高,因为写会导致所有的服务间同步状态,(读多于写是协调服务的典型场景)
  • zk有临时节点的概念。当创建临时节点的客户端会话一直保持活动,瞬时几点就一直存在。而当会话结束时瞬时节点被删除。持久节点是指一旦这个Znode被创建了,除非主动进行Znode的移除操作,否则Znode将一直保存在Zk上。
  • ZooKeeper 底层其实只提供了两个功能:①管理(存储、读取)用户程序提交的数据;②为用户程序提交数据节点监听服务。

会话(Session)

Session指的是Zookeeper服务器与客户端会话。在ZooKeeper中,一个客户端连接是指客户端和服务器之间的一个TCP长连接。客户端启动时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期就开启了。通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来自服务器Watch事件通知。Session的sessionTimeout值用来设置一个客户端会话的 超时时间,只要在sessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然是有效的。

在为客户端创建会话之前,服务端首先会为每个客户端都分配一个sessionID。由于sessionID是Zookeeper会话的一个重要标识,许多与会话相关的运行机制都是基于这个sessionID的,因此,无论是哪台服务器为客户端分配的sessionID,都必须保证全局唯一。

2.2 Znode:

在谈到分布式的时候,我们通常说的“节点"是指组成集群的每一台机器。然而,在Zookeeper中,“节点"分为两类,第一类同样是指构成集群的机器,我们称之为机器节点;第二类则是指数据模型中的数据单元,我们称之为数据节点一一ZNode。

Zookeeper将所有数据存储在内存中,数据模型是一棵树(Znode Tree),由斜杠(/)的进行分割的路径,就是一个Znode,例如/foo/path1。每个上都会保存自己的数据内容,同时还会保存一系列属性信息。

在Zookeeper中,node可以分为持久节点和临时节点两类。所谓持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。而临时节点就不一样了,它的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。另外,ZooKeeper还允许用户为每个节点添加一个特殊的属性:SEQUENTIAL.一旦节点被标记上这个属性,那么在这个节点被创建的时候,Zookeeper会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。

2.3版本:

在前面我们已经提到,Zookeeper 的每个 ZNode 上都会存储数据,对应于每个ZNode,Zookeeper 都会为其维护一个叫作 Stat 的数据结构,Stat中记录了这个 ZNode 的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode子节点的版本)和 cversion(当前ZNode的ACL版本)

2.4 Watcher

Watcher(事件监听器),是Zookeeper中的一个很重要的特性。Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是Zookeeper实现分布式协调服务的重要特性。

2.5 ACL

Zookeeper采用ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。Zookeeper 定义了如下5种权限。
![(https://img-blog.csdnimg.cn/20210128161607571.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0JvbmVzaQ==,size_16,color_FFFFFF,t_70)
在这里插入图片描述

三 Zookeeper特点:

  • 顺序一致性: 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。
  • 原子性: 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。
  • 单一系统映像 : 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。
  • 可靠性: 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。

四、Zookeeper设计目标

4.1 简单的数据模型

ZooKeeper 允许分布式进程通过共享的层次结构命名空间进行相互协调,这与标准文件系统类似。 名称空间由 ZooKeeper 中的数据寄存器组成 - 称为znode,这些类似于文件和目录。 与为存储设计的典型文件系统不同,ZooKeeper数据保存在内存中,这意味着ZooKeeper可以实现高吞吐量和低延迟。

在这里插入图片描述

4.2 可构建集群

为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么zookeeper本身仍然是可用的。 客户端在使用 ZooKeeper 时,需要知道集群机器列表,通过与集群中的某一台机器建立 TCP 连接来使用服务,客户端使用这个TCP链接来发送请求、获取结果、获取监听事件以及发送心跳包。如果这个连接异常断开了,客户端可以连接到另外的机器上。
在这里插入图片描述
上图中每一个Server代表一个安装Zookeeper服务的服务器。组成 ZooKeeper 服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 Zab 协议(Zookeeper Atomic Broadcast)来保持数据的一致性。

五 Zookeeper集群角色介绍

最典型集群模式: Master/Slave 模式(主备模式)。在这种模式中,通常 Master服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。

但是,在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了LeaderFollowerObserver 三种角色。如下图所示:
在这里插入图片描述
ZooKeeper 集群中的所有机器通过一个 Leader 选举过程来选定一台称为 “Leader” 的机器,Leader 既可以为客户端提供写服务又能提供读服务。除了 Leader 外,Follower 和 Observer 都只能提供读服务。Follower 和 Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此 Observer 机器可以在不影响写性能的情况下提升集群的读性能。
在这里插入图片描述

六 、ZooKeeper实现的ZAB协议

6.1 ZAB协议的介绍:

ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。

  • ZAB协议是专门为zookeeper实现分布式协调功能而设计。zookeeper主要是根据ZAB协议是实现分布式系统数据一致性。
  • zookeeper根据ZAB协议建立了主备模型完成zookeeper集群中数据的同步。这里所说的主备系统架构模型是指,在zookeeper集群中,只有一台leader负责处理外部客户端的事物请求(或写操作),然后leader服务器将客户端的写操作数据同步到所有的follower节点中。
    在这里插入图片描述
  • ZAB的协议核心是在整个zookeeper集群中只有一个节点即Leader将客户端的写操作转化为事物(或提议proposal)。Leader节点再数据写完之后,将向所有的follower节点发送数据广播请求(或数据复制),等待所有的follower节点反馈。在ZAB协议中,只要超过半数follower节点反馈OK,Leader节点就会向所有的follower服务器发送commit消息。即将leader节点上的数据同步到follower节点之上。
    在这里插入图片描述
  • ZAB协议中主要有俩种模式,第一是消息广播模式;第二是崩溃恢复模式

6.2 消息广播模式

1、在zk集群中数据副本的传递策略就是采用的广播模式在zk中数据副本的同步方式与二阶段相似但却又不同。二阶段提交的要求协调者必须等到所有的参与者全部反馈ACK确认消息后,再发送commint消息。要求所有的参与者要么全部成功要么全部失败。二阶段提交会产生严重的堵塞问题

2、ZAB协议中Leader等待follower的ACK反馈是指“只要半数以上的follower成功反馈即可,不需要得到全部的follower反馈ACK

在这里插入图片描述
4、zookeeper消息广播的具体步骤如下:

    4.1. 客户端发起一个写操作请求 
	4.2. Leader服务器将客户端的request请求转化为事物proposql提案,同时为每个proposal分配一个全局唯一的ID,ZXID。 
	4.3. leader服务器与每个follower之间都有一个队列,leader将消息发送到该队列 
	4.4. follower机器从队列中取出消息处理完(写入本地事物日志中)毕后,向leader服务器发送ACK确认。 
	4.5. leader服务器收到半数以上的follower的ACK后,即认为可以发送commit 
	4.6. leader向所有的follower服务器发送commit消息。

5、zk采用ZAB协议的核心就是只要有一台服务器提交了proposal,就要确保所有的服务器最终都能提交proposal,这也就是CAP最终实现一致性的一个体现。

6、leader服务器与每个follower之间都有一个单独的队列进行收发消息,使用队列消息可以做到异步解耦,leader和follower之间只要往队列中发送了消息即可 。如果使用同步方式容易引起阻塞。性能上要下降很多。

6.3、崩溃恢复模式

1、zookeeper集群中为保证任何所有进程能够有序的顺序执行,只能是leader服务器接受写请求,即使是follower服务器接受到客户端的请求,也会转发到leader服务器进行处理。

2、如果leader服务器发生崩溃,则zab协议要求zookeeper集群进行崩溃恢复和leader服务器选举。

3、ZAB协议崩溃恢复要求满足如下2个要求:

  • 3.1. 确保已经被leader提交的proposal必须最终被所有的follower服务器提交 。
  • 3.2. 确保丢弃已经被leader发出的但是没有被提交的proposal。

4、根据上述要求,新选举出来的leader不能包含未提交的proposal,即新选举的leader必须都是已经提交了的proposal的follower服务器节点。同时,新选举的leader节点中含有最高的ZXID。这样做的好处就是可以避免了leader服务器检查proposal的提交和丢弃工作。

5、leader服务器发生崩溃时分为如下场景:

  • 5.1. leader在提出proposal时未提交之前崩溃,则经过崩溃恢复之后,新选举的leader一定不能是刚才的leader。因为这个leader存在未提交的proposal。

  • 5.2 leader在发送commit消息之后,崩溃。即消息已经发送到队列中。经过崩溃恢复之后,参与选举的follower服务器(刚才崩溃的leader有可能已经恢复运行,也属于follower节点范畴)中有的节点已经是消费了队列中所有的commit消息。即该follower节点将会被选举为最新的leader。剩下动作就是数据同步过程。

6.4 数据同步

1、在zookeeper集群中新的leader选举成功之后,leader会将自身的提交的最大proposal的事物ZXID发送给其他的follower节点。follower节点会根据leader的消息进行回退或者是数据同步操作。最终目的要保证集群中所有节点的数据副本保持一致。

2、数据同步完之后,zookeeper集群如何保证新选举的leader分配的ZXID是全局唯一呢?这个就要从ZXID的设计谈起。

  • 2.1 ZXID是一个长度64位的数字,其中低32位是按照数字递增,即每次客户端发起一个proposal,低32位的数字简单加1。高32位是leader周期的epoch编号,至于这个编号如何产生(我也没有搞明白),每当选举出一个新的leader时,新的leader就从本地事物日志中取出ZXID,然后解析出高32位的epoch编号,进行加1,再将低32位的全部设置为0。这样就保证了每次新选举的leader后,保证了ZXID的唯一性而且是保证递增的。

在这里插入图片描述

6.5 ZAB协议原理

ZAB协议要求每个leader都要经历三个阶段:即发现、同步、广播。

  • 发现:即要求zk集群中必须选出一个leader进程,同时leader会维护一个follower可用列表。将来客户端可以跟这些follower节点进行通信。
  • 同步:leader要负责将本身的数据与follower完成同步,做到多副本存储。这样也就体现了分区容错性(zk最终实现的是CP原则,强一致性和分区容错性)follower将队列中未处理完的请求消费完成之后没写入本地日志中。
  • 广播:leader可以接受客户端新的proposal请求,将新的proposal请求广播给所有的follower。

七、Zookeeper实现分布式锁

7.1实现原理:

在这里插入图片描述

public class ZookeeperDistributedLock {
    public final static Joiner j = Joiner.on("|").useForNull("");

    //zk客户端
    private ZooKeeper zk;
    //zk是一个目录结构,root为最外层目录
    private String root = "/locks";
    //锁的名称
    private String lockName;
    //当前线程创建的序列node
    private ThreadLocal<String> nodeId = new ThreadLocal<>();
    //用来同步等待zkclient链接到了服务端
    private CountDownLatch connectedSignal = new CountDownLatch(1);
    private final static int sessionTimeout = 3000;
    private final static byte[] data= new byte[0];

    public ZookeeperDistributedLock(String config, String lockName) {
        this.lockName = lockName;

        try {
            zk = new ZooKeeper(config, sessionTimeout, new Watcher() {

                @Override
                public void process(WatchedEvent event) {
                    // 建立连接 ,WatcherEvent为观察者监听事件
                    if (event.getState() == KeeperState.SyncConnected) {
                    //获取事件状态(与客户端连接状态相关)枚举类型
					/*
					* KeeperState:Disconneced        连接失败 		 
					* KeeperState:SyncConnected		 连接成功	 		 
					* KeeperState:AuthFailed         认证失败		 
					* KeeperState:Expired            会话过期
					*/
                        connectedSignal.countDown();//只有当zookeeper成功建立了连接闭锁才减1
                    }
                }

            });
            
            connectedSignal.await();//当闭锁不为0的时候,一直在这里处于等待状态;
            Stat stat = zk.exists(root, false);//该方法的作用是为某个znode注册监听
            if (null == stat) {
                // 创建根节点
                zk.create(root, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    class LockWatcher implements Watcher {
        private CountDownLatch latch = null;

        public LockWatcher(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void process(WatchedEvent event) {

            if (event.getType() == Event.EventType.NodeDeleted)
                latch.countDown();
        }
    }

    public void lock() {
        try {

            // 创建临时子节点
            String myNode = zk.create(root + "/" + lockName , data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            System.out.println(j.join(Thread.currentThread().getName() + myNode, "created"));

            // 取出所有子节点
            List<String> subNodes = zk.getChildren(root, false);
            TreeSet<String> sortedNodes = new TreeSet<>();
            for(String node :subNodes) {
                sortedNodes.add(root +"/" +node);
            }
            
            String smallNode = sortedNodes.first();
            String preNode = sortedNodes.lower(myNode);//该方法返回的是比参数元素小的元素中的最大的那个

            if (myNode.equals( smallNode)) {
                // 如果是最小的节点,则表示取得锁
                System.out.println(j.join(Thread.currentThread().getName(), myNode, "get lock"));
                this.nodeId.set(myNode);
                return;
            }

            CountDownLatch latch = new CountDownLatch(1);
            Stat stat = zk.exists(preNode, new LockWatcher(latch));// 同时注册监听。
            // 判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
            if (stat != null) {
                System.out.println(j.join(Thread.currentThread().getName(), myNode,
                        " waiting for " + root + "/" + preNode + " released lock"));

                latch.await();// 等待,这里应该一直等待其他线程释放锁
                nodeId.set(myNode);
                latch = null;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    public void unlock() {
        try {
            System.out.println(j.join(Thread.currentThread().getName(), nodeId.get(), "unlock "));
            if (null != nodeId) {
                zk.delete(nodeId.get(), -1);
            }
            nodeId.remove();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值