Zookeeper

1. Zookeeper入门
【1】.概述
Zookeeper是一个开源的分布式的,为分布式框架提供协调服务的Apache项目;
Zookeepr从设计模式角度看:是一个基于观察者模式设计的分布式服务管理框架,负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应;
Zookeeper = 文件系统 + 通知机制;
【2】.特点
1). Zookeeper:一个领导者,多个跟随者组成的集群;
2). 集群中只要有半数以上节点存活,Zookeeper集群就能够正常提供服务,所以Zookeeper适合安装奇数台服务器;
3). 全局数据一致,每个Server都保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的;
4). 更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行;
5). 数据更新原子性,一次数据更新要么成功,要么失败;
6). 实时性,在一定范围内,Client能够读到最新的数据;
【3】.数据结构
Zookeeper数据模型结构与Unix文件系统很类似,整体上可以看作一棵树,每个节点称做一个ZNode。每一个ZNode默认能够存储1MB的数据,每个ZNode都可以通过其路径唯一标识;
【4】.应用场景
1). 统一命名服务
在分布式环境下,经常需要对应用/服务进行统一命名,便于识别;
2). 统一配置管理
一般要求一个集群中,所有节点的配置信息是一致的,比如Kafka集群;
对配置文件修改后,希望能够快速同步到各个节点上;
EX:将配置信息写入到Zookeeper上的一个ZNode,然后各个客户端服务器都会监听这个ZNode;一旦ZNode中的数据被修改,Zookeeper将通知各个客户端服务器;
  3). 统一集群管理
  Zookeeper可以实现实时监控节点状态变化,可以将节点信息写入到Zookeeper上的一个ZNode,然后监听这个ZNode,即可以获取它的实时状态变化;
  4). 服务器动态上下线
  客户端能够实时观察到服务器的上下线的变化;
  5). 软负载均衡
  在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求;
2. Zookeeper安装
【1】.配置参数解读
1).tickTime=2000,通信心跳时间,Zookeeper服务器与客户端心跳时间,单位毫秒;
2).initLimit=10,LF初始通信时限;Leader和Follower初始连接时能容忍的最多心跳数;
3).syncLimit=5,LF同步通信时限;Leader和Follower之间的通信时间如果超过syncLimit * tickTime,则Leader认为follower死掉,从服务器列表中删除Follower;
4).dataDir:保存Zookeeper中的数据;注意:默认的tmp目录,容易被Linux系统定期删除,所以一般不用默认的tmp目录;
5).clientPort: 客户端连接端口,通常不做修改;
3. Zookeeper集群操作
【1】.集群安装
1). 在所有服务器上解压zookeeper安装包;
tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz
2). 在zookeeper目录下创建zkData文件夹;
mkdir zkData
3). 在文件夹下创建myid文件,并添加server对应的编号;
vim myid
4). 将配置文件zoo-sample.cfg的名字修改为zoo.cfg;
mv zoo-sample.cfg zoo.cfg
5). 修改配置文件中数据的存储路径;
dataDir=/opt/zookeeper/apache-zookeeper-3.5.7-bin/zkData
6). 配置文件中增加如下配置;
server.2 = x.x.x.x:2888:3888
server.3 = x.x.x.x:2888:3888
server.4 = x.x.x.x:2888:3888
server.A=B:C:D
A - 是一个数字,表示是第几号服务器,与myid文件中的数字相对应;
B - 是服务器的地址;
C - 是服务器Follower与集群中的Leader服务器交换信息的端口;
D - 是万一集群中的Leader服务器挂了,需要一个端口来重新进行选举,选举出一个新的Leader,该端口是用来执行选举时服务器相互通信的端口;
【2】.Zookeepr第一次启动选举机制
(1).服务器1启动,发起选举,将选票投给自己,此时服务器1票数为1,不够半数,选举无法完成,服务器1的状态保持为LOOKING;
(2).服务器2启动,再次发起选举,服务器1和服务器2分别投自己1票并且交换选票信息,此时服务器1发现服务器2的myid比自己目前投票推举的服务器的myid大,因此更改选票为推荐服务器2,此时服务器1为0票,服务器2为2票,没有半数以上结果,选举无法完成,服务器1,2保持LOOKING状态;
(3).服务器3启动,发起选举,服务器1和2更改选票为服务器3,服务器3获得3票,超过半数,当选Leader,服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;
(4).服务器4启动,发起一次选举,但由于服务器1,2,3不再是LOOKING状态,因此不会更改选票信息,服务器3依然为Leader,服务器4为FOLLOWER,并更改状态为FOLLOWING;
(5).服务器5同服务器4;

 

