2022-07-17 Zookeeper基础

准备资源:apache-zookeeper-3.5.7-bin.tar.gz

一、Zookeeper概述

1. 工作机制

2. 特点

(1) 集群由一个Leader和多个Follower组成;

(2) 超过半数以上的节点存活,集群就能正常工作。通常设置节点数为奇数个;

(3) 数据一致,每个节点中存储的内容相同;

(4) 更新原子性,要么全部节点数据内容更新成功,要么全部节点数据内容都更新失败;

(5) 更新内容具有顺序性,客户端要求更新顺序为:1->2->3,请求到达集群中后,每个节点更新数据内容的顺序也是:1->2->3;

(6) 实时性,更新速度很快;

3. 数据结构:类似于文件系统,是一颗节点树。

二、Zookeeper分布式安装部署

1. 将apache-zookeeper-3.5.7-bin.tar.gz上传到集群中的一台机器上(本文中为hadoop101),置于/opt/software;

2. 将内容解压缩

cd /opt/software
tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C ../moudle/

3. 更改解压出的文件夹名称

cd ../moudle
mv apache-zookeeper-3.5.7-bin/ zookeeper-3.5.7

4. 配置环境变量

# 首先,打开环境变量的配置文件
sudo vim /etc/profile.d/my-env.sh


# 下面是配置内容:
# Zookeeper环境变量
export ZOOKEEPER_HOME=/opt/moudle/zookeeper-3.5.7
export PATH=$PATH:$ZOOKEEPER_HOME/bin

5. 修改Zookeeper的配置文件

        (1) 更名

# 来到配置文件目录下
cd /opt/moudle/zookeeper-3.5.7/conf/
# 更改配置文件名称,使之能被识别为配置文件
mv zoo_sample.cfg zoo.cfg

        (2) 修改配置文件内容

# zk集群与客户端的心跳超时时间,单位是ms
# 默认配置为每隔2s,集群和客户端之间有一次心跳
# 当超过2 * tickTime ms集群和客户端没有心跳时,表明客户端已经掉线
tickTime=2000
# zk集群中Leader和Followers初始化连接可以花费的最长时间
# 默认情况下,能容忍的最长连接时间是10 * tickTime
initLimit=10
# zk集群中节点同步数据内容可以花费的最长时间
# 默认情况下,某一节点超过5 * tickTime没有向Leader回执(无论失败或者成功)
# 就表明该节点已经掉线,将其从集群中剔除 
syncLimit=5
# zk集群存储数据内容的目录
dataDir=/opt/moudle/zookeeper-3.5.7/zkData
# zk供客户端连接的端口号
clientPort=2181
# 配置zk集群中每一台zkServer的信息
# 2888是集群中zk之间同步信息使用的端口号
# 3888是集群选举Leader使用的端口号
# 101、102、103是服务器的编号,在myid中配置
server.101=hadoop101:2888:3888
server.102=hadoop102:2888:3888
server.103=hadoop103:2888:3888

6. 创建存储数据内容的文件夹

# 来到zk安装根目录下
cd ..
# 创建zkData文件夹,用来存储数据内容
mkdir -p zkData

7. 创建myid文件,并写入当前节点上运行的zk的编号

# 创建myid,并将编号写入其中
echo "101" > zkData/myid

8. 分发环境变量配置文件、分发zk安装目录;

# 先分发环境变量配置文件
# 切换到root用户,继承普通用户的环境变量
su
# 分发
xrsync.sh /etc/profile.d/my-env.sh
# 退出root用户
exit

# 分发zk目录
xrsync.sh /opt/moudle/zookeeper-3.5.7/

9. 使环境变量配置文件生效

xcall.sh "source /etc/profile"

10. 修改其他两台机器上zkmyid编号

# 修改hadoop102上的myid
echo "102" > $ZOOKEEPER_HOME/zkData/myid

# 修改hadoop103上的myid
echo "103" > $ZOOKEEPER_HOME/zkData/myid

11. 启停

 (1) zk提供的命令

语法选项说明
zkServer.sh 选项

-start|-stop|-status

启动|停止|状态

无论启动或者停止,需要手动在每一个节点上执行命令
zkCli.sh [选项]-server (hostname|IP):port

不加选项,默认连接本地的zkServer。

加选项可以连接其他节点上的zkServer;可以加多个要连接的节点,表示第一个不行,就连接后面的,之间用逗号分割

 (2) 编写脚本

# 脚本要置于用户目录下的bin文件夹中
vim ~/bin/zookeeper.sh

