ZooKeeper学习笔记

Apache ZooKeeper

ZK简介

一个分布式的,开源的分布式应用程序协调服务,是Google的分布式锁服务Chubby的一个开源实现,是Hadoop和Hbase的重要组件。ZK由Java编写,但是支持Java和C两种编程语言。

  • 在Dubbo、SpringCloud中,担任服务注册中心的角色
  • 在Hadoop、Hbase等大数据的组件中,担任集群管理者的角色
  • 在Redis分布式开发中,担任实现分布式锁的角色。

ZK数据模型

ZK模型结构

模型结构为树状结构,类似Linux文件目录结构

image-20210405093910510

  • 每个子目录如/app1都被当作一个znode节点,每个znode节点被其所在的路径唯一标识
  • znode节点可以有子节点目录,并且每个znode可以存储数据
  • znode中存储的数据是有版本的,每次对节点数据的修改都会导致版本号的增加;同时每个znode节点可以存储数据的多个版本,即一个访问路径中可以存储多份数据
  • znode节点可以被监控,包括这个目录节点中存储数据的修改、子节点目录的变化等,一旦变化可以通知设置监控的客户端

节点分类

根据ZK模型结构中的znode节点的生命周期额外的节点特性(父节点为其第一级子节点维护一份时序),可以将节点划分成四类:持久节点、持久顺序节点、临时节点、临时顺序节点。

持久节点(P)

在节点创建后,就会存盘并一直存在,直到被要求删除,值得注意的是,它不会因为创建该节点的客户端会话的失效而被删除。

持久顺序节点(PS)

在生命周期等基本特性上和持久节点相同。额外的特性是,在ZK中,每个父节点会为其第一级子节点维护一份时序,即记录每个子节点创建的先后顺序。基于这一特性,在创建子节点时,可以设置该属性,这样在创建节点的过程中,ZK会自动地为给定的节点名增加一个数字后缀(范围是1~整型的最大值)作为新的节点名,例如节点/app1/p_1这样的节点就是持久顺序节点。

临时节点(E)

与持久节点截然相反,临时节点的生命周期和客户端会话绑定,即如果客户端会话失效(会话失效!=会话连接断开),那么绑定的临时节点将自动被清除。除此之外,临时节点下不能创建子节点。

临时顺序节点(ES)

具有临时节点的特点,额外特性和持久顺序节点的额外特性相同。

ZK安装

Linux系统中安装ZK

linux安装zookeeper及使用

Docker安装ZK

拉取ZK镜像

docker pull zookeeper:3.4.14

启动ZK容器

docker run --name zk -p 2181:2181 -d zookeeper:3.4.14

查看容器zk的启动日志

docker logs zk

image-20210405102306772

成功绑定宿主机的2181端口。

进入zk容器(交互式方式)

docker exec -it zk bash

image-20210405102559629

连接容器内的ZK服务

./bin/zkCli.sh

image-20210405102934185

ZK配置文件详解

#tickTime:Client-Server通信维持心跳的时间间隔,以毫秒为单位
tickTime=2000

#initLimit:初始化集群时,集群节点同步超时时间(tickTime的数量)
initLimit=10

#syncLimit:Leader-Follower节点之间请求和应答即同步数据的超时时间(tickTime的数量)
syncLimit=5

#dataDir:数据文件目录,默认情况下,Zookeeper将写数据的日志文件也保存在这个目录里
dataDir=/tmp/zookeeper

#clientPort:zk服务的监听端口号
clientPort=2181

#maxClientCnxns:服务器能处理的最大客户端并发数,也即服务线程池的最大线程数,默认为60
maxClientCnxns=60

#维护配置:客户端在与zookeeper交互过程中会产生非常多的日志,而且zookeeper也会将内存中的数据作为snapshot保存下来,这些数据是不会被自动删除的,这样磁盘中这样的数据就会越来越多。autopurge.snapRetainCount是设置保留snapshot的个数,而autopurge.purgeInterval则代表了合并快照的时间间隔(防止快照越来越多)
#autopurge.snapRetainCount=3
#autopurge.purgeInterval=1 

