ZooKeeper 分布式协调工具

一. ZooKeeper 概述

  1. 什么是Zookeeper: 分布式协调工具,(java语言编写的开源框架)

  2. Zookeeper的使用场景:
    注册中心(命名服务,将服务以持久节点.临时节点存入Zookeeper中),
    基于Zookeeper 实现负载均衡
    基于Zookeeper 实现分布式锁
    Zookeeper实现分布式配置中心,动态的管理配置文件(Apollo配置中心,SpringCloud 中有 config配置中心,)
    Zookeeper 实现分布式事务管理
    Zookeeper 选举策略(哨兵机制)
    Zookeeper 实现本地负载均衡(Dubbo 负载均衡原理)
    Zookeeper 实现消息中间件集群管理

    在这里插入图片描述

  3. Zookeeper 数据结构: 整体可以看为一棵树,每个节点称为一个ZNode, 每个ZNode上面默认存储1M数据,通过节点路径进行唯一标识

  4. Zookeeper 可以简单理解为文件系统+通知工具,从设计模式来讲基于观察者模式的分布式管理框架,负责存储和管理数据,接受观察者的注册,当被Zookeeper存储管理的数据发生改变时,负责通知已经注册到Zookeeper上的观察者作出相应的处理,

  5. 以Zookeeper作为dubbo注册中心来讲,当服务提供方启动时,会以自己全路径名为持久节点,分布式环境下每个该服务的ip为该持久节点下的临时节点存储到zookeeper上,服务消费方在第一次调用时,会通过需要调用的服务生提供者的全类名,在Zookeeper上寻找到对应的持久节点,进而获取到了相应的服务的IP地址列表,缓存在本地,然后通过本地进行负载均衡选在调用指定的服务

  6. Zookeeper会对服务提供方存储的节点数据进行监听,当发生改变,或有监听的服务宕机下线时,通过一个Listener监听线程调用Watcher中的process()方法进行指定的操作,例如调用服务消费方更新拿到的服务提供方数据

在这里插入图片描述

二. ZooKeeper Windows 单机版安装

  1. Zookeeper下载地址 : https://zookeeper.apache.org/
    在这里插入图片描述
  2. 将下载后的压缩包解压到非中文路径下
  3. 在解压后的文件夹中创建data文件夹,与log文件夹(data用来存储数据,log用来记录日志)
  4. 找到cnf文件夹,复制zoo_sample.cfg副本,重命名为zoo.cfg
  5. 打开zoo.cfg文件,修改 dataDir值指向新创建的data文件夹路径,添加dataLogDir值为新创建的log文件夹路径,注意"/"
  6. 查看zoo.cfg文件中的 clientPort 端口号是否正确,是否被占用
  7. (集群环境下zoo.cfg文件中添加多个zookeeper server 通信ip+端口号,并且在每个data文件夹中创建myid.txt文件,去掉后缀,编辑,第一台输入1, 第二台输入2…)
  8. 启动 找到bin文件夹,点击zkServer.Cmd 文件

在这里插入图片描述

三. ZooKeeper 集群环境下选举过程

  1. Zookeeper 集群环境下半数以上存活则可正常选举,正常运行(所以推荐奇数)
  2. 在集群环境中启动Zookeeper时,每一台机器都会尝试找到Leader,并以自己为Leader进入选举状态,进行投票,每个服务器会传递一个包含锁选举的服务器的myid与ZXID,通过这两个数据判断选择为leader的服务器是哪一台,
  3. 集群的每一条服务器接收到来自各个服务器的投票,首先判断该投票是否有效,是否是本轮投票,是否来自LOOKING状态的服务器
  4. 每一台服务器将其它服务器的投票与自己的进行比较,ZXID比较大的服务器优先作为Leader,如果ZXID相同,则比较myid较大的服务器作为Leader服务器
  5. 服务器统计投票信息,判断是否已经有过半机器接受到相同的投票信息,当统计出集群中已经有半数以上的同意,此时便认为已经选出了Leader
  6. 确定了leader后,其它服务器会改变自身状态,除了选举出来的leader,其它都变为follwer
  7. 当Leader节点宕机,会进入新一轮Leader选举,首先整个Zookeeper集群将暂停对外服务,其它存活的follwer节点会变更状态为LOOKING,进入选举

四. ZooKeeper 存储数据的过程