# 下面为脚本内容
#!/bin/bash
if [[ $# -ne 1 ]]
then
	echo "usage: zookeeper.sh (start|stop|status|open-client)"
	exit
fi

case $1 in
"open-client")
	port=$(cat $ZOOKEEPER_HOME/conf/zoo.cfg | awk -F'=' '/^clientPort=[0-9]+/{print $2}')
	connectString=$(cat $ZOOKEEPER_HOME/conf/zoo.cfg | awk -vORS=',' -vOFS=':' -vport=$port -F'[=:]' '/server\.[0-9]+.*/{print $2, port}' | grep -E -o '.*[^,]')
	zkCli.sh -server $connectString
;;
*)
	for host in $(cat $ZOOKEEPER_HOME/conf/zoo.cfg | awk -F'[=:]' '/server\.[0-9]+.*/{print $2}')
	do
		echo "***************$host***************"
		if [[ $(ping -c1 -W1 $host &>/dev/null;echo $?) -eq 0 ]]
		then
			case $1 in
			"start")
				ssh $host 'zkServer.sh start'
			;;
			"stop")
				ssh $host 'zkServer.sh stop'
			;;
			"status")
				ssh $host 'zkServer.sh status'
			;;
			*)
				echo "usage: zookeeper.sh (start|stop|status|open-client)"
				exit
			;;
			esac
		else
			echo "$host net err"
		fi
	done
;;
esac

# 赋予脚本执行的权限
chmod +x ~/bin/zookeeper.sh

(3) 启动、停止、查看状态、进入客户端操作界面

# 启动zk集群
zookeeper.sh start

# 查看zk集群的状态
zookeeper.sh status

# 进入客户端操作界面
# 进如客户端操作界面后,退出命令为:quit
zookeeper.sh open-client

# 停止zk集群
zookeeper.sh stop

三、命令行操作

语法格式选项说明
help显示所有操作命令
ls [选项] 节点路径

-w 监听该节点下子节点的变化;监听子节点的创建、删除,子节点的内容被修改时,不会监听到

-s 附加输出该节点的元数据

查看节点的子节点
create [选项] 节点路径

-s 创建带有序列号的节点;序列号从0开始,每创建一个节点,序列号自增1

-e 创建临时节点

创建节点。

如果不带选项,默认创建的是持久节点。

临时节点会在客户端下线后自动被删除,持久节点会一直保存。

get [选项] 节点路径

-w 监听节点内容是否被写入,如果被写入,通知客户端

-s 附加输出该节点的元数据

查看节点内容
set 节点路径 "str"向节点写入内容,写入的内容会覆盖原本的内容
stat 节点路径查看节点的元数据
delete 节点路径删除没有子节点的节点
deleteall 节点路径可以删除任何节点,包括含有子节点的节点

四、API操作

1. 环境准备

(1) 引入依赖

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.logging.log4j</groupId>
            <artifactId>log4j-core</artifactId>
            <version>2.8.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.zookeeper</groupId>
            <artifactId>zookeeper</artifactId>
            <version>3.5.7</version>
        </dependency>
    </dependencies>

(2) 在resources下写配置文件

# 配置文件名为:log4j.properties
log4j.rootLogger=INFO, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n
log4j.appender.logfile=org.apache.log4j.FileAppender
log4j.appender.logfile.File=target/spring.log
log4j.appender.logfile.layout=org.apache.log4j.PatternLayout
log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - %m%n


2. 创建zk客户端对象,操作zk必须使用zk客户端对象

        /*
        * public ZooKeeper(String connectString, int sessionTimeout, Watcher watcher)
        * 参数解读
        *   connectString: 连接zkServer的地址,即hadoop101:2181[,hadoop102:2181]
        *   sessionTimeout: 客户端和zkServer如果超过该值没有心跳,表示客户端已经下线,单位为ms
        *   watcher: 监听器,内有process方法,当连接上或者断开连接集群时都会调用process方法
        */
        String connectString = "hadoop101:2181,hadoop102:2181,hadoop103:2181";
        int sessionTimeout = 20000;
        ZooKeeper zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
            }
        });
        
        
        // 断开连接,释放资源
        zooKeeper.close();

3. 获取子节点列表

