Zookeeper

Zookeeper 概述

什么是Zookeeper?
  Zookeeper是一个开源的分布式的,为其他分布式监控提供协调服务的Apache项目。
 
 

Zookeeper 的特点

Zookeeper 有哪些特点?

  • Zookeeper是由一个Leader,和多个Follower组成的集群。

  • Zookeeper集群中只有半数以上的节点存活,Zookeeper集群才能正常提供服务。

  • Zookeeper集群全局数据统一,每个Zookeeper Server保存一份相同的数据副本,Client无论连接哪一台Server,数据都是一致的。

  • 由于有事务的存在,每次写操作都有事务id(zxid),所以数据更新具有原子性,要么成功,要么失败。
     
     

Zookeeper 的应用场景

1、提供记录IP地址的域名服务。
  如我们可以将www.baidu.com作为父节点,下面的子节点为真实的IP地址,便于标识。
请添加图片描述

2、统一配置管理。
  可将配置信息写入Zookeeper的一个ZNode中,然后各个客户端服务器监听这个ZNode,一旦这个ZNode中的数据发生变化,Zookeeper将通知各个客户端服务器。
3、统一集群管理。Zookeeper可以实现监控集群中各节点的状态变化。
  可将集群节点的信息写入Zookeeper的一个ZNode中,然后监听这个ZNode可获得它的实时状态变化。
请添加图片描述

4、服务器动态上下线。客户端能实时洞察服务器上下线的变化。

请添加图片描述
5、软负载均衡。
  让Zookeeper记录每台服务器的访问数,让访问最好的服务器去处理最小的客户端请求。

请添加图片描述

 
 

Zookeeper的数据结构

  Zookeeper的数据结构与文件系统很类似,属于树形结构
整体可以看做是一颗树,每个节点称为一个ZNode,每个ZNode都可以通过其路径唯一标识,如/znode1/leaf1,就可以找到根目录/下的/znode1下的leaf1。每个ZNode能存储的数据量非常小,大约能够存储1MB的数据,只能用来存储一些简单的配置信息,不能存储海量数据。

ZNode数据类型

  ZNode包括四种不同的类型,分别是 持久的、持久有序的、短暂的、短暂有序的。其中持久和短暂是指
1)持久:客户端与服务器断开连接之后,创建的节点不删除。
2)短暂:客户端与服务器断开连接之后,创建的节点被删除。
3)有序:创建有序的ZNode时,ZNode的名称后会附加一个顺序号,顺序号是一个单调递增的计数器,由父节点维护。

ZNode里面存储的信息

  ZNode里面包含了存储的数据(data)、访问权限(acl)、子节点引用(child)和节点状态信息(stat)。
其中
访问权限acl是指记录客户端对ZNode节点的访问权限。
子节点引用child是指当前节点的子节点引用。
节点状态信息stat包含Znode节点的状态信息,比如事务id,版本号,时间戳等。

 
 

Zookeeper的选举机制(重要)

关于Zookeeper的选举机制,我们先要记住它的半数以上选举机制,而且我们还需要看看是第一次启动还是非第一次启动。

Zookeeper第一次启动选举机制

假设我们的Zookeeper集群有4个节点。而刚开始我们的Zookeeper服务都是关闭的。
(1)节点1启动,会发起一次选举。节点1会投自己一票,不够半数以上(3票),选举无法完成,节点1状态保持为LOOKING状态;
(2)节点2启动,再次发起一次选举。节点1和节点2分别投自己一票然后交换选品信息,此时节点1发现节点2的myid比自己大,所以更改选票为节点2,不够半数以上(3票),选举无法完成,节点1和节点2状态保持为LOOKING状态;
(3)节点3启动,发起一次选举。过程和节点2发起选票一样,最后更改选票为节点3.此次投票结果:节点1和节点2为0票,节点3为3票,由于半数以上选举机制,节点3当选Leader,并更改状态为LEADING,节点1和节点2Follower,并且更改状态为FOLLOWING。
(4)节点4启动,发起一次选票。此时此时节点1/2/3已经半数LOOKING状态,不会更改选票信息。交换选票信息结果:节点3为3票,节点4为1票,此时节点4更改选票为节点3,并更改状态为FOLLOWING。
 

Zookeeper非第一次启动选举机制