ZK客户端基本指令

image-20210405105626880

单体命令

ls path

查看当前节点路径下的下一级子节点

ls /
ls /zookeeper
ls /zookeeper/quota

image-20210405112738805

create [-s] [-e] path data acl

创建节点(四大类),默认创建持久化节点,加上-s代表创建顺序节点,加上-e代表创建临时节点。

#创建p节点
create /testCreate person

#创建ps节点
create -s /testCreate/person zhangsan1
create -s /testCreate/person zhangsan2
create -s /testCreate/person zhangsan3

#创建e节点
create -e /testCreate/session session1

#创建es节点
create -s -e /testCreate/session session1

查看创建的持久化节点

image-20210405112654307

查看创建的临时节点

image-20210405112804150

quit退出客户端,自动关闭会话,此时查看之前创建的节点的状态。

image-20210405112520961

临时节点在会话失效后会自动删除,同时,临时节点下不能创建任何子节点。

stat path

查看节点的信息

stat /testCreate

image-20210405121454718

get path

获取节点存储的数据及节点信息

get /testCreate

image-20210405121703253

set path data

修改节点的数据并查看

set /testcreate people

get /testCreate

image-20210405122048136

delete path

删除节点(该节点必须没有子节点)

ls /testCreate

delete /testCreate

delete /testCreate/person0000000002

ls /testCreate

image-20210405123113775

rmr path

递归删除节点,可以删除含子节点的节点

create /testDelete person

create -s /testDelete xiaoming
create -s /testDelete xiaohong
create -s /testDelete xiaoma

rmr /testCreate

image-20210405123637490

history

查看操作历史

history

image-20210405122527807

quit

退出当前会话(会话失效)

quit

image-20210405123837578

组合命令

ls2 path

ls pathstat path 的组合

ls2 /testCreate

image-20210405122316016

节点监听机制(Watch)

客户端可以监听znode节点的变化,主要包括节点路径节点数据两种变化。znode节点的变化会触发响应的事件,然后清除对该节点的监测,当监测一个znode节点时,zk会发送通知给监测节点。一个Watch事件是一个一次性的触发器,当被设置了Watch的数据和节点路径发生了变化时,则服务器将会把这个改变发送给设置Watch的客户端进行通知。

节点路径监听

监听客户端设置节点路径监听—— 该节点路径下的所有路径变化都会通知到设置该监听的客户端

ls path true

节点数据监听

监听客户端设置节点数据监听 —— 该节点路径下的所有数据变化都会通知到设置该监听的客户端

get path true

应用

微服务框架下作为服务的注册中心

image-20210405125717905

Java操作ZK

依赖导入

导入zkclient依赖包

<!--引入zookeeper依赖-->
<dependencies>
    <dependency>
        <groupId>com.101tec</groupId>
        <artifactId>zkclient</artifactId>
        <version>0.10</version>
    </dependency>
</dependencies>

导入测试时要用到的两个依赖:junit 和 lombok

<!--junit-->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.11</version>
    <scope>provided</scope>
</dependency>

<!--lombok依赖-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.18</version>
</dependency>

测试案例

package com.youzikeji.client;

import com.youzikeji.pojo.User;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import org.I0Itec.zkclient.serialize.SerializableSerializer;
import org.I0Itec.zkclient.serialize.ZkSerializer;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.data.Stat;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.util.Date;
import java.util.List;

public class TestZKClient {
    private ZkClient zkClient;

