上篇文章是单机版的,这里我们安装下集群的,集群版安装和单机版类似,相同的操作这里不做阐述,大家可以先看下单机版安装,用来比较下区别,集群比单机版多了什么,加深印象。
集群操作
集群安装
首先准备好三台linux 机器,可以使用虚拟机搭建,可参考 https://blog.csdn.net/weixin_45847167/article/details/121160467,此篇文章中的虚拟机ip地址是私有的,只能本机访问,由于zookeeper之间需要互相通信,我们采用共享网络ip,注释掉 config.vm.network "private_network", ip: "192.168.56.10"
此行,改成 config.vm.network "public_network", ip: "192.168.3.51"
,此ip可以改成你wifi局域网的IP地址。
比如我的三台机器:
192.168.3.51
192.168.3.52
192.168.3.53
安装zookeeper 之前需要安装jdk,参考此篇文章 https://blog.csdn.net/weixin_45847167/article/details/121176936
zoo.cfg
# The number of milliseconds of each tick
tickTime=2000
# The number of ticks that the initial
# synchronization phase can take
initLimit=10
# The number of ticks that can pass between
# sending a request and getting an acknowledgement
syncLimit=5
# the directory where the snapshot is stored.
# do not use /tmp for storage, /tmp here is just
# example sakes.
dataDir=/home/vagrant/apache-zookeeper-3.5.7-bin/data
# the port at which the clients will connect
clientPort=2181
# the maximum number of client connections.
# increase this if you need to handle more clients
#maxClientCnxns=60
#
# Be sure to read the maintenance section of the
# administrator guide before turning on autopurge.
#
# http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
#
# The number of snapshots to retain in dataDir
#autopurge.snapRetainCount=3
# Purge task interval in hours
# Set to "0" to disable auto purge feature
#autopurge.purgeInterval=1
server.1=192.168.3.51:2888:3888
server.2=192.168.3.52:2888:3888
server.3=192.168.3.53:2888:3888
集群配置文件中比单机版多了:
server.1=192.168.3.51:2888:3888
server.2=192.168.3.52:2888:3888
server.3=192.168.3.53:2888:3888
这个coo.cfg 三台机器都是一样的
在 /home/vagrant/apache-zookeeper-3.5.7-bin/data 目录下创建 myid 文件,内容相应机器的标识,也就是server.后面的1,2,3标识。
myid 也就是机器编号。
三台机器分别启动:
cd apache-zookeeper-3.5.7-bin/bin
./zkServer.sh start
./zkServer.sh status
启动后可以看到效果。
选举机制
先熟悉几个参数:
- SID:服务器ID。用来唯一标识一台ZooKeeper集群中的机器,每台机器不能重复,和myid一致。
- ZXID:事务ID。ZXID是一个事务ID,用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的ZXID值不一定完全一
致,这和ZooKeeper服务器对于客户端“更新请求”的处理逻辑有关。 - Epoch:每个Leader任期的代号。没有Leader时同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加
Zookeeper选举机制 第一次启动
- 服务器1启 动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING;
- 服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的myid比自己目前投票推举的(服务器1) 大,更改选票为推举服务器2。此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING
- 服务器3启动,发起一次选举。此时服务器1和2都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数,服务器3当选Leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;
- 服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为 1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING;
- 服务器5启动,同4一样当小弟。
Zookeeper选举机制 非第一次启动
当ZooKeeper集群中的一台服务器出现以下两种情况之一时,就会开始进入Leader选举:
- 服务器初始化启动。
- 服务器运行期间无法和Leader保持连接。
而当一台机器进入Leader选举流程时,当前集群也可能会处于以下两种状态:
- 集群中本来就已经存在一个Leader。
对于第一种已经存在Leader的情况,机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader机器建立连接,并进行状态同步即可。 - 集群中确实不存在Leader。
假设ZooKeeper由5台服务器组成,SID分别为1、2、3、4、5,ZXID分别为8、8、8、7、7,并且此时SID为3的服务器是Leader。某一时刻,3和5服务器出现故障,因此开始进行Leader选举。
SID为1、2、4的机器投票情况:
(EPOCH,ZXID,SID ) | (EPOCH,ZXID,SID ) | (EPOCH,ZXID,SID ) |
---|---|---|
(1,8,1) | (1,8,2) | (1,7,4) |
选举Leader规则:
- EPOCH大的直接胜出
- EPOCH相同,事务id大的胜出
- 事务id相同,服务器id大的胜出
客户端命令行操作
命令行语法
命令 | 功能描述 |
---|---|
help | 显示所有操作命令 |
ls path | 使用 ls 命令来查看当前 znode 的子节点 [可监听] -w 监听子节点变化 -s 附加次级信息 |
create | 普通创建 -s 含有序列 -e 临时(重启或者超时消失) |
get path | 获得节点的值 [可监听] -w 监听节点内容变化 -s 附加次级信息 |
set | 设置节点的具体值 |
stat | 查看节点状态 |
delete | 删除节点 |
deleteall | 递归删除节点 |
启动客户端
连接本机server:
cd /home/vagrant/apache-zookeeper-3.5.7-bin/bin
[vagrant@localhost bin]$ ./zkCli.sh
Connecting to localhost:2181
........................
[zk: localhost:2181(CONNECTED) 0]
连接其他server:
[vagrant@localhost bin]$ ./zkCli.sh -server 192.168.3.52:2181
Connecting to 192.168.3.52:2181
...........
[zk: 192.168.3.52:2181(CONNECTED) 0] ls /
[servers, watch, zookeeper]
显示所有操作命令
[zk: localhost:2181(CONNECTED) 0] help
ZooKeeper -server host:port cmd args
addauth scheme auth
close
config [-c] [-w] [-s]
connect host:port
create [-s] [-e] [-c] [-t ttl] path [data] [acl]
delete [-v version] path
deleteall path
delquota [-n|-b] path
get [-s] [-w] path
getAcl [-s] path
history
listquota path
ls [-s] [-w] [-R] path
ls2 path [watch]
printwatches on|off
quit
reconfig [-s] [-v version] [[-file path] | [-members serverID=host:port1:port2;port3[,...]*]] | [-add serverId=host:port1:port2;port3[,...]]* [-remove serverId[,...]*]
redo cmdno
removewatches path [-c|-d|-a] [-l]
rmr path
set [-s] [-v version] path data
setAcl [-s] [-v version] [-R] path acl
setquota -n|-b val path
stat [-w] path
sync path
znode 节点数据信息
查看当前znode中所包含的内容
[zk: localhost:2181(CONNECTED) 1] ls /
[zookeeper]
查看当前节点详细数据
[zk: localhost:2181(CONNECTED) 2] ls -s /
[zookeeper]cZxid = 0x0
ctime = Thu Jan 01 00:00:00 UTC 1970
mZxid = 0x0
mtime = Thu Jan 01 00:00:00 UTC 1970
pZxid = 0x0
cversion = -1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
节点数据参数:
参数 | 说明 |
---|---|
czxid | 创建节点的事务 zxid 每次修改 ZooKeeper 状态都会产生一个 ZooKeeper 事务 ID。事务 ID 是 ZooKeeper 中所有修改总的次序。每次修改都有唯一的 zxid,如果 zxid1 小于 zxid2,那么 zxid1 在 zxid2 之前发生。 |
ctime | znode 被创建的毫秒数(从 1970 年开始) |
mzxid | znode 最后更新的事务 zxid |
mtime | znode 最后修改的毫秒数(从 1970 年开始) |
pZxid | znode 最后更新的子节点 zxid |
cversion | znode 子节点变化号,znode 子节点修改次数 |
dataversion | znode 数据变化号 |
aclVersion | znode 访问控制列表的变化号 |
ephemeralOwner | 如果是临时节点,这个是 znode 拥有者的 session id。如果不是临时节点则是 0。 |
dataLength | znode 的数据长度 |
numChildren | znode 子节点数量 |
节点类型(持久/短暂/有序号/无序号)
持久(Persistent):客户端和服务器端断开连接后,创建的节点不删除
短暂(Ephemeral):客户端和服务器端断开连接后,创建的节点自己删除
说明:创建znode时设置顺序标识,znode名称后会附加一个值,顺序号是一个单调递增的计数器,由父节点维护;
注意:在分布式系统中,顺序号可以被用于为所有的事件进行全局排序,这样客户端可以通过顺序号推断事件的顺序;
(1)持久化目录节点
客户端与Zookeeper断开连接后,该节点依旧存在
(2)持久化顺序编号目录节点
客户端与Zookeeper断开连接后,该节点依旧存
在,只是Zookeeper给该节点名称进行顺序编号
(3)临时目录节点
客户端与Zookeeper断开连接后,该节点被删除
(4)临时顺序编号目录节点
客户端与 Zookeeper 断开连接后 , 该 节 点 被 删 除 , 只 是
Zookeeper给该节点名称进行顺序编号。
创建 永久节点不带序号节点
[zk: localhost:2181(CONNECTED) 8] create /servers "server"
Created /servers
[zk: localhost:2181(CONNECTED) 9] create /servers/server1 "server1 value"
Created /servers/server1
[zk: localhost:2181(CONNECTED) 10] get -s /servers/server1
server1 value
cZxid = 0x200000005
ctime = Sun Jan 02 18:26:01 UTC 2022
mZxid = 0x200000005
mtime = Sun Jan 02 18:26:01 UTC 2022
pZxid = 0x200000005
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 13
numChildren = 0
创建 永久节点带序号节点
[zk: localhost:2181(CONNECTED) 11] create -s /servers/server2 "server2 value"
Created /servers/server20000000001
[zk: localhost:2181(CONNECTED) 12] create -s /servers/server2 "server2 value"
Created /servers/server20000000002
[zk: localhost:2181(CONNECTED) 13] get -s /servers/server2
org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /servers/server2
[zk: localhost:2181(CONNECTED) 15] get -s /servers/server20000000001
server2 value
cZxid = 0x200000006
ctime = Sun Jan 02 18:30:04 UTC 2022
mZxid = 0x200000006
mtime = Sun Jan 02 18:30:04 UTC 2022
pZxid = 0x200000006
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 13
numChildren = 0
[zk: localhost:2181(CONNECTED) 16] get -s /servers/server20000000002
server2 value
cZxid = 0x200000007
ctime = Sun Jan 02 18:30:08 UTC 2022
mZxid = 0x200000007
mtime = Sun Jan 02 18:30:08 UTC 2022
pZxid = 0x200000007
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 13
numChildren = 0
创建 短暂节点不带序号
[zk: localhost:2181(CONNECTED) 17] create -e /servers/server3 "server3 value"
Created /servers/server3
[zk: localhost:2181(CONNECTED) 18] get -s /servers/server3
server3 value
cZxid = 0x200000008
ctime = Sun Jan 02 18:34:37 UTC 2022
mZxid = 0x200000008
mtime = Sun Jan 02 18:34:37 UTC 2022
pZxid = 0x200000008
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x100002b56cf0000
dataLength = 13
numChildren = 0
创建 短暂节点带序号
[zk: localhost:2181(CONNECTED) 19] create -e -s /servers/server4 "server4 value"
Created /servers/server40000000004
[zk: localhost:2181(CONNECTED) 20] create -e -s /servers/server4 "server4 value"
Created /servers/server40000000005
[zk: localhost:2181(CONNECTED) 21] get -s /servers/server4
org.apache.zookeeper.KeeperException$NoNodeException: KeeperErrorCode = NoNode for /servers/server4
[zk: localhost:2181(CONNECTED) 22] get -s /servers/server40000000004
server4 value
cZxid = 0x200000009
ctime = Sun Jan 02 18:35:27 UTC 2022
mZxid = 0x200000009
mtime = Sun Jan 02 18:35:27 UTC 2022
pZxid = 0x200000009
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x100002b56cf0000
dataLength = 13
numChildren = 0
[zk: localhost:2181(CONNECTED) 23] get -s /servers/server40000000005
server4 value
cZxid = 0x20000000a
ctime = Sun Jan 02 18:35:41 UTC 2022
mZxid = 0x20000000a
mtime = Sun Jan 02 18:35:41 UTC 2022
pZxid = 0x20000000a
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x100002b56cf0000
dataLength = 13
numChildren = 0
[zk: localhost:2181(CONNECTED) 25] ls /servers
[server1, server20000000001, server20000000002, server3, server40000000004, server40000000005]
退出客户端:
quit
重新查看:
[zk: localhost:2181(CONNECTED) 0] ls /servers
[server1, server20000000001, server20000000002]
监听器原理
客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、节点删除、子目录节点增加删除)时,ZooKeeper 会通知客户端。监听机制保证 ZooKeeper 保存的任何的数据的任何改变都能快速的响应到监听了该节点的应用程序。
1)首先要有一个main()线程
2)在main线程中创建Zookeeper客户端,这时就会创建两个线程,一个负责网络连接通信(connet),一个负责监听(listener)。
3)通过connect线程将注册的监听事件发送给Zookeeper。
4)在Zookeeper的注册监听器列表中将注册的监听事件添加到列表中。
5)Zookeeper监听到有数据或路径变化,就会将这个消息发送给listener线程。
6)listener线程内部调用了process()方法。
常见的监听
监听节点数据的变化
在zk1上起一个客户端:
[vagrant@localhost bin]$ ./zkCli.sh
执行以下操作:
[zk: localhost:2181(CONNECTED) 2] create /watch
Created /watch
[zk: localhost:2181(CONNECTED) 3] ls /
[servers, watch, zookeeper]
[zk: localhost:2181(CONNECTED) 4] get -w /watch
null
在zk2上起一个客户端:
[vagrant@localhost bin]$ ./zkCli.sh
执行:
[zk: localhost:2181(CONNECTED) 2] set /watch "111"
[zk: localhost:2181(CONNECTED) 3] set /watch "222"
这时观察zk1
[zk: localhost:2181(CONNECTED) 5]
WATCHER::
WatchedEvent state:SyncConnected type:NodeDataChanged path:/watch
注意:在zk2上多次修改/watch 的值,zk1上不会再收到监听。因为注册一次,只能监听一次。想再次监听,需要再次注册。
监听子节点增减的变化
在zk1上监听:
[zk: localhost:2181(CONNECTED) 2] ls -w /watch
[]
[zk: localhost:2181(CONNECTED) 3]
在zk2 上 增肌子节点:
[zk: localhost:2181(CONNECTED) 1] create /watch/node1
Created /watch/node1
[zk: localhost:2181(CONNECTED) 2] create /watch/node2
Created /watch/node2
再观察zk1:
WATCHER::
WatchedEvent state:SyncConnected type:NodeChildrenChanged path:/watch
注意:节点的路径变化,也是注册一次,生效一次。想多次生效,就需要多次注册。
节点删除与查看
先创建节点:
[zk: localhost:2181(CONNECTED) 4] create /path1
Created /path1
[zk: localhost:2181(CONNECTED) 5] create /path1/path2
Created /path1/path2
删除单层节点
[zk: localhost:2181(CONNECTED) 8] delete /path1
Node not empty: /path1
[zk: localhost:2181(CONNECTED) 9] delete /path1/path2
[zk: localhost:2181(CONNECTED) 10] ls /path1
[]
递归删除节点
[zk: localhost:2181(CONNECTED) 11] create /path1/path2
Created /path1/path2
[zk: localhost:2181(CONNECTED) 12] create /path1/path2/path3
Created /path1/path2/path3
[zk: localhost:2181(CONNECTED) 13] ls /path1
[path2]
[zk: localhost:2181(CONNECTED) 14] ls /path1/path2
[path3]
[zk: localhost:2181(CONNECTED) 15] deleteall /path1/path2/path3
[zk: localhost:2181(CONNECTED) 16] ls /
[path1, servers, watch, zookeeper]
[zk: localhost:2181(CONNECTED) 17] ls /path1
[path2]
[zk: localhost:2181(CONNECTED) 18] ls /path1/path2
[]
从执行结果可以看出,只是删除的叶子节点
查看节点状态
[zk: localhost:2181(CONNECTED) 20] create /stat1
Created /stat1
[zk: localhost:2181(CONNECTED) 21] stat /stat1
cZxid = 0x900000010
ctime = Fri Jan 14 02:31:31 UTC 2022
mZxid = 0x900000010
mtime = Fri Jan 14 02:31:31 UTC 2022
pZxid = 0x900000010
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 0
[zk: localhost:2181(CONNECTED) 22] create /stat1/stat2
Created /stat1/stat2
[zk: localhost:2181(CONNECTED) 23] stat /stat1
cZxid = 0x900000010
ctime = Fri Jan 14 02:31:31 UTC 2022
mZxid = 0x900000010
mtime = Fri Jan 14 02:31:31 UTC 2022
pZxid = 0x900000011
cversion = 1
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 0
numChildren = 1
客户端 API 操作
引入依赖
<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>
代码
新建一个测试类:ZooKeeperClientTest
package com.youzijun.zk;
import org.apache.zookeeper.*;
import org.junit.Before;
import org.junit.Test;
import java.util.List;
/**
* @author 吴尚慧
* @since 2022/1/18 22:28
*/
public class ZooKeeperClientTest {
private static String connectString = "192.168.3.51:2181,192.168.3.52:2181,192.168.3.53:2181";
private static int sessionTimeout = 200000;
private ZooKeeper zkClient = null;
@Before
public void init() throws Exception {
zkClient = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// 收到事件通知后的回调函数(用户的业务逻辑)
System.out.println(watchedEvent);
}
});
}
}
使用@Before来初始化ZooKeeper客户端,注册一个监听器,只能监听一次,连续监听需要多次注册。
创建节点
/**
* 创建节点
*/
@Test
public void create() throws Exception {
// 参数 1:要创建的节点的路径; 参数 2:节点数据 ; 参数 3:节点权限 ; 参数 4:节点的类型
String nodeCreated = zkClient.create("/youzijunkj", "hello".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
System.out.println("创建节点: " + nodeCreated);
}
WatchedEvent state:SyncConnected type:None path:null
创建节点: /youzijunkj
重复创建会报错:
org.apache.zookeeper.KeeperException$NodeExistsException: KeeperErrorCode = NodeExists for /youzijunkj
.....
查看节点数据
@Test
public void getNodeData() throws KeeperException, InterruptedException {
byte[] data = zkClient.getData("/youzijunkj", false, null);
System.out.println(new String(data));
}
WatchedEvent state:SyncConnected type:None path:null
hello
修改节点数据
@Test
public void setValue() throws Exception {
zkClient.setData("/youzijunkj", "".getBytes(), -1);
}
删除节点
/**
* 删除节点
*
* @throws Exception
*/
@Test
public void delete() throws Exception {
zkClient.delete("/youzijunkj", -1);
}
存在子节点删除会抛异常
org.apache.zookeeper.KeeperException$NotEmptyException: KeeperErrorCode = Directory not empty for /youzijunkj
查看子节点
/**
* 获取子节点
*
* @throws Exception
*/
@Test
public void getChildren() throws Exception {
List<String> children = zkClient.getChildren("/", true);
for (String child : children) {
System.out.println(child);
}
// 延时阻塞
Thread.sleep(Long.MAX_VALUE);
}
WatchedEvent state:SyncConnected type:None path:null
zookeeper
youzijunkj
判断节点是否存在
/**
* 判断 znode 是否存在
*
* @throws Exception
*/
@Test
public void exist() throws Exception {
Stat stat = zkClient.exists("/existtest", true);
System.out.println("stat=" + stat);
Thread.sleep(Long.MAX_VALUE);
}
WatchedEvent state:SyncConnected type:None path:null
stat=null
上面我们设置了监听,不声明新的监听器走的是默认的监听器,也就是init()方法中的监听器,方法中只是打印了一下监听事件
使用命令行方式手动创建一下节点看看效果:
[zk: localhost:2181(CONNECTED) 2] create /existtest
Created /existtest
在程序控制台上面可以看到:
WatchedEvent state:SyncConnected type:NodeCreated path:/existtest
创建之后再重新运行一下程序
WatchedEvent state:SyncConnected type:None path:null
stat=60129542216,60129542216,1642201787028,1642201787028,0,0,0,0,0,0,60129542216
接着再修改一下值:
[zk: localhost:2181(CONNECTED) 3] set /existtest '1234'
在程序控制台上面可以看到:
WatchedEvent state:SyncConnected type:NodeDataChanged path:/existtest