一、Zookeeper客户端命令行操作
在服务器上安装好Zookeeper后,使用zkCli.sh启动Zookeeper。
Zookeeper的命令行操作分为增删改查,以下为一些常用的命令。
查看子节点:
ls path 使用 ls 命令来查看当前znode的子节点
如ls -w / -w 不仅查看根目录的子节点,还会监听子节点变化,
如果根目录的子节点出现变化,Zookeeper会通知使用者
-s 查看子节点,并附加次级信息
增加子节点:
在102执行create /test 123(在根目录下创建子节点test,值为123),当在103执行 ls -w / ,新建子节点后,103会显示更新的信息
Create pathname value
如creat -s /test 123 普通创建
-s 普通创建下不能创建重名节点,创建含有序列的节点,
也就是节点后面会带有一串数字,这串数字是全局递增的
-e 临时(重启或者超时消失)
获取节点的值:
get pathname 获得节点的值
-w 并且监听节点内容的变化
-s 并且附加次级信息
更改节点的值:
Set
如set /test abcd 设置节点的具体值
stat 查看节点状态
delete 删除节点
deleteall 递归删除节点
二、Zookeeper API
创建Maven工程,添加依赖
<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>
<!-- https://mvnrepository.com/artifact/org.apache.zookeeper/zookeeper -->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.7</version>
</dependency>
</dependencies>
在项目的src/main/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
环境配置完成。
创建节点:
public class ZKClient {
/**
* 创建新节点
* @throws IOException
*/
@Test
public void create() throws IOException, KeeperException, InterruptedException {
String connectString = "hadoop101:2181,hadoop102:2181,hadoop103:2181"; //地址
int sessionTimeout = 2000; //超时时间
//创建Zookeeper对象
ZooKeeper zookeeper = new ZooKeeper(connectString,
sessionTimeout,
new Watcher() {
//Zookeeper监听的回调函数(进程A通过调用进程B的回调函数通知B,
// 当B察觉到自己的回调函数调用时,可以马上/过一会做出反应)
//回调函数用于异步通信,同步通信没有回调函数(不需要)
public void process(WatchedEvent watchedEvent) {
System.out.println("默认的回调函数");
}
});
//做一些工作
zookeeper.create("/testAPI", //设置节点的name
"123".getBytes(), //设置节点的value
ZooDefs.Ids.OPEN_ACL_UNSAFE, //设置访问权限,类似于linux中的777
CreateMode.PERSISTENT); //设置创建节点类型
//关闭资源
zookeeper.close();
}
}
查询子节点:
private ZooKeeper zookeeper;
@Before
public void before() throws IOException {
String connectString = "hadoop101:2181,hadoop102:2181,hadoop103:2181"; //地址
int sessionTimeout = 2000; //超时时间
//创建Zookeeper对象
zookeeper = new ZooKeeper(connectString,
sessionTimeout,
new Watcher() {
//Zookeeper监听的回调函数(进程A通过调用进程B的回调函数通知B,
// 当B察觉到自己的回调函数调用时,可以马上/过一会做出反应)
//回调函数用于异步通信,同步通信没有回调函数(不需要)
public void process(WatchedEvent watchedEvent) {
System.out.println("默认的回调函数");
}
});
}
@After
public void after() throws InterruptedException {
//关闭资源
zookeeper.close();
}
/**
* 查询子节点
*/
@Test
public void ls() throws KeeperException, InterruptedException {
List<String> children = zookeeper.getChildren(
"/",
new Watcher() {
public void process(WatchedEvent watchedEvent) {
System.out.println("自定义的回调函数");
}
});
for (String child : children) {
System.out.println(child);
}
Thread.sleep(Long.MAX_VALUE); //将线程阻塞住,持续监听,如果节点发生变化,则打印“自定义的回调函数”
}
查询节点的值:
/**
* 查询节点的值
*/
@Test
public void get() throws KeeperException, InterruptedException, IOException {
//节点的附加信息(stat结构体)
Stat stat = new Stat();
byte[] data = zookeeper.getData("/testAPI", false, stat);
System.out.println(stat.getCversion());
System.out.write(data); //将节点值写在控制台
System.out.println();
}
查询节点的状态(stat):
/**
* 查询一个节点的状态
*/
@Test
public void stat() throws KeeperException, InterruptedException {
Stat stat = zookeeper.exists("/testAPI", false);
if(stat == null){
System.out.println("节点不存在");
}else {
System.out.println(stat);
}
}
设置节点的值:
/**
* 设置节点的值
*/
@Test
public void set() throws KeeperException, InterruptedException {
String node = "/testAPI";
Stat stat = zookeeper.exists(node, false);
if(stat == null){
System.out.println("节点不存在");
}else{
//第三个参数为节点的修改次数,因为在查节点和修改节点的间隔,其他人可能会修改节点
//添加第三个参数的目的是为了防止我们改错数据,保证“我要修改的数据”和“我实际修改的
//数据”是一致的,称为乐观锁(大家都可以看见这份数据,
//都可以修改,只有最先改的人可以成功);悲观锁(独占这份资源,其他人不可以使用)
//表达这是我们看到的第几个版本的数据
zookeeper.setData("/testAPI","abcd".getBytes(),stat.getVersion());
}
}
删除节点:
/**
* 删除节点
*/
@Test
public void delete() throws KeeperException, InterruptedException {
String node = "testAPI";
Stat stat = zookeeper.exists("/testAPI", false);
if(stat == null){
System.out.println("节点不存在");
}else {
int version = stat.getVersion();
zookeeper.delete("/testAPI",version);
}
}
三、Zookeeper内部原理
节点类型:持久化节点,持久化有序节点,临时节点,临时有序节点
stat结构体:
(1)czxid-创建节点的事务zxid
每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。
事务ID是ZooKeeper中所有修改总的次序。每个修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生。如:cZxid = 0x100000003 前3个字符一部分,表示目前的leader是第一个选举出来的leader,后面为一部分,表示第一个leader做的第三件事。
(2)ctime - znode被创建的毫秒数(从1970年开始)
(3)mzxid - znode最后更新的事务zxid
(4)mtime - 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子节点数量
监听器原理:
简而言之,若监听A节点,主线程(main)会将请求发送给sent thread,sent thread将请求发送给服务器,执行完,主线程去做自己的事情,直到节点出现变化,服务器将变化信息发送给event thread,event thread再调用回调函数,来通知主线程。
ZAB协议:
Zookeeper为了实现全局数据一致,使用了ZAB协议。协议分为两个部分,第一部分:集群中的所有节点都是平等的,但是ZAB想要达到全局一致的目的,如果没有leader,需要在原来互相平等的节点中选取一个leader;第二部分:如果有leader,让它正常运行即可。
对于第一部分,Zookeeper有一套自己的选举机制。
选举机制:
(1)半数机制:集群中半数以上机器存活,集群可用。所以Zookeeper适合安装奇数台服务器。
(2)Zookeeper虽然在配置文件中并没有指定Master和Slave。但是,Zookeeper工作时,是有一个节点为Leader,其他则为Follower,Leader是通过内部的选举机制临时产生的。
(3)以一个简单的例子来说明整个选举的过程。
假设有五台服务器组成的Zookeeper集群,它们的id从1-5,同时它们都是最新启动的,也就是没有历史数据,在存放数据量这一点上,都是一样的。假设这些服务器依序启动。
(1)服务器1启动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING;
(2)服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的ID比自己目前投票推举的(服务器1)大,更改选票为推举服务器2。此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING
(3)服务器3启动,发起一次选举。此时服务器1和2都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数,服务器3当选Leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;
(4)服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING;
(5)服务器5启动,同4一样当小弟。
以上有一个问题,每台服务器怎么认为其他服务器选举的leader比自己选举的leader厉害?比较规则:两台服务器先比较自己选举leader的zxid(Zookeeper的数据经过哪些改变)谁大,谁的数据就比较新,适合作为leader,相同时,再比较myid,谁大,谁适当作为leader,myid不可能相等。只要leader选举成功,之后的服务器都会服从。
根据以上规则,如果5台同时启动且zxid相同,5号就是leader。正常工作的集群,zxid都相同。
第二部分leader正常工作,即数据的读写。
数据的读写:
leader发起一次写请求,如果server1同意,server2不同意,leader自己同意,共计两票同意,leader发起写请求,server1,leader完成写操作,写成功之后向client发布广播,但是server不容易写,它就会原地自杀,然后重启,重启之后再向leader同步数据。因为Zookeeper集群要尽最大的可能保持数据的一致性。
那么它会在什么情况下不同意呢?每个节点都会落实写请求,也就是每个节点都会有一个zxid(leader发送请求时已经编好了),但是每个server又会有自己最新的zxid,当leader发送来的zxid比它自己的大,就同意写请求,反之,不同意,因为follower的数据比leader更新。
为什么会出现这种情况呢?因为集群中的网络状况十分复杂且Zookeeper使用UDP协议,如当发生网络延迟时,某一请求丢失的情况。因为网络问题,可能会出现leader发送的请求其他server都不同意,那么leader就会原地自杀,重启,然后发起新一轮的选举(这种情况十分少见)。