当向Zookeeper中注册存储数据时,客户端发送一个"写"请求,而Zookeeper集群时分为Leader与Follwer,假设接收请求的是Follwer zk1,当前节点会将请求转发给Leader,Leader再将这个"写"请求广播给所有的Follwer,所有Follewer都会接收请求执行"写"存储数据的操作(各个Zookeeper上存储了相同的数据),执行成功后通知Leader,当Leader接收到Follwer写成功的结果超过半数以上则说明写成功了,在将请求结果发送给zk1

五. ZooKeeper 监听

Zookeeper内部会创建创建启动两个线程,一个正常的业务逻辑线程Connect线程,出了可以实现对数据的增删改查以外,还可以设置监听,还有一个专门用来监听的线程Listener,该线程运行监控设置开启监听的数据,当发生监听行为时,会自动调用Zookeeper 中持有的Watcher中的process()方法,该方法中可以拿到监听的事件WatchedEvent对象,通过该对象可以获取到当前发生的是什么类型的事件,事件状态等

六. java 操作 ZooKeeper

  1. ZooKeeper API 方法: ZooKeeper API文档
  2. java 中操作 Zookeeper 需要引入依赖,创建Zookeeper 连接对象,通过连接对象实现对节点数据的增删改查,事件通知等
		<!--zookeeper依赖-->
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.4.13</version>
        </dependency>
        <!--管理zookeeper需要用到的-->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>4.0.1</version>
        </dependency>

基本用法

  1. ZooKeeper 连接的创建
  2. 创建ZooKeeper对象时传递的Watcher中的process()通知方法
  3. 设置监听指定节点是否存活
  4. 设置监听指定节点下的数据变化
  5. 创建节点(注意持久节点,与临时节点,重名节点与上层节点不存在的问题)
  6. 获取指定节点的数据
  7. 关闭ZooKeeper连接
public class Test {

    //信号量,阻塞程序执行,用户等待zookeeper连接成功,发送成功信号
    //设置信号量为1,通过信号量来控制代码执行顺序
    //当zookeeper连接获取成功事件监听成功时 CountDownLatch 调用 countDown()
    //对信号量做累减操作,在后续Zookeeper创建节点时首先通过CountDownLatch 调用
    //await() 判断如果不为0,则阻塞等待,为0才继续向下执行
    private static final CountDownLatch countDownLatch = new CountDownLatch(1);


    public static void main(String[] args) throws IOException, KeeperException, InterruptedException {

        ZooKeeper zooKeeper = null;
        try{
             //1.创建Zookeeper 连接
             //参数一: Zookeeper 服务器连接地址
             //参数二: 连接超时时间
             //参数三: Watcher,通过Watcher监听节点数据
            zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new Watcher() {
            
            	//2.当被设置开启监听是否存在的的节点消失或启动时
            	//当被设置开启监听的节点数据发生改变时,都会执行该方法
            	//可以在该方法中自定义被监听者发生变化时需要完成的功能,例如通知消费方重新获取节点上的数据
                @Override
                public void process(WatchedEvent watchedEvent) {
                    // 获取事件状态
                    Event.KeeperState keeperState = watchedEvent.getState();
                    // 获取事件类型
                    Event.EventType eventType = watchedEvent.getType();
                    if (Event.KeeperState.SyncConnected == keeperState) {
                        if (Event.EventType.None == eventType) {
                            //信号量累减操作(1-1变为0)
                            countDownLatch.countDown();
                            System.out.println("zk 启动连接...");
                        }
                    }
                    //==========可以编写触发监听执行时的业务代码(下方只是模拟可能会报错)===========
                    children = zooKeeper .getChildren("/", true);
                    for (String child : children) {
                        System.out.println(child);
                    }
                    //==================模拟触发监听执行的逻辑end==========================
                }
            });
            //信号量判断是否为0,不为0,说明Zookeeper连接未创建成功则阻塞等待
            countDownLatch.await();

            String node = "/test";

            //3.获取并设置监听node节点是否存在 true为设置开启监听
            Stat stat = zooKeeper.exists(node,true);

            //4.获取node节点下的所有子节点,并设置开启监听node节点下的变化
            List<String> childrenList = zooKeeper.getChildren(node, true);

            
             //5.创建节点数据
             //参数一: 创建节点名称(注意点:假设当天添加"/aaa/test"节点,
             //       但是"/test'上层的父节点不存在,则会报错)
             //参数二: 存放的数据
             //参数三: 设置当前存放的数据的允许访问权限Ids.OPEN_ACL_UNSAFE 为开放
             //参数四: 当前创建的节点类型 CreateMode
             
             //节点类型: CreateMode 常量值
             //EPHEMERAL 临时节点 (注意点:临时节点在连接断开时会自动消失)
             //EPHEMERAL_SEQUENTIAL 临时节点,
             //    与EPHEMERAL的不同之处在于,假设当
             //      前以及存储了一个"/test"临时节点数据,
             //      后续再去存储"/test"向同的临时节点,
             //      该类型情况下会自动使用id自增去设置唯一性
             //     第二个"/test"实际存储的就可能是"/test0001"
             //PERSISTENT 持久节点,持久节点会永久的保存在硬盘上
             // PERSISTENT_SEQUENTIAL 持久节点,与上面一样,会使用id进行唯一设置
            String nodeResult = zooKeeper.create(node+"/test", "测试数据".getBytes(), 
            		ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
          
           //6.获取指定节点的数据
            byte[] resultByte = zooKeeper.getData(node+"/test", true, null);
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            //7.关闭连接
            if(null != zooKeeper){
                zooKeeper.close();
            }
        }

    }

}