    /**
     * @description: 创建节点测试
     * @params: []
     * @return: void
     * @author: caoyusang
     * @dateTime: 2021/4/5 13:27
     */
    @Test
    public void testCreateNode(){
        //参数1: 节点路径
        //参数2: 节点数据
        //参数3: 指定创建节点的类型

        //1.创建P节点
        zkClient.create("/testCreateNode","root", CreateMode.PERSISTENT);
        zkClient.create("/testCreateNode/pNode", "p-node", CreateMode.PERSISTENT);

        //2.创建PS节点
        zkClient.create("/testCreateNode/PSNode", "ps-node", CreateMode.PERSISTENT_SEQUENTIAL);

        //3.创建E节点
        zkClient.create("/testCreateNode/ENode", "e-node", CreateMode.EPHEMERAL);

        //4.创建ES节点
        zkClient.create("/testCreateNode/ESNode", "es-node", CreateMode.EPHEMERAL_SEQUENTIAL);
    }

    /**
     * @description: 删除节点测试
     * @params: []
     * @return: void
     * @author: caoyusang
     * @dateTime: 2021/4/5 13:56
     */
    @Test
    public void testDeleteNode(){
        /*//1.只能删除不含子节点的节点
        boolean deleteStatus = zkClient.delete("/testCreateNode");
        System.out.println("节点删除状态: " + deleteStatus);*/

        //2.递归删除节点
        boolean deleteRecursiveStatus = zkClient.deleteRecursive("/testCreateNode");
        System.out.println("节点删除状态: " + deleteRecursiveStatus);

    }

    /**
     * @description: 测试查询当前节点路径下的所有子节点
     * @params: []
     * @return: void
     * @author: caoyusang
     * @dateTime: 2021/4/5 13:59
     */
    @Test
    public void testGetChidrenNodes(){
        List<String> children = zkClient.getChildren("/");
        for (String child : children){
            System.out.println(child);
        }
    }

    /**
     * @description: 查看某个节点路径存储的数据
     * @params: []
     * @return: void
     * @author: caoyusang
     * @dateTime: 2021/4/5 14:00
     */
    @Test
    public void testGetNodeData(){
        //注意读数据时要保证节点创建时的数据序列化方式和获取时的序列化方式一致
        Object data = zkClient.readData("/testCreateNode");
        System.out.println(data);
    }

    /**
     * @description: 获取节点的状态信息
     * @params: []
     * @return: void
     * @author: caoyusang
     * @dateTime: 2021/4/5 14:08
     */
    @Test
    public void testGetNodeStatus(){
        Stat stat = new Stat();
        Object readData = zkClient.readData("/testCreateNode", stat);
        System.out.println(readData);
        System.out.println("创建版本:" + stat.getCversion());
        System.out.println("创建时间:" + stat.getCtime());
    }


    /**
     * @description: 更新节点数据
     * @params: []
     * @return: void
     * @author: caoyusang
     * @dateTime: 2021/4/5 14:17
     */
    @Test
    public void modifyNodeData(){
        User user = new User();
        user.setId(1).setName("wandehua").setAge(23).setBirth(new Date());
        zkClient.writeData("/testCreateNode", user);
        User userInfo = zkClient.readData("/testCreateNode");
        System.out.println("更新后的数据:" + userInfo);
        System.out.println("ID:" + userInfo.getId());
        System.out.println("姓名:" + userInfo.getName());
        System.out.println("年龄:" + userInfo.getAge());
        System.out.println("生日:" + userInfo.getBirth());

    }

    /**
     * @description: 监听节点的数据变化
     * @params: []
     * @return: void
     * @author: caoyusang
     * @dateTime: 2021/4/5 15:13
     */
    @Test
    public void testWatchDataChange() throws IOException {
        zkClient.subscribeDataChanges("/testCreateNode", new IZkDataListener() {
            @Override
            public void handleDataChange(String s, Object o) throws Exception {
                System.out.println("当前路径: " + s);
                System.out.println("当前节点变化后的数据: " + o);
            }

            @Override
            public void handleDataDeleted(String s) throws Exception {
                System.out.println("当前路径: " + s);
            }
        });

        //阻塞客户端
        System.in.read();
    }