【3】.非第一次启动选举机制
(1).当Zookeeper集群中的一台服务器出现以下两种情况之一时,会开始进入Leader选举:
- 服务器初始化启动;
- 服务器运行期间,无法和Leader保持连接;
(2).当一台机器进入Leader选举流程时,当前集群可能会处于以下两种状态:
- 集群中本来存在Leader;
机器试图去选举Leader,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader建立连接,并进行状态同步即可;
- 集群中确实不存在Leader;
假设Zookeeper由5台服务器组成,SID分别为1,2,3,4,5,ZXID分别为8,8,8,7,7,此时服务器3为Leader,某一时刻,3和5服务器出现故障,因此开始进行选举;
选举规则:EPOCH大的直接胜出;
EPOCH相同的,ZXID大的胜出;
ZXID相同的,SID大的胜出;
【4】.zookeepr集群启动/停止脚本
 1 #!/bin/bash
 2 
 3 case $1 in
 4 "start")
 5     for i in 192.168.147.100 192.168.147.101 192.168.147.102
 6     do 
 7         echo "--启动 $i zookeeper"
 8         ssh $i "/opt/zookeeper/zookeeper/bin/zkServer.sh start"
 9     done
10 ;;
11 "stop")
12     for i in 192.168.147.100 192.168.147.101 192.168.147.102
13     do 
14         echo "--停止 $i zookeeper"
15         ssh $i "/opt/zookeeper/zookeeper/bin/zkServer.sh stop"
16     done
17 ;;
18 "status")
19     for i in 192.168.147.100 192.168.147.101 192.168.147.102
20     do 
21         echo "--查询状态 $i zookeeper"
22         ssh $i "/opt/zookeeper/zookeeper/bin/zkServer.sh status"
23     done    
24 ;;
25 esac

 

4. Zookeeper客户端命令行操作
【1】.节点信息(get -s /)
cZxid - 创建节点的事务zxid;
ctime - znode被创建的毫秒数;
mzxid - znode最后更新的事务zxid;
mtime - znode最后修改的毫秒数;
pZxid - znode最后更新的子节点zxid;
cversion - znode子节点变化号,znode子节点修改次数;
dataversion - znode数据变化号;
aclVersion - znode访问控制表的变化号;
ephemeralOwner - 如果是临时节点,这个是znode拥有者的session id,如果不是临时节点,则是0;
dataLength - znode的数据长度;
numChildren - znode的子节点数量;

【2】.节点类型
持久:客户端和服务端断开连接后,创建的节点不删除;
持久化目录节点/持久化顺序编号目录节点(有编号)
短暂:客户端和服务端断开连接后,创建的节点自己删除;
临时目录节点/临时顺序编号目录节点(有编号)
创建持久化节点: create /znode "z1"
创建带序号的持久化节点: create -s /znode "z2"
创建临时节点:create -e /znode "z3"
创建带序号的临时节点: create -e -s /znode "z4"
【3】.监听器及节点删除
监听器原理:
1). 首先有一个main()线程;
2). 在main()线程中创建一个zookeeper客户端,这里会创建两个线程,一个负责网络连接通信(connect),一个负责监听(listener);
3). 通过connect线程将注册的监听事件发送给zookeeper;
4). 在zookeeper的注册监听器列表中将注册的监听事件添加到列表中;
5). zookeeper监听到有数据或路径变化,会将这个消息发送给listener线程;
6). listener线程内部调用process()方法;
    监听内容:
1). 监听节点数据的变化; get -w /znode NodeDataChanged
2). 监听子节点增减的变化; ls -w /znode NodeChildrenChanged
注册一次监听,则只能监听一次,如果想要再次监听,则需要重新注册;
节点删除及查看:
删除节点:delete /znode
递归删除节点:deleteall /znode
查看节点状态:stat /znode
     设置节点内容:set /znode "XXX"