模拟服务节点动态上下线,动态通知消费服务

1. 分析

  • 服务提供方启动时,使用一个可以标识当前服务的变量,存储到ZooKeeper上,以dubbo为例: dubbo/全类名/providers/实际ip, dubbo/全类名/providers为持久节点,实际ip为临时节点
  • 开启监听,监听临时节点是否存在(临时节点是跟随连接的,连接断开临时节点消失,也就是服务提供方宕机下线了),监听指定节点数据变化以dubbo为例,监听"dubbo/全类名/providers"下的所有数据,
  • 服务消费方启动时,将消费方存储到Zookeeper上,以dubbo为例,服务消费方中要调用服务提供方的API,存储的是"dubbo/调用的服务提供方的全类名/customers/实际ip"
  • ZooKeeper 的 Watcher接口中的回调方法process()中,编写当监控的服务提供方发生改变,上下线时,需要执行的逻辑代码,例如获取Zookeeper上的服务消费方,进行更新等

2. 创建服务提供方

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;

public class ServerTest {
	//main方法执行
    public static void main(String[] args) throws Exception {
        //1.获取ZooKeeper连接
        ZooKeeper zooKeeper = getConnect();
        //2.创建服务提供方
        ServerTest server = new ServerTest(zooKeeper, "/register.server", "/provider", "1.1.1.1");
        //3.向ZooKeeper上注册,也就是存储能代表当前服务的数据
        //服务消费方启动后获取这些数据进行调用
        server.registerServer();
        //4.模拟程序运行(防止程序运行完毕连接断开后临时节点消失)
        server.serverRun();
    }

    //ZooKeeper 连接
    public ZooKeeper zooKeeper;

    //当前服务提供方的全类名
    public String classPathName;

    //服务提供方节点
    public String provider;

    //当前服务提供方IP,ZooKeeper上存储的真实数据
    //服务消费方获取这个ip调用到指定的服务器
    public String ip;

    public ServerTest(ZooKeeper zooKeeper, String classPathName, String provider, String ip) {
        this.zooKeeper = zooKeeper;
        this.classPathName = classPathName;
        this.provider = provider;
        this.ip = ip;
    }

    //模拟向ZooKeeper注册服务提供方
    public void registerServer() throws Exception {
        //1.判断当前是否存在classPathName节点,如果不存在再创建,防止同名节点报错
        Stat stat = zooKeeper.exists(classPathName, false);
        if (null == stat) {
            //2.第一层节点创建为公开的,持久节点例如dubbo中的全类名
            zooKeeper.create(classPathName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        //3.向节点中存储当前服务提供方的实际Ip,为临时节点,原因是临时节点会跟随连接消失,如果消失了说明宕机了
        //可以通过监听做到动态通知
        zooKeeper.create(classPathName + provider, ip.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
    }

    //模拟服务运行
    public void serverRun() throws InterruptedException {
        Thread.sleep(Long.MAX_VALUE);
    }

    //获取Zookeeper连接
    public static ZooKeeper getConnect() throws IOException {
        ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 10000, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                // 获取事件状态
                Event.KeeperState keeperState = watchedEvent.getState();
                // 获取事件类型
                Event.EventType eventType = watchedEvent.getType();
                if (Event.KeeperState.SyncConnected == keeperState) {
                    if (Event.EventType.None == eventType) {
                        System.out.println("zk 启动连接...");
                    }
                    //节点不存在事件
                    if (Event.EventType.NodeDataChanged == eventType) {
                        System.out.println("zk 节点不存在事件...");
                    }
                    //节点数据修改事件
                    if (Event.EventType.NodeChildrenChanged == eventType) {
                        System.out.println("zk 节点数据修改事件...");
                    }
                }
            }
        });
        return zooKeeper;
    }

}

