准备资源: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. 修改其他两台机器上zk的myid编号
# 修改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中节点的元数据,较为重要的元数据属性如下表:
属性名 | 说明 |
czxid | zookeeper中每次发生变化,都会生成新的czxid。当集群中的一台zk发生更新时,其他zk会数据同步,其他zk的数据同步时刻肯定会有先后顺序,此时,数据同步完成最慢的,得到的新czxid值越大。 |
dataversion | zk中节点内容的版本号 |
dataLength | zk中节点内容的数据长度 |
numChildren | zk中节点的子节点数量 |
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;
hadoop101 | hadoop102 | hadoop103 | hadoop104 | hadoop105 | |
启动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提出写数据请求