【4】.Idea创建节点
1). 创建zookeeper客户端;
   @Test
       public  void init() throws IOException {
           int sessionTimeout = 10000;
           String connectString = "192.168.75.100:2181,192.168.75.101:2181,192.168.75.102:2181";
           ZooKeeper zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
               @Override
               public void process(WatchedEvent watchedEvent) {
              }
          });
      }
2). 创建节点;
   @Test
   public  void create() throws IOException, InterruptedException, KeeperException {
       String node = zooKeeper.create("/atguigu", "songhongkang".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
  }
3). 获取子节点并监听其变化;
   @Test
   public  void getChildren() throws IOException, InterruptedException, KeeperException {
       List<String> children = zooKeeper.getChildren("/atguigu", new Watcher() {
           @Override
           public void process(WatchedEvent watchedEvent) {
               try {
                   List<String> children = zooKeeper.getChildren("/atguigu", new Watcher() {
                       @Override
                       public void process(WatchedEvent watchedEvent) {

                      }
                  });
                   System.out.println(children);
              } catch (KeeperException e) {
                   throw new RuntimeException(e);
              } catch (InterruptedException e) {
                   throw new RuntimeException(e);
              }
          }
      });
       System.out.println(children);
       Thread.sleep(Integer.MAX_VALUE);
  }      
4). 判断节点是否存在;
  @Test
   public  void exist() throws IOException, InterruptedException, KeeperException {
       Stat stat = zooKeeper.exists("/atguigu", false);
       System.out.println(stat);
  }      
所需jar包:        
  <dependency>
      <groupId>org.apache.zookeeper</groupId>
      <artifactId>zookeeper</artifactId>
      <version>3.5.3-beta</version>
  </dependency>
  <dependency>
      <groupId>org.apache.curator</groupId>
      <artifactId>curator-framework</artifactId>
      <version>4.0.1</version>
  </dependency>
  <dependency>
      <groupId>org.apache.curator</groupId>
      <artifactId>curator-recipes</artifactId>
      <version>4.0.1</version>
  </dependency>
  <dependency>
      <groupId>org.apache.curator</groupId>
      <artifactId>curator-client</artifactId>
      <version>4.0.1</version>
  </dependency>
【5】.写数据原理
1). 写入请求直接发送给Leader节点;

 

    2). 写入请求发送给follower节点;

 

5. 服务器动态上下线监听案例

 

【1】.服务器注册
public class DistributedServer {
   private int sessionTimeout = 10000;
   private String connectionString = "192.168.75.100:2181,192.168.75.101:2181,192.168.75.102:2181";

   public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
       DistributedServer distributedServer = new DistributedServer();
       //1. 获取zk连接;
       ZooKeeper zooKeeper = distributedServer.getConnection();
       //2. 注册服务器到zk集群;
       distributedServer.regist(zooKeeper,args[0]);
       //3. 启动业务逻辑;
       distributedServer.business();

  }

   private void business() throws InterruptedException {
       Thread.sleep(Integer.MAX_VALUE);
  }

   private void regist(ZooKeeper zooKeeper,String hostname) throws InterruptedException, KeeperException {
       String s = zooKeeper.create("/servers", hostname.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
       System.out.println(s + "上线了!");
  }


   public ZooKeeper getConnection() throws IOException {
       ZooKeeper zooKeeper = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
           @Override
           public void process(WatchedEvent watchedEvent) {

          }
      });
       return zooKeeper;
  }
}
【2】.客户端监听
public class DistributedClient {

   private String connectionString = "192.168.75.100:2181,192.168.75.101:2181,192.168.75.102:2181";
   private int sessionTimeout = 10000;