3. 创建服务消费方

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class ClientTest {
	//main方法运行
    public static void main(String[] args) throws Exception {
    	//1.获取连接
        ZooKeeper zooKeeper = getConnect();
        //2.创建服务消费方
        ClientTest client = new ClientTest(zooKeeper, "/register.server", "/customer", "/provider", "2.2.2.2");
        //3.注册服务消费方
        client.registerClient();
        //4.开启监听
        client.monitor();
        //5.输出消费方获取的服务方ip
        client.infoId();
        client.clientRun();
    }

    //ZooKeeper 连接
    public ZooKeeper zooKeeper;
    //当前服务消费方需要调用非服务提供方的全类名(消费方持有提供方)
    public String classPathName;
    //服务消费方节点
    public String customer;
    //服务消费方ip,当监听到数据有变化时,获取该Ip将变化后的数据发送给指定的消费方
    public String ip;
    //服务提供方节点路径,消费方与对应的提供方都是存储在同一个全类名下
    //在监听时只要监听提供方的即可
    public String provider;
    //获取服务提供方ip存储到该容器中
    public List<String> serverIpList = new ArrayList<>();

    public ClientTest(ZooKeeper zooKeeper, String classPathName, String customer, String provider, String ip) {
        this.zooKeeper = zooKeeper;
        this.classPathName = classPathName;
        this.customer = customer;
        this.provider = provider;
        this.ip = ip;
    }

    //模拟向ZooKeeper中注册服务消费方
    public void registerClient() throws Exception {
        Stat stat = zooKeeper.exists(classPathName, false);
        if (null == stat) {
            zooKeeper.create(classPathName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        }
        zooKeeper.create(classPathName + customer, ip.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
    }


    //设置开启监听
    public void monitor() throws Exception {
        //1.设置监听"classPathName+provider" 节点是否存在
        zooKeeper.exists(classPathName + provider, true);
        //2.设置监听"classPathName+provider"节点的数据变化,并获取节点中的数据,也就是服务提供方的真实Ip
        List<String> childrenList = zooKeeper.getChildren(classPathName, true);
        //3.将获取到的服务提供方ip存储给服务消费方
        for (String children : childrenList) {
            if (provider.equals("/" + children)) {
                byte[] result = zooKeeper.getData(classPathName + provider, true, null);
                serverIpList.add(new String(result));
            }
        }
    }

    //模拟服务运行
    public void clientRun() throws InterruptedException {
        Thread.sleep(Long.MAX_VALUE);
    }

    public void infoId() {
        serverIpList.stream().forEach(sIp -> System.out.println("获取到的服务提供方ip为:" + sIp));
    }

    //获取Zookeeper连接
    public static ZooKeeper getConnect() throws IOException {
        ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 10000, new Watcher() {
            @Override
            public void process(WatchedEvent watchedEvent) {
                // 获取事件状态
                Event.KeeperState keeperState = watchedEvent.getState();
                // 获取事件类型
                Event.EventType eventType = watchedEvent.getType();
                if (Event.KeeperState.SyncConnected == keeperState) {
                    //启动事件
                    if (Event.EventType.None == eventType) {
                        System.out.println("zk 启动连接...");
                    }
                    //删除节点事件
                    if (Event.EventType.NodeDeleted == eventType) {
                        System.out.println("zk 节点不存在事件...");
                    }
                    //节点数据修改事件
                    if (Event.EventType.NodeChildrenChanged == eventType) {
                        System.out.println("zk 节点数据修改事件...");
                    }
                }
            }
        });
        return zooKeeper;
    }
}
发布了48 篇原创文章 · 获赞 0 · 访问量 569

猜你喜欢

转载自blog.csdn.net/qq_29799655/article/details/105570415
今日推荐