手写RPC框架-zookeeper搭建和集群

127 阅读14分钟

手写RPC框架-zookeeper搭建和集群

一、概述

  • 概念:解释ZooKeeper是一个开源的分布式协调服务组件,用于构建可靠的分布式系统。它提供了一个高性能的、有序的、可靠的分布式数据存储和协调服务,简单的理解为一个内存数据库,特殊的数据结构和一些特性,他可以实现一些特殊的功能。
  • 应用场景:在分布式系统中我们经常使用ZooKeeper实现服务注册发现、分布式锁、配置管理、命名服务和分布式协调等功能。
  • 数据模型:ZooKeeper的数据模型是一个类似于文件系统的层次结构。每个节点称为ZNode,它可以存储数据和子节点。zookeeper中的ZNode可以分为持久节点和临时节点。
  • Watcher机制:ZooKeeper允许开发人员在节点上设置监视点,以便在节点发生更改时接收通知。

二、数据模型

ZooKeeper的数据模型是一个类似于文件系统的层次结构,*被组织成一个树形结构,每个节点称为ZNode。ZNode是ZooKeeper中的基本数据单元,它可以存储*数据和子节点

image-20230614172740978-c8e78ddd.png 以下是ZooKeeper数据模型的详细说明:

  1. 树形结构:

    ZooKeeper的数据模型是一个树形结构,类似于文件系统的目录结构。整个树由根节点(称为"/")开始,每个节点可以有多个子节点。

    每个节点都有一个路径来唯一标识它,路径是由斜杠(/)分隔的一系列名称组成。

  2. ZNode:

    ZNode是ZooKeeper的基本数据单元,类似于文件系统中的文件或目录。

    每个ZNode可以存储一个字节数组作为其数据,可以是任意类型的数据,例如配置信息、状态信息等

    每个ZNode还可以有多个子节点,形成层次结构

  3. 持久节点(Persistent Node):

    持久节点在创建后一直存在,直到主动删除。

    持久节点的数据和子节点都是持久的,即它们在节点创建后仍然存在,直到被显式删除。

  4. 临时节点(Ephemeral Node):

    临时节点的生命周期与客户端会话绑定,当会话结束(例如客户端关闭或与ZooKeeper集合的连接断开)时,临时节点将自动被删除

    临时节点的数据和子节点也将随之被删除。

  5. 顺序节点(Sequential Node):

    顺序节点在创建时会自动分配一个全局唯一且递增的编号。

    顺序节点的编号由ZooKeeper集合维护,可以用于实现有序性或生成全局唯一的标识符。

三、watcher机制

Watcher机制是ZooKeeper中非常重要的概念,它允许客户端在ZooKeeper上设置监视点,以便在节点发生变化时接收通知。Watcher机制使得开发人员可以及时获取关于数据变化的通知,以便采取相应的操作。

以下是Watcher机制的详细说明:

  1. 注册Watcher:

    客户端可以通过在对节点进行操作时注册Watcher来设置监视点。例如,在创建、更新或删除节点时都可以注册Watcher。

    客户端在注册Watcher时需要指定监视的路径和Watcher对象。当指定路径的节点发生变化时,ZooKeeper会将通知发送给客户端。

  2. Watcher通知:

    当一个节点发生变化时,ZooKeeper会将通知发送给注册了Watcher的客户端。

    Watcher通知是一次性的,也就是说,当客户端接收到Watcher通知后,该Watcher将被移除,需要客户端重新注册Watcher才能再次接收通知。

  3. Watcher的类型:

    数据变更触发的Watcher(Data Watcher):当节点的数据发生变化时触发,例如节点的值被修改。

    子节点变更触发的Watcher(Child Watcher):当节点的子节点发生变化时触发,例如新增或删除子节点。

    连接状态变更触发的Watcher(Existence Watcher):当客户端与ZooKeeper集合的连接状态发生变化时触发,例如连接断开或重新连接。

  4. Watcher的触发流程:

    当一个节点发生变化时,ZooKeeper首先会将通知发送给与该节点相关的Watcher。

    客户端接收到Watcher通知后,需要处理通知并根据需要采取相应的操作,例如重新读取数据或重新注册Watcher。

  5. Watcher的注意事项:

    Watcher通知是异步的,客户端需要保证处理Watcher通知的代码是线程安全的。

    Watcher通知是最终一致性的,即ZooKeeper不能保证Watcher通知的实时性,只能保证最终一致性。

    Watcher通知是有序的,当多个Watcher同时触发时,ZooKeeper会按照注册顺序依次发送通知