当其中一台机器由于网络分区故障无法与集群和Leader保持连接时,该节点进入选举状态,但是由于半数以上选举机制,该节点无法成为Leader,只能不断与其他节点进行连接,在连接成功之后,集群可能会出现两种情况:
1、集群中本来就存在Leader。对于这种情况,该节点会被当中当前集群的Leader信息,对于该节点来说,只要与Leader重新进行连接并更新状态即可。
2、集群中确实不存在Leader。
这个情况下,我们需要了解几个核心参数:
(1)SID:服务器ID,服务器ID和配置中的myid一致。
(2)ZXID:事务ID,该ID和服务器对于客户端更新请求有关。
(3)Epoch:每个Leader任期的代号,没有Leader时这个在各节点中是相同的,每次投完一次票这个数据就会增加。
假设Zookeeper集群由4个节点组成,SID为1/2/3/4,ZXID为8887,此时节点3为Leader。某时刻,节点4和Leader都挂掉了。

那么只剩下节点1和节点2,他们的(Epoch,ZXID,SID)分别为(1,8,1)和(1,7,2)
非第一次启动并Leader不存在的Leader选举机制为:
(1)Epoch大的胜出
(2)Epoch相同,ZXID大的胜出
(3)ZXID相同,SID大的胜出
综上,节点1当选Leader。

 
 

Zookeeper 底层如何按照请求的先后顺序来处理的

Leader收到请求之后,会给每个请求分配一个全局唯一的递增的ZXID,如何把请求放入一个FIFO队列里面,之后就按照FIFO的策略发送给所有的Follower。

 
 

Zookeeper 实现分布式锁

分布式锁的概述、由单体锁引申到分布式锁具体见我另一篇博客:分布式锁的实现

分布式锁一般步骤:多个进程抢占同一个共享资源,先抢到互斥锁的,可以继续执行相应的代码,其他进程阻塞在外面,只有等待先抢占到锁的进程操作完之后释放锁(一般是delete),然后其他进程才可以重试抢占。

具体Zookeeper实现分布式锁的过程。

1)多个ZK客户端申请在ZK集群中创建临时顺序节点 (create -e -s /locks/seq-)
2)然后创建完临时节点之后,判断自己是不是当前节点下的最小节点
(1)是,获取到锁,可以进行执行相应代码。
(2)不是,对前一个节点进行监听(watch)。
3)获取到锁的进程处理完业务之后,delete节点释放锁,然后下面的节点会收到通知,重复步骤(2)。
这就是Zookeeper的大致的实现细节。我们给别人说,说上面的就行了。

但是具体的实现,还需要两个JUC下的减法器,CountDownLatch
一个是为了保证连接的健壮性而设置的,获取到连接就继续让它执行代码。
一个是监听上一个节点发生变化来使用的。

具体细姐看代码:

Zookeeper 实现分布式锁使用Java 原生API





public class DistributedLock {

    private final String connectString = "VM102:2181,VM103:2181,VM104:2181";
    private final int sessionTimeout = 2000;
    private final ZooKeeper zk;

    private CountDownLatch connectLatch = new CountDownLatch(1);
    private CountDownLatch waitLatch = new CountDownLatch(1);

    private String waitPath;
    private String currentMode;