   public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
       DistributedClient distributedClient = new DistributedClient();
       //1. 获取zk连接;
       ZooKeeper zooKeeper = distributedClient.getConnetion();
       //2. 监听servers子节点下的增加和删除;
       distributedClient.watch(zooKeeper);
       //3. 业务逻辑;
       distributedClient.business();
  }

   private void business() throws InterruptedException {
       Thread.sleep(Integer.MAX_VALUE);
  }

   private void watch(ZooKeeper zooKeeper) throws InterruptedException, KeeperException {
       List<String> children = zooKeeper.getChildren("/servers", new Watcher() {
           @Override
           public void process(WatchedEvent watchedEvent) {
               try {
                   watch(zooKeeper);
              } catch (InterruptedException e) {
                   throw new RuntimeException(e);
              } catch (KeeperException e) {
                   throw new RuntimeException(e);
              }
          }
      });
       ArrayList<String> servers = new ArrayList<>();
       for (String child : children) {
           byte[] data = zooKeeper.getData("/servers/" + child, false, null);
           servers.add(new String(data));
      }
       System.out.println(servers);
  }

   private ZooKeeper getConnetion() throws IOException {
       ZooKeeper zooKeeper = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
           @Override
           public void process(WatchedEvent watchedEvent) {

          }
      });
       return zooKeeper;
  }
}    
【3】.测试

 

6. Zookeeper分布式锁案例

 

【1】.代码实现
public class DistributedLock {
   private final String connectionString = "192.168.75.100:2181,192.168.75.101:2181,192.168.75.102:2181";
   private final int sessionTimeout = 10000;

   private CountDownLatch countDownLatch = new CountDownLatch(1);

   private CountDownLatch waitDownLatch = new CountDownLatch(1);

   public ZooKeeper zooKeeper;

   private String previousPath;
   private String currentNode;

   public DistributedLock() throws IOException, InterruptedException, KeeperException {
       //1. 获取zk连接;
       zooKeeper = new ZooKeeper(connectionString, sessionTimeout, new Watcher() {
           @Override
           public void process(WatchedEvent watchedEvent) {
               if(watchedEvent.getState() == Event.KeeperState.SyncConnected){
                   countDownLatch.countDown();
              }
               if(watchedEvent.getType() == Event.EventType.NodeDeleted &&
                   watchedEvent.getType().equals(previousPath)){
                   waitDownLatch.countDown();
              }
          }
      });

       countDownLatch.await();

       //2. 判断父节点/locks是否存在;
       Stat stat = zooKeeper.exists("/locks", false);
       if(stat == null){
           zooKeeper.create("/locks","locks".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.PERSISTENT);
      }
  }

   //3. 对zk加锁
   public void zkLock() throws InterruptedException, KeeperException {
       //3.1 创建对应的临时的带序号的节点;
       currentNode = zooKeeper.create("/locks/" + "seq-", null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
       //3.2 判断创建的节点是否是最小的序号节点;如果是,获取到锁;如果不是,则监听前一个节点;
       List<String> children = zooKeeper.getChildren("/locks", false);
       // 如果children只有一个值,则获取锁;如果有多个节点,则需要判断;
       if (children.size() == 1) {
           return;
      }else{
           Collections.sort(children);
           String thisNode = currentNode.substring("/locks/".length());
           int index = children.indexOf(thisNode);
           if(index == -1){
               System.out.println("数据异常");
          } else if (index == 0) {
               return;
          }else{
               previousPath = "/locks/"+children.get(index-1);
               byte[] data = zooKeeper.getData(previousPath, true, null);
               waitDownLatch.await();
               return;
          }
      }
  }

   //4. 对zk解锁
   public void zkUnlock() throws InterruptedException, KeeperException {
       //4.1 删除节点;
       zooKeeper.delete(currentNode,-1);
  }
}
【2】.测试
public class DistributedLockTest {
   public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
       DistributedLock lock1 = new DistributedLock();
       DistributedLock lock2 = new DistributedLock();
       new Thread(new Runnable() {
           @Override
           public void run() {
               try {
                   lock1.zkLock();
                   System.out.println("线程1启动,获取到锁");

                   Thread.sleep(5000);
                   lock1.zkUnlock();
                   System.out.println("线程1释放锁");
              } catch (InterruptedException e) {
                   throw new RuntimeException(e);
              } catch (KeeperException e) {
                   throw new RuntimeException(e);
              }
          }
      }).start();

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