通过理解Watcher机制,开发人员可以更好地利用ZooKeeper来实现分布式系统中的实时数据变更和协调操作。Watcher机制提供了一种高效且可靠的方式,使得分布式系统能够实时响应节点变化,保持数据的一致性。

四、安装和基本操作

ZooKeeper提供了一组命令行工具(CLI)来与ZooKeeper集群进行交互。以下是一些常见的ZooKeeper命令:

  1. connect:
    • 连接到ZooKeeper集群。
    • 语法:connect <host:port>
  2. ls:
    • 列出指定路径下的子节点。
    • 语法:ls <path>
  3. create:
    • 创建一个节点。
    • 语法:create <path> <data>
  4. get:
    • 获取指定节点的数据。
    • 语法:get <path>
  5. set:
    • 设置指定节点的数据。
    • 语法:set <path> <data>
  6. delete:
    • 删除指定节点。
    • 语法:delete <path>
  7. stat:
    • 获取指定节点的详细信息,包括数据版本、ACL权限等。
    • 语法:stat <path>
  8. getAcl:
    • 获取指定节点的ACL(访问控制列表)权限信息。
    • 语法:getAcl <path>
  9. setAcl:
    • 设置指定节点的ACL权限信息。
    • 语法:setAcl <path> <acl>
  10. quit/exit:
    • 退出ZooKeeper命令行工具。
    • 语法:quitexit

这些命令可以通过在ZooKeeper命令行工具中输入相应的命令来执行。使用这些命令,可以查看和操作ZooKeeper集群中的节点和数据,管理ACL权限,以及执行其他与ZooKeeper相关的操作。

五、java操作

1、基础api

当使用Java客户端连接ZooKeeper时,您可以使用Maven来管理项目依赖。下面是使用Maven连接ZooKeeper的基本步骤:

1、创建Maven项目:首先,创建一个新的Maven项目。您可以使用任何IDE或命令行工具来执行此操作。

2、添加Maven依赖:在您的Maven项目的pom.xml文件中,添加以下依赖项来引入ZooKeeper客户端库:

<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.8.1</version>
</dependency>

在上述代码中,我们指定了ZooKeeper客户端库的groupIdorg.apache.zookeeperartifactIdzookeeper,并指定了所需的版本号。您可以根据需要更新版本号。

3、测试用例

public class ZookeeperTest {

    ZooKeeper zooKeeper;