    public DistributedLock() throws IOException, InterruptedException, KeeperException {

        // 获取连接
        zk = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                // connectLatch  如果连接上zk  可以释放
                if (watchedEvent.getState() == Event.KeeperState.SyncConnected){
                    connectLatch.countDown();
                }

                // waitLatch  需要释放
                if (watchedEvent.getType()== Event.EventType.NodeDeleted && watchedEvent.getPath().equals(waitPath)){
                    waitLatch.countDown();
                }
            }
        });

        // 等待zk正常连接后,往下走程序
        connectLatch.await();

        // 判断根节点/locks是否存在
        Stat stat = zk.exists("/locks", false);

        if (stat == null) {
            // 创建一下根节点
            zk.create("/locks", "locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
    }

    // 对zk加锁
    public void zklock() {
        // 创建对应的临时带序号节点
        try {
            currentMode = zk.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);

            // wait一小会, 让结果更清晰一些
            Thread.sleep(10);

            // 判断创建的节点是否是最小的序号节点,如果是获取到锁;如果不是,监听他序号前一个节点

            List<String> children = zk.getChildren("/locks", false);

            // 如果children 只有一个值,那就直接获取锁; 如果有多个节点,需要判断,谁最小
            if (children.size() == 1) {
                return;
            } else {
                Collections.sort(children);

                // 获取节点名称 seq-00000000
                String thisNode = currentMode.substring("/locks/".length());
                // 通过seq-00000000获取该节点在children集合的位置
                int index = children.indexOf(thisNode);

                // 判断
                if (index == -1) {
                    System.out.println("数据异常");
                } else if (index == 0) {
                    // 就一个节点,可以获取锁了
                    return;
                } else {
                    // 需要监听  他前一个节点变化
                    waitPath = "/locks/" + children.get(index - 1);
                    zk.getData(waitPath,true,new Stat());

                    // 等待监听
                    waitLatch.await();

                    return;
                }
            }


        } catch (KeeperException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }

    // 解锁
    public void unZkLock() {

        // 删除节点
        try {
            zk.delete(this.currentMode,-1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }

    }

}

使用Java原生API开发Zookeeper会出现的问题:
(1)会话连接是异步的,需要自己去处理,比如使用CountDownLatch。
(2)Watch 需要重复注册,不然不会生效。
(3)开发复杂性较高。

真正在生产环境下,我们不会使用Java 原生API的方式来实现Zookeeper分布式锁,我们会使用Curator 框架实现Zookeeper分布式锁。实现起来非常简单!

Zookeeper 实现分布式锁使用Curator 架实现Zookeeper分布式锁

导入依赖

      <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-client</artifactId>
            <version>4.3.0</version>
        </dependency>
public class CuratorLockTest {

    public static void main(String[] args) {

        // 创建分布式锁1
        InterProcessMutex lock1 = new InterProcessMutex(getCuratorFramework(), "/locks");

        // 创建分布式锁2
        InterProcessMutex lock2 = new InterProcessMutex(getCuratorFramework(), "/locks");

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock1.acquire();
                    System.out.println("线程1 获取到锁");

                    lock1.acquire();
                    System.out.println("线程1 再次获取到锁");

                    Thread.sleep(5 * 1000);

                    lock1.release();
                    System.out.println("线程1 释放锁");

                    lock1.release();
                    System.out.println("线程1  再次释放锁");

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    lock2.acquire();
                    System.out.println("线程2 获取到锁");

                    lock2.acquire();
                    System.out.println("线程2 再次获取到锁");

                    Thread.sleep(5 * 1000);

                    lock2.release();
                    System.out.println("线程2 释放锁");

                    lock2.release();
                    System.out.println("线程2  再次释放锁");

                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    private static CuratorFramework getCuratorFramework() {

        ExponentialBackoffRetry policy = new ExponentialBackoffRetry(3000, 3);

        CuratorFramework client = CuratorFrameworkFactory.builder().connectString("hadoop102:2181,hadoop103:2181,hadoop104:2181")
                .connectionTimeoutMs(2000)
                .sessionTimeoutMs(2000)
                .retryPolicy(policy).build();

        // 启动客户端
        client.start();

        System.out.println("zookeeper 启动成功");
        return client;
    }
}

 
 

生产环境下,Zookeeper安装多少台服务器合适

安装奇数台服务器合适。
这个问题我们来举个例子,加入我们安装了5台机器。1/2、3/4、5。
由于Zookeeper集群中只要有半数以上节点存活,Zookeeper集群就可以正常服务。

假设我们挂掉了1/2/3三台,剩下4/5。机器数2<3(半数以上),Zookeeper集群不能正常工作。
而如果我们再加一台机器6,并且1/2/3还是挂了,那么剩下4/5/6。机器数3<4(半数以上),所以集群还是不能正常工作。

刚才是5台服务器,挂3台,集群是瘫痪。而现在6台服务器,挂3台,集群还是瘫痪。所以说偶数台服务器每什么原因,不仅仅没有提高集群的可靠性,反而浪费了一台服务器,是集群间的通信延迟。

由于生产经验可以知道:

  • 10台服务器:3台ZK

  • 20台服务器:5台ZK

  • 100台服务器:11台ZK

  • 200台服务器:11台ZK
      
      
      

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值