(1) 不监听子节点列表

        /*
        * List<String> getChildren(String path, boolean watch)
        * 方法解读
        *   path: 获取path节点路径下的子节点
        *   watch: false表示不监听子节点列表
        *   返回值: 返回的时字符串列表,只返回相对于path节点路径的子节点名称,不返回子节点的绝对路径
        */
        List<String> children = zooKeeper.getChildren(path, false);

 (2) 监听子节点列表

        /*
        * List<String> getChildren(String path, Watcher watcher)
        * 方法解读
        *   path: 获取path节点路径下的子节点
        *   watcher: 监听器对象,内置process方法,子节点一旦发生创建、删除动作,都会调用process方法,只通知一次
        *   返回值: 返回的时字符串列表,只返回相对于path节点路径的子节点名称,不返回子节点的绝对路径
        */
        List<String> children = zooKeeper.getChildren(path, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                Event.EventType type = event.getType();

                if (Event.EventType.NodeChildrenChanged.equals(type)) {
                    // 子节点列表发生了改变, 可能新增、可能删除
                    System.out.println("子节点列表改变了");
                }
            }
        });
        for (String child : children) {
            System.out.println(child);
        }

4. 创建节点

        /*
        * String create(final String path, byte data[], List<ACL> acl, CreateMode createMode)
        * 方法解读
        *   path: 要创建的节点
        *   data: 节点内容的字节数组
        *   acl: 节点的权限控制,最高权限(ZooDefs.Ids.OPEN_ACL_UNSAFE)
        *   createMode: 创建的节点类型
        *               临时节点(CreateMode.EPHEMERAL)、
        *               持久节点(CreateMode.PERSISTENT)、
        *               带序列号持久节点(CreateMode.PERSISTENT_SEQUENTIAL)、
        *               带序列号临时节点(CreateMode.EPHEMERAL_SEQUENTIAL)
        *   返回值: 创建的节点的绝对路径
        */
        String s = zooKeeper.create(path, "hello".getBytes(StandardCharsets.UTF_8), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
        System.out.println(s);

5. 判断节点是否存在

(1) 不监听

        /*
        * Stat exists(String path, boolean watch)
        * 方法解读
        *   path: 节点路径
        *   watch: false表示不监听
        *   返回值: 如果节点存在,返回节点的元数据对象;如果节点不存在,返回null
        */
        // stat是节点的元数据对象
        Stat stat = zooKeeper.exists(path, false);
        if (stat == null){
            // 表示节点不存在
            System.out.println(path + "不存在");
        }else {
            // 节点
            System.out.println(path + "存在");
        }

(2) 监听

        /*
        * Stat exists(String path, Watcher watcher)
        * 方法解读
        *   path: 节点路径
        *   watcher: 监听对象,触发一次,内置process方法,触发process的时机为
        *            (1) path不存在,成功创建后触发
        *            (2) path存在,删除或者写入内容触发
        *   返回值: 如果节点存在,返回节点的元数据对象;如果节点不存在,返回null
        */
        // stat是节点的元数据对象
        Stat stat = zooKeeper.exists(path, new Watcher() {
            @Override
            public void process(WatchedEvent event) {
                Event.EventType type = event.getType();
                if (Event.EventType.NodeCreated.equals(type)){
                    System.out.println("节点原本不存在,刚刚创建出来");
                }else if (Event.EventType.NodeDeleted.equals(type)){
                    System.out.println("节点原本存在,但是刚刚被删除了");
                }else if (Event.EventType.NodeDataChanged.equals(type)){
                    System.out.println("节点原本存在,但是刚刚向其中写入了数据");
                }
            }
        });
        if (stat == null){
            // 表示节点不存在
            System.out.println(path + "不存在");
        }else {
            // 节点
            System.out.println(path + "存在");
        }

6. 获取子节点的数据

(1) 不监听

        /*
        * byte[] getData(String path, boolean watch, Stat stat)
        * 方法解读
        *   path: 节点路径
        *   watch: false表示不监听
        *   stat: path对应的stat对象
        *   返回值: 节点内容的字节数组
        */
        Stat stat = zooKeeper.exists(path, false);
        if (stat != null){
            String data = new String(zooKeeper.getData(path, false, stat), StandardCharsets.UTF_8);
            System.out.println(data);
        }

 (2) 监听

        /*
        * byte[] getData(final String path, Watcher watcher, Stat stat)
        * 方法解读
        *   path: 节点路径
        *   watcher: 监听对象,触发一次,触发时机为
        *            (1) 向path节点写入内容
        *            (2) 删除path节点
        *   stat: path对应的stat对象
        *   返回值: 节点内容的字节数组
        */
        Stat stat = zooKeeper.exists(path, false);
        if (stat != null){
            String data = new String(zooKeeper.getData(path, new Watcher() {
                @Override
                public void process(WatchedEvent event) {
                    Event.EventType type = event.getType();
                    if (Event.EventType.NodeDataChanged.equals(type)){
                        System.out.println("向" + path + "中了写入内容");
                    }else if (Event.EventType.NodeDeleted.equals(type)){
                        System.out.println(path + "被删除了");
                    }
                }
            }, stat), StandardCharsets.UTF_8);
            System.out.println(data);
        }

7. 设置节点的值

        /*
        * Stat setData(final String path, byte data[], int version)
        * 方法解读
        *   path: 节点路径
        *   data: 要写入内容的字节数组
        *   version: 节点的版本,版本号可以从stat对象中获取,当从stat对象中获取的版本号和现在集群中该节点的版本号不一样时,无法写入内容;
        *           -1表示无视版本号
        *   返回值: 写入内容后返回的新stat对象
        */
        Stat stat = zooKeeper.exists(path, false);
        stat = zooKeeper.setData(path, "Hello2".getBytes(StandardCharsets.UTF_8), stat.getVersion());

8. 删除节点

(1) 删除不含子节点的节点

        /*
        * void delete(final String path, int version)
        * 方法解读
        *   path: 要删除的节点路径
        *   version: 节点的版本,版本号可以从stat对象中获取,当从stat对象中获取的版本号和现在集群中该节点的版本号不一样时,无法写入内容;
        *           -1表示无视版本号
        */
        Stat stat = zooKeeper.exists(path, false);
        if (stat != null){
            zooKeeper.delete(path, stat.getVersion());
        }

(2) 递归删除节点

    public void deleteAll(String path, ZooKeeper zooKeeper) throws KeeperException, InterruptedException {
        Stat stat = zooKeeper.exists(path, false);
        if (stat != null){
            // 获取该节点的子节点列表
            List<String> children = zooKeeper.getChildren(path, false);
            if (children != null && !children.isEmpty()){
                // 该节点拥有子节点,先递归删除所有子节点
                for (String child : children) {
                    deleteAll(path + "/" + child, zooKeeper);
                }
                // 删除之后,该节点成为了不含子节点的节点
            }
            // 删除不含子节点的节点
            zooKeeper.delete(path, stat.getVersion());
        }
    }

五、Zookeeper内部原理

1. Stat结构体

        Stat结构体用来描述zookeeper中节点的元数据,较为重要的元数据属性如下表:

属性名说明
czxidzookeeper中每次发生变化,都会生成新的czxid。当集群中的一台zk发生更新时,其他zk会数据同步,其他zk的数据同步时刻肯定会有先后顺序,此时,数据同步完成最慢的,得到的新czxid值越大。
dataversionzk中节点内容的版本号
dataLengthzk中节点内容的数据长度
numChildrenzk中节点的子节点数量

2. 选举机制

 (1) 集群还未启动,集群启动时的选举流程        

选举机制概要:

        a. 当集群中还没有Leader时,启动的机器都会为自己投一票,之后比较myid,随后myid小的机器将自己的票数投给myid大的机器;当myid最大的机器获得的选票数大于集群内机器总数的半数时,就会成为Leader,否则启动下一台机器的时候,重新进行选举;

        b.当集群中已经存在Leader时,机器之间不会再进行投票选举,自动成为Follower

        假设集群内有5台机器,分别为:hadoop101(myid=101)、hadoop102(myid=102)、hadoop103(myid=103)、hadoop104(myid=104)、hadoop105(myid=105);五台机器中zk的启动顺序为:hadoop101 -> hadoop103 -> hadoop104 -> hadoop102 -> hadoop105

hadoop101hadoop102hadoop103hadoop104hadoop105
启动hadoop101

选票数:1

状态:Locking

启动hadoop103

选票数:0

状态:Locking

选票数:2

状态:Locking

启动hadoop104

选票数:0

状态:Follower

选票数:0

状态:Follower

选票数:3

状态:Leader

启动hadoop102

选票数:无

状态:Follower

选票数:无

状态:Follower

选票数:无

状态:Follower

选票数:无

状态:Leader

启动hadoop105

选票数:无

状态:Follower

选票数:无

状态:Follower

选票数:无

状态:Follower

选票数:无

状态:Leader

选票数:无

状态:Follower

 (2) 集群已经启动,Leader已经选举出来,但是当集群运行的时候,Leader宕机,新Leader的选举流程

选举机制为:

        从集群中剩余的机器中选择zcxid最大的机器作为Leader;极端情况下,如果剩余机器的zcxid值都一样,则选取myid最大的机器作为Leader

3. 写数据流程(例如创建节点、删除节点、写入节点内容......)

(1) 客户端向集群中的Leader提出写数据请求

(2) 客户端向集群中的Follower提出写数据请求

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值