    @Before
    public void createZk(){

        // 定义连接参数
        String connectString = "127.0.0.1:2181";
        // 定义超时时间
        int timeout = 10000;
        try {
            // new MyWatcher() 默认的watcher
            zooKeeper = new ZooKeeper(connectString,timeout,new MyWatcher());
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void testCreatePNode(){
        try {
            String result = zooKeeper.create("/ydlclass", "hello".getBytes(),
                    ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            System.out.println("result = " + result);
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                if(zooKeeper != null){
                    zooKeeper.close();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Test
    public void testDeletePNode(){
        try {
            // version: cas  mysql  乐观锁,  也可以无视版本号  -1
            zooKeeper.delete("/ydlclass",-1);
        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                if(zooKeeper != null){
                    zooKeeper.close();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Test
    public void testExistsPNode(){
        try {
            // version: cas  mysql  乐观锁,  也可以无视版本号  -1
            Stat stat = zooKeeper.exists("/ydlclass", null);

            zooKeeper.setData("/ydlclass","hi".getBytes(),-1);

            // 当前节点的数据版本
            int version = stat.getVersion();
            System.out.println("version = " + version);
            // 当前节点的acl数据版本
            int aversion = stat.getAversion();
            System.out.println("aversion = " + aversion);
            // 当前子节点数据的版本
            int cversion = stat.getCversion();
            System.out.println("cversion = " + cversion);


        } catch (KeeperException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            try {
                if(zooKeeper != null){
                    zooKeeper.close();
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

注: (1)version :当前结点的数据内容版本号;

(2)cversion:当前数据子节点的版本号;

(3)aversion:当前数据节点ACL变更的版本号;

2、watcher机制

watcher机制的本质是向zookeeper注册关心的事件,然后在本地存储钩子函数,当事件发生后调用钩子函数,如下图:

1687230238436-d8cbb3ba.png

概念

  • Zookeeper提供了数据的发布/订阅功能。多个订阅者可监听某一特定主题对象(节点)。当主题对象发生改变(数据内容改变,被删除等),会实时通知所有订阅者。该机制在被订阅对象发生变化时,会异步通知客户端,因此客户端不必在注册监听后轮询阻塞。
  • Watcher机制实际上与观察者模式类似,也可看作观察者模式在分布式场景中给的一种应用。

特性

特性说明
一次性Watcher是一次性的,一旦触发,就会被移除,再次使用需要重新注册
轻量级WatcherEvent是最小的通信单元,结构上只包含连接状态、事件类型和节点路径,并不会告诉数据节点变化前后的具体情况
时效性watcher只有在当前session彻底时效时才会无效,若在session有效期内重新连接成功,则watcher依然存在

ZooKeeper中的读取操作getData、exist、getChildren 等都可以使用指定参数为节点设置监听。

Zookeeper监听有三个关键点:

  1. 客户端对该节点注册监听,也就是客户端对该节点进行订阅。
  2. 该节点发生改变,触发某一事件后,客户端会收到一个通知。可以执行相应回调执行相应动作。
  3. 注册的监听只会生效一次,要想继续生效,就要在回调的方法中继续注册监听。

java api中 有三个方法可以注册监听,getData、exist、getChildren。

监听器:

  • 接听器接口Watcher,我们可以实现该接口实现自定义的监听器注册到节点上。
  • 事件类型可以分为连接事件状态类型和节点事件类型
  • 事件类型:由Watcher.Event.EventType枚举维护。

主要有5种类型:

  1. NodeCreated:节点被创建时触发。
  2. NodeDeleted:节点被删除时触发。
  3. NodeDataChanged:节点数据被修改时触发。
  4. NodeChildrenChanged:子节点被创建或者删除时触发。
  5. NONE: 该状态就是连接状态事件类型。前面四种是关于节点的事件,这种是连接的事件,具体由Watcher.Event.KeeperState枚举维护。

注册事件的方式与节点事件的关系:

方式NodeCreatedNodeDeletedNodeDataChangedNodeChildrenChanged
exist可监控可监控可监控不可监控
getData不可监控可监控可监控不可监控
getChildren不可监控可监控不可监控可监控

以上表格是对设置监听的方法对相应的事件是否可监控到。比如exist方法对节点删除事件不可监控,假如用该方法注册监听的话,节点删除时并不会触发事件回调。

连接事件类型,是指客户端连接时会触发的事件,由Watcher.Event.KeeperState枚举维护,主要有四个类型:

  • SyncConnected :客户端与服务器正常连接时触发的事件。
  • Disconnected :客户端与服务器断开连接时触发的事件。
  • AuthFailed:身份认证失败时触发的事件
  • Expired :客户端会话Session超时时触发的事件。

我们举例并进行如下测试:

public class MyWatcher implements Watcher {
     @Override
     public void process(WatchedEvent event) {
         // 判断事件类型,连接类型的事件
         if(event.getType() == Event.EventType.None){
              if(event.getState() == Event.KeeperState.SyncConnected){
                  System.out.println("zookeeper连接成功");
              } else if (event.getState() == Event.KeeperState.AuthFailed){
                  System.out.println("zookeeper认证失败");
              } else if (event.getState() == Event.KeeperState.Disconnected){
                  System.out.println("zookeeper断开连接");
              }

         } else if (event.getType() == Event.EventType.NodeCreated){
             System.out.println(event.getPath() + "被创建了");
         } else if (event.getType() == Event.EventType.NodeDeleted){
             System.out.println(event.getPath() + "被删除了了");
         } else if (event.getType() == Event.EventType.NodeDataChanged){
             System.out.println(event.getPath() + "节点的数据改变了");
         } else if (event.getType() == Event.EventType.NodeChildrenChanged){
             System.out.println(event.getPath() + "子节点发生了变化");
         }
     }
}

测试 用例:

@Test
public void testWatcher(){
    try {
        // 以下三个方法可以注册watcher,可以直接new一个新的watcher,
        // 也可以使用true来选定默认的watcher
        zooKeeper.exists("/ydlclass", true);
        //            zooKeeper.getChildren();
        //            zooKeeper.getData();

        while(true){
            Thread.sleep(1000);
        }

    } catch (KeeperException | InterruptedException e) {
        e.printStackTrace();
    } finally {
        try {
            if(zooKeeper != null){
                zooKeeper.close();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

六、集群安装

zookeeper的运行模式有单机模式,伪集群模式,集群模式三种。单个Zookeeper节点是会存在单点故障的我们搭建一个zk集群。Zookeeper节点部署越多,服务的可靠性越高,通常建议部署奇数个节点,因为zookeeper集群是以宕机个数过半才会让整个集群宕机的。

zookeeper的集群模式下,节点分为leader和follower两种状态,leader负责所有的写操作,follower负责相关的读操作。

1687581421177-0b33f7f3.png

1、准备环境

主机名系统IP地址
linux-node1CentOS release 8192.168.126.129
linux-node2CentOS release 8192.168.126.132
linux-node2CentOS release 8192.168.126.133

2、JDK安装

JDK下载地址:www.oracle.com/technetwork…

rpm -ivh jdk-8u101-linux-x64.rpm

3、Zookeeper安装

Zookeeper链接:zookeeper.apache.org/

wget http://mirrors.cnnic.cn/apache/zookeeper/zookeeper-3.4.8/zookeeper-3.4.8.tar.gz -P /usr/local/src/
tar zxvf zookeeper-3.4.8.tar.gz -C /opt
cd /opt && mv zookeeper-3.4.8 zookeeper
cd zookeeper
cp conf/zoo_sample.cfg conf/zoo.cfg

把zookeeper加入到环境变量

echo -e "# append zk_env\nexport PATH=$PATH:/opt/zookeeper/bin" >> /etc/profile

Zookeeper运行需要java环境,需要安装jdk,注:每台服务器上面都需要安装zookeeper、jdk,建议本地下载好需要的安装包然后上传到服务器上面,服务器上面下载速度太慢。

修改zookeeper的配置文件,构建集群的基础配置:

dataLogDir=/opt/zookeeper/logs
dataDir=/opt/zookeeper/data
server.1= 192.168.126.129:2888:3888
server.2= 192.168.126.132:2888:3888
server.3= 192.168.126.133:2888:3888

server.1中的1指代第几个节点,2888端口用来辅助这个服务器与集群中的leader服务器做交换信息的端口,3888端口是在leader挂掉时专门用来进行选举leader所用的端口。

创建日志和持久化目录:

mkdir -p /opt/zookeeper/{logs,data}

创建ServerID标识

除了修改zoo.cfg配置文件外,zookeeper集群模式下还要配置一个myid文件,这个文件需要放在dataDir目录下。

这个文件里面有一个数据就是服务器编号,在zoo.cfg文件中配置的dataDir路径中创建myid文件。如在server.1服务器上面创建myid文件,就将他的值设置为1。以此类推2,3

echo "1" > /opt/zookeeper/data/myid

**小节:**ZooKeeper集群的启动过程包括以下步骤:

  1. 准备配置文件:
    • 创建每个ZooKeeper节点的配置文件。配置文件包含节点的唯一标识(例如,ID)、监听地址和端口、数据目录等信息。
    • 配置文件还包括集群的信息,包括每个节点的连接信息和选举算法。
  2. 启动ZooKeeper节点:
    • 在每个节点上运行ZooKeeper进程。可以使用命令行或脚本来启动每个节点。
    • 在启动命令中指定节点的配置文件,确保每个节点使用正确的配置。
  3. 节点启动顺序:
    • 为了保证ZooKeeper集群的正常运行,节点的启动顺序很重要。通常,建议先启动节点ID较小的节点,然后依次启动其他节点。
    • 启动过程中,每个节点会尝试连接到其他节点并加入集群。
  4. 初始化数据目录:
    • 在节点启动时,ZooKeeper会检查配置文件中指定的数据目录。
    • 如果数据目录不存在,ZooKeeper会自动创建,并初始化节点的数据存储结构。
  5. 数据同步和选举:
    • 在节点加入集群后,ZooKeeper会开始进行数据同步和选举过程。
    • 数据同步是指将节点的数据状态与其他节点进行同步,以确保集群中的数据一致性。
    • 选举过程是为了选出一个节点作为Leader(领导者),负责协调和处理客户端请求。
  6. Leader选举完成:
    • 一旦Leader选举完成,集群中的节点将分为Leader和Follower两类。
    • Leader节点负责处理所有写操作和协调集群的状态,而Follower节点负责复制和同步Leader节点的数据。
  7. 集群就绪:
    • 当ZooKeeper集群中的节点完成数据同步和选举后,整个集群将进入就绪状态。
    • 客户端可以通过连接到任意一个节点来与集群进行通信和操作。

通过以上步骤,ZooKeeper集群中的节点将完成启动过程并开始提供服务。在启动过程中,注意配置文件的正确性和节点启动顺序的保持,这样可以确保集群的正常运行和数据的一致性。