一、Zookeeper概述
(一)为什么学习zk
我们为什么要学习zookeeper呢?其实从字面的意思我们就可以揣测到zookeeper就是动物园管理员的意思,如果把大数据的一些技术栈、框架比作各种动物,管理员应该就是处于协调、处理这些动物的角色,其实我们经常会发现共享的资源在并发的情况下会出现竞争,在线程间可以使用Java提供的锁机制来协调这些资源,那么在分布式的环境下,如何来协调这些资源呢?
1、分布式环境下无法保证顺序
在单机环境如果想让A先执行,B后执行,先调用A后调用B就可以了;由于网络是不可靠的,分布式环境中则不同
2、分布式环境下无法明确执行结果
单机中调用A成功和失败很明确;而在分布式环境中,即使调用A执行成功了,而在网络传输中超时了,此时无法判断A是否执行成功了,需要通过重试的方式才能判断A有没有执行成功。
,在网络延迟的情况下A可能比B执行要晚。
3、分布式环境下无法保证数据一致性
分布式环境如果很多台服务器提供相同的服务,如何保证服务的某一个改动要么同时生效,要么失败,是分布式和单机环境的最重要的区别。
(二)zk的概述
Zookeeper 是一个分布式的开源框架。 主要用来解决分布式集群中应用系统的一致性问题,即为分布式应用提供协调服务。
在学习zookeeper之前,我们可以想下微信公众号推送文章,微信服务器有两个功能。当公众号作者写文章后,点击发布,这个时候微信服务器做了两件事情,第一件事:将作者的文章保存下来,即实现了文件系统的功能,保存文件。第二件事:给每个订阅了该公众号的微信用户发送通知,而不给那些未订阅的用户推送,即实现了通知功能。
而zookeeper也有这两大功能,即Zookeeper=文件系统(可以在zk上存储数据)+通知机制。我们也可以在zookeeper上存储数据,也可以连接到zookeeper服务器事先订阅的某个数据,然后zookeeper发现数据变化后,会通知客户端。
ZooKeeper 本质上是一个含有监听、通知机制的分布式的小文件存储系统。 提供基于类似于文件系统的目录树方式的数据存储,并且可以对树中的节点进行有效管理。
(三)zk的特点
1、主从架构
一个领导者(Leader),多个跟随者(Follower),【多个观察者(Observer)】
2、容错性
集群中只要有半数以上节点正常就可以保证正常对外提供服务 搭建奇数台
3、全局数据一致性
每个server都保存一份相同的数据副本,client无论链接那个server,数据都是一致的
4、顺序性
更新请求顺序进行,来自同一个client的更新请求按其发送顺序依次进行
5、原子性
数据更新时,要么全部成功,要么全部失败
6、实时性
在一定的时间范围内,Client能读取到最新的数据
(四)数据结构
ZooKeeper数据模型的结构与Unix文件系统很类似,整体上可以看作是一棵树,每个节点称做一个Znode,zookeeper把数据存储在节点上,每个节点最多存储1M数据。
很显然zookeeper集群自身维护了一套数据结构。这个存储结构是一个树形结构,其上的每一个节点,我们称之为"znode",每个ZNode都可以通过其路径唯一标识,如图所示
(五)zk的核心架构
1、架构图
2、角色职责介绍
(1)Leader
一个Zookeeper集群同一时间只会有一个实际工作的Leader,它会发起并维护与各Follwer及Observer间的心跳。所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。
(2)Follower
一个Zookeeper集群可能同时存在多个Follower,它会响应Leader的心跳。
Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,并且负责在Leader处理写请求时对请求进行投票。
(3)Observer
角色与Follower类似,但是无投票权。
角色 | 功能描述 | |
领导者(Leader) |
| |
学习者(Learner) | 跟随者(Follower) |
|
观察者(Observer) | 1. Follower用于接收客户端请求,并向客户端返回结果 2. 处理客户端非事务(读)请求 3. 转发事务(写)请求给Leader 4. 不参与集群投票 | |
客户端(Client) | 请求发起方 |
备注:
事务性请求:更新操作、新增操作、删除操作,因为这些操作是会影响数据的,所以要保证这些操作在整个集群内的事务性,所以这些操作就是事务性请求。
非事务性请求:像查询操作、exist操作这些不影响数据的操作,就不需要集群来保持事务性,所以这些操场就是非事务性请求。
二、ZooKeeper安装和部署
(一)安装前准备
检查ssh免密登录实现与否
[offcn@bd-offcn-01 ~]$ ssh bd-offcn-02
检查jdk的安装是否成功
[offcn@bd-offcn-01 ~]$ java -version
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)
检查时钟是否同步
三台机器统一执行date命令
(二)安装配置
1、下载地址:
https://zookeeper.apache.org/
https://zookeeper.apache.org/releases.html#download
https://archive.apache.org/dist/zookeeper/
2、上传解压重命名:
cd /home/offcn/software/
tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz -C /home/offcn/apps
cd /home/offcn/apps/
mv apache-zookeeper-3.5.7-bin zookeeper-3.5.7
3、创建zk数据存储目录:
cd /home/offcn/data/
mkdir zookeeper-3.5.7
4、创建myid
cd /home/offcn/data/zookeeper-3.5.7
echo 1 > myid
5、重命名配置文件:
cd /home/offcn/apps/zookeeper-3.5.7/conf/
mv zoo_sample.cfg zoo.cfg
6、修改核心配置文件
vim zoo.cfg
(1)指定数据存储目录
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/home/offcn/data/zookeeper-3.5.7/
(2)添加集群信息(节点、监听端口、选举端口)
server.1=bd-offcn-01:2888:3888
server.2=bd-offcn-02:2888:3888
server.3=bd-offcn-03:2888:3888
zk端口说明:
2181:对client端提供服务
2888:集群内机器通讯使用(Leader监听此端口)
3888:选举leader使用
(4)修改zk日志输出目录
cd /home/offcn/apps/zookeeper-3.5.7/bin
大约70行处
vim zkEnv.sh
if [ "x${ZOO_LOG_DIR}" = "x" ]
then
# ZOO_LOG_DIR="$ZOOKEEPER_PREFIX/logs"
ZOO_LOG_DIR="$HOME/logs/zookeeper-3.5.7/"
fi
(5)分发安装包以及修改myid
cd /home/offcn/apps/
scp -r zookeeper-3.5.7/ bd-offcn-02:$PWD
scp -r zookeeper-3.5.7/ bd-offcn-03:$PWD
cd /home/offcn/data/
scp -r zookeeper-3.5.7/ bd-offcn-02:$PWD
scp -r zookeeper-3.5.7/ bd-offcn-03:$PWD
注意:修改bd-offcn-02、bd-offcn-03的myid分别为2、3
(6)修改环境变量
sudo vim /etc/profile
#zookeeper-3.5.7
export ZOOKEEPER_HOME=/home/offcn/apps/zookeeper-3.5.7
export PATH=$PATH:$ZOOKEEPER_HOME/bin
sudo scp /etc/profile bd-offcn-02:/etc
sudo scp /etc/profile bd-offcn-03:/etc
三台机器全部执行
source /etc/profile
(三)服务启停
1、服务端启停
zkServer.sh start
zkServer.sh stop
zkServer.sh status
2、客户端启停
zkCli.sh
3、一键启停脚本
cd /home/offcn
mkdir bin
vim zk.sh
chmod 777 zk.sh
-e 表示将双引号中的特殊字符进行解释,\e是控制输出字符的颜色,后边紧跟着颜色
#!/bin/bash
echo -e "\e[1;35m Zookeeper $1 .... \e[0m"
export ScriptsPath=`pwd`
for host in bd-offcn-01 bd-offcn-02 bd-offcn-03
do
if [ $1 = status ]
then
ssh $host "source /etc/profile;zkServer.sh $1"
elif [ $1 = restart ]
then
sh $ScriptsPath/$0 stop;sleep 3;sh $ScriptsPath/$0 start;break
else
ssh $host "source /etc/profile;nohup zkServer.sh $1>/dev/null 2>&1 &"
fi
done
三、客户端命令实操
(一)shell客户端操作
命令 | 说明 | 参数 |
create [-s] [-e] path data [acl] | 创建节点 PS:zookeeper中,节点的路径只有完整路径的概念,没有相对路径的概念,并且 不能出现同名同路径的节点 | -s:指定节点特性:序列化(加编号); -e:指定节点特性:临时节点。若两个都不指定,则表示创建的一个 “非序列化的持久节点" path:节点的 完整路径。 data:该节点绑定的数据。 acl:权限控制 |
ls [-s] [-w] [-R] path | 列出该路径节点下的所有子节点(只能获取第一级的子节点列表) | -s:指定:查询该路径节点时,除了子节点列表之外,还需要返回该节点的部分属性信息; -w:指定:查询该路径节点下的所有子节点,同时监听该节点,一旦该节点的子节点发生CUD(增删改)操作,立即推送消息给客户端; -R:指定:返回该路径节点下的所有的子节点列表 |
get [-s] [-w] path | 获取该路径节点的数据内容(不会返回子节点列表) | -s:指定:返回该节点的数据以及属性信息; -w:指定:当该节点数据/属性发生改变,立即推送消息给客户端。 |
ls2 path [watch] | 该查询方式与ls -s path类似,不过ZK不推荐使用该查询方式)列出该路径节点下的所有子节点(只能获取第一级的子节点列表)以及部分属性信息 | watch:指定:查询该路径节点下的所有子节点,同时监听该节点,一旦该节点的子节点发生CUD(增删改)操作,立即推送消息给客户端 |
set [-s] [-v version] path data | 更新节点数据 | data:更新的内容; -s:指定,当修改成功之后,同时返回该节点的属性。 -v version 表示数据版本( version值要与当前节点的 最新dataVersion 一致,否则会报错 ) |
delete [-v version] path | 删除指定路径节点 | 如果要删除的节点存在子节点,那么必须先删除子节点,然后才能删除该节点 |
deleteall path | 删除该路径下的所有节点 | 递归删除节点 |
setquota -n|-b val path | 对节点添加限制 | -n:表示该节点下,其子节点的最大个数(该节点本身也要算进去); -b:表示该节点的空间大小(byte) val:指定的值 |
listquota path | 列出指定节点的quota | |
delquota [-n|-b] path | 删除目标节点的限制 | |
history | 列出历史命令 |
1、列出根节点下所有子节点
[zk: localhost:2181(CONNECTED) 0] ls /
[zookeeper]
2、创建节点
[zk: localhost:2181(CONNECTED) 2] create /shell
Created /shell
[zk: localhost:2181(CONNECTED) 3] ls /
[shell, zookeeper]
3、获取节点数据和属性
[zk: localhost:2181(CONNECTED) 9] create /shell/test01 "haha"
Created /shell/test01
[zk: localhost:2181(CONNECTED) 11] ls /shell
[test01]
[zk: localhost:2181(CONNECTED) 10] get /shell
null
4、获取节点所有子节点以及自身属性
[zk: localhost:2181(CONNECTED) 12] ls2 /shell
'ls2' has been deprecated. Please use 'ls [-s] path' instead.
[test01]
cZxid = 0x500000002
ctime = Sat Mar 20 13:02:57 CST 2021
mZxid = 0x500000002
mtime = Sat Mar 20 13:02:57 CST 2021
pZxid = 0x500000003
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
5、限制节点子节点个数
[zk: localhost:2181(CONNECTED) 19] create /test02
Created /test02
[zk: localhost:2181(CONNECTED) 20] listquota /test02
absolute path is /zookeeper/quota/test02/zookeeper_limits
quota for /test02 does not exist.
[zk: localhost:2181(CONNECTED) 21] setquota -n 2 /test02
[zk: localhost:2181(CONNECTED) 47] get /zookeeper/quota/test02/zookeeper_limits
count=2,bytes=-1
[zk: localhost:2181(CONNECTED) 22] create /test02/1
Created /test02/1
[zk: localhost:2181(CONNECTED) 23] create /test02/2
Created /test02/2
查看日志发现:
WARN[CommitProcWorkThread-3:DataTree@340] - Quota exceeded: /test02 count=3 limit=2
只是警告,并无阻停
6、更新节点数据
[zk: localhost:2181(CONNECTED) 0] create /test04 "hadoop"
Created /test04
[zk: localhost:2181(CONNECTED) 1] set /test04 "zookeeper"
[zk: localhost:2181(CONNECTED) 2] get /test04
zookeeper
7、删除空节点
[zk: localhost:2181(CONNECTED) 9] create /test05 aaa
Created /test05
[zk: localhost:2181(CONNECTED) 10] delete /test05
[zk: localhost:2181(CONNECTED) 11] ls /
[test01,test02,test03, test04, zookeeper]
8、删除节点(非空)
[zk: localhost:2181(CONNECTED) 12] create /test05
Created /test05
[zk: localhost:2181(CONNECTED) 13] create /test05/a
Created /test05/a
[zk: localhost:2181(CONNECTED) 14] delete /test05
Node not empty: /test05
[zk: localhost:2181(CONNECTED) 15] rmr /test05
The command 'rmr' has been deprecated. Please use 'deleteall' instead.
[zk: localhost:2181(CONNECTED) 11] ls /
[test01,test02,test03, test04, zookeeper]
[zk: localhost:2181(CONNECTED) 19] create /test05
Created /test05
[zk: localhost:2181(CONNECTED) 20] create /test05/a
Created /test05/a
[zk: localhost:2181(CONNECTED) 21] deleteall /test05
[zk: localhost:2181(CONNECTED) 11] ls /
[test01,test02,test03, test04, zookeeper]
(二)javaApi
1、创建工程
2、引入pom文件、日志配置文件
<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>
log4j.properties
# 控制台输出配置
log4j.appender.Console=org.apache.log4j.ConsoleAppender
log4j.appender.Console.layout=org.apache.log4j.PatternLayout
log4j.appender.Console.layout.ConversionPattern=%d [%t] %p [%c] - %m%n
# 指定日志的输出级别与输出端
log4j.rootLogger=DEBUG,Console
3、核心代码操作
package com.bigdata.myzk;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
public class MyzkTest {
ZooKeeper client = null;
@Before // 获取与zookeeper通信的客户端
public void testGetClient() throws Exception {
client = new ZooKeeper("bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181", 5000, null);
System.out.println(client);
}
@Test // 获取子节点
public void testGetChildren() throws Exception {
List<String> children = client.getChildren("/movie", false);
for (String child : children) {
System.out.println(child);
}
}
@Test // 创建节点
public void testCreateNode() throws Exception {
String path = client.create("/story", "hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println(path);
}
@Test // 获取数据
public void testGetData() throws Exception {
// 参数1:要获取数据的节点路径
// 参数2:是否要监听
// 参数3:stat结构体 null表示获取最新版本的数据
byte[] data = client.getData("/story", false, null);
String s = new String(data);
System.out.println(s);
}
@Test // 修改数据
public void testSetData() throws Exception {
Stat stat = client.setData("/story", "world".getBytes(), -1);
System.out.println(stat);
}
@Test // 判断节点是否存在
public void testJudgeNode() throws Exception {
Stat exists = client.exists("/story", false);
if(exists == null){
System.out.println("节点不存在");
}else{
System.out.println("节点存在");
}
}
@Test
public void testDeleteNode() throws Exception {
client.delete("/story",-1);
}
}
报错:
org.apache.zookeeper.KeeperException$ConnectionLossException: KeeperErrorCode = ConnectionLoss for /
1. :zookeeper所在节点或集群的防火墙未关闭。这样会导致Linux为开放zookeeper的客户端连接的端口而无法连接
2. :使用zookeeper原生的客户端API连接时,设置的sessionTimout时间太短,这个时间个人认为必须要大于zookeeper配置文件中的一个心跳的时间,如果小于一个心跳的时间,zookeeper给客户端发送心跳的时候客户端还没有收到就已经超时了,永远也不会连上;所以如果zookeeper的一个心跳时间是2000ms,那么至少客户端的sessTimout时间是3000ms
3. :网络不稳定,这种情况就是客户端和zookeeper之间网络连接不稳定的情况下也会导致这个问题;在客户端和zookeeper的服务器上互相ping一下看看是否有网络丢包率的存在
4. :如果以上三种情况都不是,那么就要检查zookeeper集群有没有在正常运行,如果是可以运行的,那么检查一下客户端连接的url是否有问题,集群的话必须保证每个地址都是对的,不然也是无法连接的
(三)Znode节点
1、特点
文件系统的核心是 Znode
如果想要选取一个 Znode , 需要使用路径的形式, 例如 /test1/test11
Znode 本身并不是文件, 也不是文件夹, Znode 因为具有一个类似于 Name 的路径, 所以可以从逻辑上实现一个树状文件系统
ZK 保证 Znode 访问的原子性, 不会出现部分 ZK 节点更新成功, 部分 ZK 节点更新失败的问题
Znode 中数据是有大小限制的, 最大只能为 1M
2、组成
Znode 是由三个部分构成
• stat : 状态, Znode的权限信息, 版本等
• data : 数据, 每个Znode都是可以携带数据的, 无论是否有子节点
• children : 子节点列表
3、类型
每个 Znode 有两大特性, 可以构成四种不同类型的 Znode
持久性
持久:客户端断开时, 不会删除持有的Znode
临时 :客户端断开时, 删除所有持有的Znode, 临时Znode不允许有子Znode
顺序性
有序 :创建的Znode有先后顺序, 顺序就是在后面追加一个序列号, 序列号是由父节点管理的自增
无序 : 创建的Znode没有先后顺序
zookeeper把数据存储到节点上
节点的类型:两大类型(四小类)
永久性(持久性)节点:客户端连接到zookeeper集群后,创建的是永久性节点的话,那么客户端在退出zookeeper集群后,创建的节点还在
分为两类:
永久普通的:create /movie 111
永久带序号的:create -s /music 222 serial
短暂性(临时性)节点:客户端连接到zookeeper集群后,创建的是短暂性节点的话,那么客户端在退出zookeeper集群后,创建的节点就没了
短暂普通的:create -e /art 333 ephemeral
短暂带序号的;create -e -s /sci 444
列出某个节点的子节点
ls /
通过ephemeralOwner的值可以判断节点的类型,如果值为0x0,则该节点是永久的,否则是临时的
ls -s /zookeeper
查看节点数据
get /moive
修改节点数据
set /movie 222
删除节点
delete只能删除没有子节点的节点
delete /music0000000001
rmr 也能删除带有节点的节点
rmr /movie
查看状态
stat /music0000000002
监听的类型:
子节点的增减
ls /movie watch
ls -w /movie
数据监听
get /movie watch
节点是否存在监听
4、属性
1)czxid- create 引起这个znode创建的zxid,创建节点的事务的zxid
每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。
事务ID是ZooKeeper中所有修改总的次序。每个修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生。
2)ctime - create znode被创建的毫秒数(从1970年开始)
3)mzxid - modify znode最后更新的zxid
4)mtime - modify znode最后修改的毫秒数(从1970年开始)
5)pZxid-znode最后更新的子节点zxid
6)cversion - znode子节点变化号,znode子节点修改次数
7)dataversion - znode数据变化号
8)aclVersion - znode访问控制列表的变化号
9)ephemeralOwner- 如果是临时节点,这个是znode拥有者的session id。如果不是临时节点则是0。
10)dataLength- znode的数据长度
11)numChildren - znode子节点数量
四、zk的监听/通知机制
通知类似于数据库中的触发器, 对某个Znode设置 Watcher , 当Znode发生变化的时候,WatchManager 会调用对应的 Watcher
当Znode发生删除, 修改, 创建, 子节点修改的时候, 对应的 Watcher 会得到通知
(一)Watcher 的特点
Zk的回调函数分两种,一种是连接/断开连接相关的系统回调,一种是基于节点事件触发的回调,系统回调不需要注册,内部实现,在创建、断开连接时进行触发。而节点事件触发回调需要自己注册监听,在节点的创建、删除、数据的修改事件进行触发,
一次性触发:一个 Watcher 只会被触发一次, 如果需要继续监听, 则需要再次添加Watcher
事件封装: Watcher 得到的事件是被封装过的, 包括三个内容 keeperState,eventType, path
(二)实现监听
1、shell客户端模拟监听
(1)监听节点值变化
bd-offcn-01创建普通节点
[zk: localhost:2181(CONNECTED) 26] create /app1 "app1"
监听节点值变化
[zk: localhost:2181(CONNECTED) 27] get /app1 watch
在bd-offcn-02服务器上面开启zk连接客户端,更改app1节点的值
[root@bd-offcn-02 ~]#zkCli.sh
进入shell客户端更新app1节点的值
[zk: localhost:2181(CONNECTED) 0] set /app1 "hello"
查看bd-offcn-01节点的反应
(2)子节点变化监听
bd-offcn-01服务器监听子节点变化
[zk: localhost:2181(CONNECTED) 30] ls /app1 watch
bd-offcn-02服务器给/app3节点增加或减少子节点
[zk: localhost:2181(CONNECTED) 3] create /app1/hello "addnode"
Created /app1/hello
bd-offcn-01服务器查看响应状态
2、javaApi模拟监听
(1)系统回调
@Before
public void testGetClient() throws Exception {
ZooKeeper client = new ZooKeeper("bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181", 5000, new Watcher() {
// 回调方法,用于zookeeper集群调用
// 系统默认回调:建立与集群的连接,关闭与集群的连接,会调用此方法
public void process(WatchedEvent event) {
System.out.println(event.getPath() + "-" + event.getState() + "-" + event.getType());
}
});
System.out.println("获取到了客户端");
client.close();
}
(2)自定义回调
@Test
public void testEventWatch() throws Exception{
ZooKeeper zookeeper = new ZooKeeper(
"bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181",
5000,
new Watcher() {
//回调函数
@Override // 此处是默认的回调函数,什么时候会被回调呢?
// 1 刚刚获取客户端的时候
// 2 关闭客户端的时候
// 3 自己注册监听,如果没有提供监听,即没有重写process方法,也会调用默认的procees
public void process(WatchedEvent event) {
System.out.println("事件监听");
}
}
);
//注册监听 监听子节点的增减 如果此处自定义了Watcher,那么在回调的时候,就会回调此处重写的process
// 如果在此处只给了一个true,在回调的时候,就会回调获取客户端时候重写的那个process
List<String> list = zookeeper.getChildren("/",new Watcher() {
//回调函数
@Override
public void process(WatchedEvent event) {
System.out.println("hahaha");
}
} );
Thread.sleep(Long.MAX_VALUE);
// zookeeper.close();
}
(3)默认回调
/* ************************************************************************
* 功能描述:默认回调
* 对节点注册监听时,没有自定义回调函数时间执行
*/
@Test
public void testDefault() throws Exception{
ZooKeeper zookeeper = new ZooKeeper(
"bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181",
5000,
new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("默认系统回调");
}
}
);
//注册监听 客户端执行 set /shell/test01 "hello"
zookeeper.getData("/shell/test01",true,null);
Thread.sleep(5000);
zookeeper.close();
}
(4)永久监听
/* ************************************************************************
* 功能描述:永久监听
* 在系统默认监听时继续 注册监听
*/
ZooKeeper client=null;
@Test
public void testForEver() throws Exception{
client = new ZooKeeper(
"bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181",
5000,
new Watcher() {
@Override
public void process(WatchedEvent event) {
System.out.println("默认系统回调");
try {
client.getData("/shell/test01",true,null);
} catch (Exception e) {
e.printStackTrace();
}
}
}
);
Thread.sleep(Long.MAX_VALUE);
client.close();
}
package com.bigdata.myzk;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.junit.Before;
import org.junit.Test;
public class MzkWithWatcher {
ZooKeeper client = null;
@Before
public void testGetClient() throws Exception {
client = new ZooKeeper("bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181", 5000, new Watcher() {
// 回调方法,用于zookeeper集群调用
// 系统默认回调:建立与集群的连接,关闭与集群的连接,会调用此方法
public void process(WatchedEvent event) {
System.out.println(event.getPath() + "-" + event.getState() + "-" + event.getType());
try {
// client.getChildren("/movie", true);
client.getData("/movie", true, null);
} catch (Exception e) {
e.printStackTrace();
}
}
});
System.out.println("获取到了客户端");
}
@Test
public void testGustomCallback() throws Exception {
// List<String> children = client.getChildren("/movie", true);
// for (String child : children) {
// System.out.println(child);
// }
byte[] data = client.getData("/movie", true, null);
System.out.println(new String(data));
// client.close();
Thread.sleep(Long.MAX_VALUE);
}
五、zk的可视化操作
(一)ZooInspector的使用
解压文件,运行jar包,测试连接
(二)ZKUI的使用
1、解压zip文件、编译文件
cmd控制台切换到项目文件夹根部录下,使用mvn clean install,执行前需要安装java环境,maven环境,
执行成功后会生成一个jar文件,这个jar包在项目根目录文件夹的target文件夹里。如图
2、修改复制config.cfg
将config.cfg复制到上一步生成的jar文件所在目录,然后修改配置文件中的zookeeper地址。
这个配置文件在项目文件夹根目录下,修改后将它复制到target就可以了。
3、执行jar
windows环境下,cmd控制台切到target文件夹下,然后执行
4、访问登录
六、监听服务器动态上下线案例
1.需求
某分布式系统中,主节点可以有多台,可以动态上下线,任意一台客户端都能实时感知到主节点服务器的上下线。
2.需求分析
1)提供时间查询服务,客户端系统调用该时间查询服务。
2)动态上线,下线该时间查询服务的节点,让客户端实时感知服务器列表变化,查询时候访问最新的机器节点。
3)具体实现:
先在集群上创建/servers节点
[zk: localhost:2181(CONNECTED) 10] create /servers "servers"
Created /servers
时间服务器:
package com.bigdata.anli;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
public class TimeServer {
ZooKeeper client = null;
public static void main(String[] args) throws Exception {
TimeServer timeServer = new TimeServer();
// 1 获取zookeeper的客户端
timeServer.getClient();
// 2 把时间服务器的地址和端口号写入到zookeeper集群 /servers/server
timeServer.registServerInfo(args[0],Integer.parseInt(args[1]));
// 3 提供授时服务
new TimeService(Integer.parseInt(args[1])).start();
}
public void getClient() throws Exception {
client = new ZooKeeper("bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181", 5000, null);
}
public void registServerInfo(String hostname,int port) throws Exception {
String path = client.create("/servers/server", (hostname + ":" + port).getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// /servers/server0000001 localhost:6666
System.out.println("时间服务器上线了,主机和端口号是:"+hostname+port+",创建的节点是:"+path);
}
}
时间服务线程:
package com.bigdata.anli;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Date;
public class TimeService extends Thread{
private int port;
public TimeService(int port) {
this.port = port;
}
@Override
public void run() {
System.out.println("开始提供授时服务了");
// 监听本地的端口
try {
ServerSocket serverSocket = new ServerSocket(port);
while(true){
// 该accept是个阻塞的方法,只有Socket过来建立连接,该行才会继续往下执行
Socket accept = serverSocket.accept();
OutputStream out = accept.getOutputStream();
out.write(new Date().toString().getBytes());
out.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
时间客户端:
package com.bigdata.anli;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class TimeClient {
ZooKeeper client = null;
// localhost:6666
// localhost:7777
List<String> serverList = null;
public static void main(String[] args) throws Exception {
TimeClient timeClient = new TimeClient();
// 1 获取zookeeper集群的客户端
timeClient.getClient();
// 2 获取zookeeper上时间服务器的地址 /servers
timeClient.getServerList();
// 3 访问某个时间服务器,获取时间
timeClient.getTime();
}
public void getClient() throws Exception {
client = new ZooKeeper("bd-offcn-01:2181,bd-offcn-02:2181,bd-offcn-03:2181", 5000, new Watcher() {
public void process(WatchedEvent event) {
System.out.println("执行回调方法了");
try {
// 当zookeeper回调的时候,意味着有新的时间服务器上线了,或者宕机了,
// 要重新获取时间服务器列表,重新注册监听
getServerList();
} catch (Exception e) {
e.printStackTrace();
}
}});
}
public void getServerList() throws KeeperException, InterruptedException {
//创建集合存放时间服务器的地址
List<String> list = new ArrayList();
List<String> children = client.getChildren("/servers", true);
for (String child : children) {
// child server0000001
byte[] data = client.getData("/servers/" + child, false, null);
String hostAndPort = new String(data);
list.add(hostAndPort);
}
serverList = list;
System.out.println("获取到时间服务器地址:"+list);
}
public void getTime() throws Exception {
Random random = new Random();
while(true){
int i = random.nextInt(serverList.size());
// localhost:6666
String hostAndPort = serverList.get(i);
String host = hostAndPort.split(":")[0];
String port = hostAndPort.split(":")[1];
System.out.println("访问主机:"+host+port);
Socket socket = new Socket(host, Integer.parseInt(port));
OutputStream outputStream = socket.getOutputStream();
outputStream.write("hello".getBytes());
outputStream.flush();
InputStream inputStream = socket.getInputStream();
byte[] bytes = new byte[1024];
inputStream.read(bytes);
String time = new String(bytes);
System.out.println(time);
inputStream.close();
outputStream.close();
socket.close();
Thread.sleep(5000);
}
}
}
七、zk的选举机制
Leader选举是保证分布式[数据一致性]的关键所在。当Zookeeper集群中的一台服务器出现以下两种情况之一时,需要进入Leader选举。
1、全新集群选主
以一个简单的例子来说明整个选举的过程:假设有五台服务器组成的 zookeeper 集群,它们 的 serverid 从 1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点 上,都是一样的。假设这些服务器依序启动,来看看会发生什么
(1)服务器 1 启动,此时只有它一台服务器启动了,它发出去的报没有任何响应,所以它的 选举状态一直是 LOOKING 状态
(2)服务器 2 启动,它与最开始启动的服务器 1 进行通信,互相交换自己的选举结果,由于两者都没有历史数据,所以 id 值较大的服务器 2 胜出,但是由于没有达到超过半数以上的服务器都同意选举它(这个例子中的半数以上是 3),所以服务器 1、2 还是继续保持 LOOKING 状态
(3)服务器 3 启动,根据前面的理论分析,服务器 3 成为服务器 1,2,3 中的老大,而与上面不 同的是,此时有三台服务器(超过半数)选举了它,所以它成为了这次选举的 leader
(4)服务器 4 启动,根据前面的分析,理论上服务器 4 应该是服务器 1,2,3,4 中最大的,但是 由于前面已经有半数以上的服务器选举了服务器 3,所以它只能接收当小弟的命了
(5)服务器 5 启动,同 4 一样,当小弟
2、非全新集群选主
那么,初始化的时候,是按照上述的说明进行选举的,但是当 zookeeper 运行了一段时间之 后,有机器 down 掉,重新选举时,选举过程就相对复杂了。
需要加入数据 version、serverid 和逻辑时钟。
Data version:数据新的 version 就大,数据每次更新都会更新 version
server id:就是我们配置的 myid 中的值,每个机器一个
逻辑时钟:这个值从 0 开始递增,每次选举对应一个值,也就是说:如果在同一次选举中, 那么这个值应该是一致的;逻辑时钟值越大,说明这一次选举 leader 的进程更新,也就是 每次选举拥有一个 zxid,投票结果只取 zxid 最大的
选举的标准就变成:
- 1.逻辑时钟小的选举结果被忽略,重新投票
- 2.逻辑时钟相同的话,数据 version 大的胜出
- 3.数据 version 相同的情况下,server id 大的胜出
根据这个规则选出 leader。
八、zk的应用场景
(一)统一的命名服务
在分布式调度系统中可以使用zookeeper实现统一命名服务,以获得类似于UUID的全局唯一名称。使用zookeeper创建顺序节点时,成功创建的每个节点都会返回一个编号,使用该编号以及给定的名称即可生成具有特定含义的统一名称。
(二)统一配置中心
配置文件比如数据库连接,缓存更新时间,接口调用地址,加解密密钥,sesion超时时间,等等每个项目里面用的太多,如果项目里面都统一放在一个properties文件里面,会出现的问题,就是一旦一个地方修改了,假如有10台机器或者上百台,那么就需要重新部署这10台或者上百台的服务器
(三)数据发布/订阅
具体来说,是服务器在zk上订阅自己要监听的节点,在节点上存放配置信息,然后注册一个watch监听器,当这个节点信息发生变化,zk不是直接将变更内容发布至服务器,而是告诉服务器这个节点内容有变化,由服务器感应到通知后重新拉取节点信息,实现动态更新。
(四)分布式锁
分布式锁用于控制分布式系统之间同步访问共享资源的一种方式,可以保证不同系统访问一个或一组资源时的一致性,主要分为排它锁和共享锁。
1、排它锁
排它锁又称为写锁或独占锁,若事务T1对数据对象O1加上了排它锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作,直到T1释放了排它锁。
(1)非公平锁
缺陷:
这种方式在并发问题比较严重的情况下,性能较低,主要原因是,所有的连接都在对同一个节点进行监听,当服务器检测到删除事件时,要通知所有的连接,所有的连接同时收到事件,再次并发竞争,这就是羊群效应。
(2)公平锁
公平锁的实现,通过zk提供的临时顺序节点,可以避免同时多个节点的并发竞争锁,缓解了服务端压力,避免羊群效应。
2、共享锁
另一种分布式锁的类型是共享锁。它在性能上要优于排他锁,这是因为在共享锁的实现中,只对数据对象的写操作加锁,而不为对象的读操作进行加锁。这样既保证了数据对象的完整性,也兼顾了多事务情况下的读取操作。可以说,共享锁是写入排他,而读取操作则没有限制。
对于共享锁,首先所有的客户端都会到某个节点,例如:/shared_lock 下创建一个临时顺序节点,如果是读请求,就会创建诸如 /shared_lock/192.168.0.1-R-0000000001 的节点,如果是写操作,则创建诸如 /shared_lock/192.168.0.1-W-0000000001 的节点。是否获取到共享锁,从以下四个步骤来判断:
1、创建完节点后,获取/shared_lock节点下的所有子节点,并对该节点注册子节点变更的watcher监听。
2、确定自己的节点序号在所有子节点中的顺序。
3、对于读请求:
如果没有比自己序号小的子节点,或是所有比自己序号小的子节点都是读取请求,那么表明自己已经成功获取到了共享锁,同时开始执行读取逻辑。
如果比自己序号小的子节点中有写请求,那么就需要进入等待。
对于写请求:
如果自己不是序号最小的子节点,那么就需要进入等待。
4、接收到Watcher通知后,重复步骤1
(五)分布式队列
在传统的单进程编程中,我们使用队列来存储一些数据结构,用来在多线程之间共享或传递数据。分布式环境下,我们同样需要一个类似单进程队列的组件,用来实现跨进程、跨主机、跨网络的数据共享和数据传递,这就是我们的分布式队列。利用zk的短暂序列化节点特性,生产者创建节点,消费者按照顺序消费节点,即可实现。
(六)软负载均衡
当单台服务器由于性能不足无法处理众多用户的访问时,就要考虑用多台服务器来提供服务,如何保证多台服务器请求数量相差不大,实现的方式就是负载均衡。zookeeper本身是不提供负载均衡的策略,需要自己来实现,所以这里确切的说,是在负载均衡中应用到了zookeeper做集群的协调。
首先建立/servers节点,并建立监听器监听/servers子节点的状态(用于在服务器增添时及时同步当前集群中服务器列表)。
当每个服务器启动时,在/servers节点下建立子节点worker server(可以用服务器地址命名),并在对应的字节点下存入服务器的相关信息。
这样,我们在zookeeper服务器上可以获取当前集群中的服务器列表及相关信息,然后可以自定义一个负载均衡算法,在每个请求过来时从zookeeper服务器中获取当前集群服务器列表,根据负载均衡算法选出其中一个服务器来处理请求。