    /**
     * @description: 监听当前节点路径下的下一级子节点的变化
     * @params: []
     * @return: void
     * @author: caoyusang
     * @dateTime: 2021/4/5 15:20
     */
    @Test
    public void testWatchNodesChange() throws IOException {
        zkClient.subscribeChildChanges("/", new IZkChildListener() {
            @Override
            public void handleChildChange(String s, List<String> list) throws Exception {
                System.out.println("父节点名称" + s);
                System.out.println("发生改变的子节点名称:");

                for (String name : list){
                    System.out.println(name);
                }
            }
        });

        //阻塞客户端
        System.in.read();
    }

    /**
     * @description: 测试之前初始化客户端对象
     * @params: []
     * @return: void
     * @author: caoyusang
     * @dateTime: 2021/4/5 13:27
     */
    @Before
    public void initClient(){
        //参数1: 服务器ip:port
        //参数2: 会话超时时间
        //参数3: 连接超时时间
        //参数4: 序列化方式
        zkClient = new ZkClient("127.0.0.1:2181", 60000 * 3, 60000, new SerializableSerializer());
    }

    /**
     * @description: 测试结束,释放资源
     * @params: []
     * @return: void
     * @author: caoyusang
     * @dateTime: 2021/4/5 13:27
     */
    @After
    public void releaseClient() throws InterruptedException {
        /*//延迟资源释放,不然临时节点的创建看不到
        Thread.sleep(25000);*/
        zkClient.close();
    }

}

创建节点的结果

image-20210405134809157

获取节点信息

image-20210405141203293

更新节点

使用User对象更新节点的数据

先创建一个实体类User——要实现序列化,这里用到了lombok提供构造器和get/set方法以及允许链式编程@Accessors(chain = true)

package com.youzikeji.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;

import java.io.Serializable;
import java.util.Date;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class User implements Serializable {
    private Integer id;
    private String name;
    private Integer age;
    private Date birth;
}

更新结果

image-20210405142216629

监听节点数据变化事件 —— 更新节点数据

image-20210405150959604

image-20210405151014276

监听节点数据变化事件 —— 删除节点

image-20210405151219002

ZK的集群

集群

集合同一种软件服务的多个节点同时为客户端提供服务,集群的出现就是为了解决以下几个问题:

  • 单节点的并发访问压力问题
  • 单节点故障问题(由于硬件老化、自然灾害等原因)

集群架构

image-20210405153128249

  • 两类节点:leader 和 follower,一个ZK集群中一般只有一个leader节点,follower节点又称仲裁节点,它们的配置完全一样。leader节点由投票选举从所有follower节点中产生。
  • 搭建集群时,至少需要三台服务器,且强烈建议使用奇数个服务器,如果只有两台服务器,在其中一个节点宕机时,没办法形成多数仲裁。同时,由于存在两个单点故障,所以两台服务器甚至不如一台服务器稳定。
  • 分布式写数据不一致问题的解决 —— zab原子广播协议,当客户端像任意一个服务节点进行了写操作,此时服务节点并不直接更新,而是先向主节点leader询问是否可以更新,如果得到肯定的回复,那么所有follwer节点之间就会进行广播通信,同步更新写入的数据。如果写入失败,则将失败信息返回给客户端。
  • 如果某一时刻leader节点宕机,那么就从所有剩下的follower节点中进行多数仲裁,选举出新的leader节点。

ZK集群的搭建

zookeeper 集群搭建

通过Java客户端操作ZK集群

在初始化客户端对象时,把服务端的ip:port换成对应集群中所有节点的ip:port即可。

参考资料

linux安装zookeeper及使用

zookeeper 集群搭建

2021最新ZooKeeper教程

posted @ 2021-06-20 10:13  打瞌睡的布偶猫  阅读(185)  评论(0编辑  收藏  举报