                   Thread.sleep(5000);
                   lock2.zkUnlock();
                   System.out.println("线程2释放锁");
              } catch (InterruptedException e) {
                   throw new RuntimeException(e);
              } catch (KeeperException e) {
                   throw new RuntimeException(e);
              }
          }
      }).start();
  }
}
【3】.Curator框架实现分布式锁
1). 添加依赖:curator-framework | curator-recipes | curator-client
<dependency>
           <groupId>org.apache.curator</groupId>
           <artifactId>curator-framework</artifactId>
           <version>4.0.1</version>
       </dependency>
       <dependency>
           <groupId>org.apache.curator</groupId>
           <artifactId>curator-recipes</artifactId>
           <version>4.0.1</version>
       </dependency>
       <dependency>
           <groupId>org.apache.curator</groupId>
           <artifactId>curator-client</artifactId>
           <version>4.0.1</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(5000);

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

                   lock1.release();
                   System.out.println("线程1再次释放锁");
              } catch (Exception e) {
                   throw new RuntimeException(e);
              }
          }
      }).start();

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

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

                   Thread.sleep(5000);

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

                   lock2.release();
                   System.out.println("线程2再次释放锁");
              } catch (Exception e) {
                   throw new RuntimeException(e);
              }
          }
      }).start();

  }

   private static CuratorFramework getCuratorFramework() {
       ExponentialBackoffRetry exponentialBackoffRetry = new ExponentialBackoffRetry(3000, 3);
       CuratorFramework client = CuratorFrameworkFactory.builder().connectString("192.168.75.100:2181,192.168.75.101:2181,192.168.75.102:2181")
              .connectionTimeoutMs(5000)
              .sessionTimeoutMs(5000)
              .retryPolicy(exponentialBackoffRetry).build();
       // 启动客户端;
       client.start();
       System.out.println("zookeeper 启动成功");
       return client;
  }
}
7. zookeeper源码分析
【1】.Paxos算法
Paxos算法:基于消息传递其具有高度容错性的一致性算法;
目的:快速正确的在一个分布式系统中对某个数据值达成一致,并且保证无论发生任何异常,都不会破坏整个系统的一致性;
Paxos算法描述:
1). 在一个Paxos系统中,首先会将节点划分为Proposer、Acceptor、Learner;
2). 一个完整的Paxos算法流程分为三个阶段:
Prepare准备阶段:
- Proposer向多个Acceptor发出Propose请求;
- Acceptor针对收到的Propose请求进行Promise;
Accept接受阶段:
- Proposer收到多数Acceptor承诺的Promise后,向Acceptor发出Propose请求;
- Acceptor针对收到的Promise请求进行Accept处理;
Learn学习阶段;
- Proposer将形成的决议发送给所有的Learners;
(1).Prepare:Proposer生成全局唯一且递增的Proposal ID,向所有Acceptor发送Propose请求,此时无需携带提案内容,只需要携带Proposal ID即可;
(2).Promise:Acceptor收到Propose请求后,做出“两个承诺,一个应答”;
不在接受Proposal ID小于等于当前请求的Propose请求;
不在接受proposal ID小于当前请求的Accept请求;
在不违背承诺的前提下,回复已经Accept过的提案中Proposal ID最大的提案的value和proposal ID,没有则返回空值;
(3).Propose:Proposal收到多数Acceptor的Promise应答后,从中选择Proposal ID最大的提案的value值,作为本次要发起的提案;
(4).Accept:Acceptor收到Propose请求后,在不违背承诺的前提下,接受并持久化当前Proposal ID和提案value;
(5).Learn:Proposer收到多数Acceptor的Accept后,将形成的决议发送给所有Learner;
【2】.ZAB算法
ZAB算法:在Paxos算法的基础上,将zookeeper设计为只有一个Leader负责处理外部的写事务请求,然后Leader会将数据同步到其他Follower节点。
ZAB协议内容:
ZAB协议包括两种基本模式:消息广播、崩溃恢复;
消息广播:

 

        崩溃恢复:

 

        数据恢复:

 

【3】.CAP理论
Consistency - 一致性
多个副本之间保证数据的一致性;
Available - 可用性
系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求,能够在有限的时间内返回结果;
Partition Tolerance - 分区容错性
分布式系统在遇到任何网络故障时,仍然能够保证对外提供满足一致性和可用性的服务;
Zookeeper保证的是CP;(在极端环境下,zookeeper可能会丢弃一些请求,消费者程序需要重新请求才能获得结果,所以,zookeeper不能保证服务可用性;)
进行Leader选举时,集群是不可用的;
 
posted on 2023-09-19 14:16  VaeSSAQ  阅读(18)  评论(0)    收藏  举报