Zookeeper集群及相关概念
一、前言
本文章主要讲述zk的集群使用到的概念,比如ZAB协议,如何搭建zk集群,怎么使用javaApi来操作zk集群。
首先我们要下载zookeeper,将安装包解压,安装zk就不说了。安装成功后按以下步骤进行:
1. 重命名zoo_sample.cfg文件
cp conf/zoo_sample.cfg conf/zoo-1.cfg
2.修改配置文件zoo-1.cfg,配置文件里有的,改成下面的值,没有的加上
dataDir=/tmp/zookeeper-1
# the port at which the clients will connect
clientPort=2181
server.1=192.168.199.11:2888:3888
server.2=192.168.199.11:2889:3889
server.3=192.168.199.11:2890:3890
3.再从zoo-1.cfg复制两个配置文件zoo-2.cfg和zoo-3.cfg,只需要修改dataDir和clientPort不同即可。
[root@localhost conf]# cp zoo-1.cfg zoo-2.cfg
[root@localhost conf]# cp zoo-1.cfg zoo-3.cfg
#vim zoo-2.cfg
dataDir=/tmp/zookeeper-2
# the port at which the clients will connect
clientPort=2182
#vim zoo-3.cfg
dataDir=/tmp/zookeeper-3
# the port at which the clients will connect
clientPort=2183
4.标志Server ID
创建三个文件夹/tmp/zookeeper-1,/tmp/zookeeper-2,/tmp/zookeeper-3
在每个目录中建立文myid文件,写入当前实例的server id, 即1,2,3
当然这个实例id你自己可以自定义,这个myid主要是用来zk主节点选举时,声明你是哪个节点的。
5. 启动三个zk实例
[root@localhost zookeeper-3.4.14]# bin/zkServer.sh start conf/zoo-1.cfg
ZooKeeper JMX enabled by default
Using config: conf/zoo-1.cfg
Starting zookeeper ... STARTED
[root@localhost zookeeper-3.4.14]# bin/zkServer.sh start conf/zoo-2.cfg
ZooKeeper JMX enabled by default
Using config: conf/zoo-2.cfg
Starting zookeeper ... STARTED
[root@localhost zookeeper-3.4.14]# bin/zkServer.sh start conf/zoo-3.cfg
ZooKeeper JMX enabled by default
Using config: conf/zoo-3.cfg
Starting zookeeper ... STARTED
此时zk集群就启动了。
二、查看集群的状态
查看集群状态也十分简单,启动集群后可以通过下面命令查看
[root@localhost zookeeper-3.4.14]# bin/zkServer.sh status conf/zoo-3.cfg
ZooKeeper JMX enabled by default
Using config: conf/zoo-3.cfg
Mode: leader
[root@localhost zookeeper-3.4.14]# bin/zkServer.sh status conf/zoo-2.cfg
ZooKeeper JMX enabled by default
Using config: conf/zoo-2.cfg
Mode: follower
[root@localhost zookeeper-3.4.14]# bin/zkServer.sh status conf/zoo-1.cfg
ZooKeeper JMX enabled by default
Using config: conf/zoo-1.cfg
Mode: standalone
注意到,zoo-3的实例时master角色,zoo-2则是zoo-3的一个从节点。
观察zoo-1发现启动模式是standalone,表示这个节点没正常启动。经过排查可以发现,这个节点的端口之前是以单机模式启动的,我们需要把这个节点停止掉,并且删除对应的tmp目录下存放该节点的数据的dataDir的文件(保留myid即可)。
然后重启该节点即可。
三、停止集群节点
[root@localhost bin]# ./zkServer.sh stop ../conf/zoo-2.cfg
ZooKeeper JMX enabled by default
Using config: ../conf/zoo-2.cfg
Stopping zookeeper ... STOPPED
[root@localhost bin]# ./zkServer.sh stop ../conf/zoo-3.cfg
ZooKeeper JMX enabled by default
Using config: ../conf/zoo-3.cfg
Stopping zookeeper ... STOPPED
四、启动|停止|查看节点状态的脚本
提供一个日常集群常用操作脚本
[root@localhost bin]# cat zkOperation.sh
#!/bin/bash
for i in "$@"; do
if [ "$i" = "start" ];then
./zkServer.sh start ../conf/zoo-1.cfg
./zkServer.sh start ../conf/zoo-2.cfg
./zkServer.sh start ../conf/zoo-3.cfg
fi
if [ "$i" = "status" ];then
./zkServer.sh status ../conf/zoo-1.cfg
./zkServer.sh status ../conf/zoo-2.cfg
./zkServer.sh status ../conf/zoo-3.cfg
fi
if [ "$i" = "stop" ];then
./zkServer.sh stop ../conf/zoo-1.cfg
./zkServer.sh stop ../conf/zoo-2.cfg
./zkServer.sh stop ../conf/zoo-3.cfg
fi
done
键入命令即可执行
[root@localhost bin]# ./zkOperation.sh start status
ZooKeeper JMX enabled by default
Using config: ../conf/zoo-1.cfg
Starting zookeeper ... STARTED
ZooKeeper JMX enabled by default
Using config: ../conf/zoo-2.cfg
Starting zookeeper ... STARTED
ZooKeeper JMX enabled by default
Using config: ../conf/zoo-3.cfg
Starting zookeeper ... STARTED
ZooKeeper JMX enabled by default
Using config: ../conf/zoo-1.cfg
Mode: follower
ZooKeeper JMX enabled by default
Using config: ../conf/zoo-2.cfg
Mode: leader
ZooKeeper JMX enabled by default
Using config: ../conf/zoo-3.cfg
Mode: follower
五、连接集群
集群节点都启动成功了,就可以使用下面的命令用客户端连接到集群。
./zkCli.sh -server 192.168.199.11:2181
[root@localhost bin]# ./zkCli.sh -server 192.168.199.11:2181
Connecting to 192.168.199.11:2181
2020-04-29 10:43:43,781 [myid:] - INFO [main:Environment@100] - Client environment:zookeeper.version=3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
2020-04-29 10:43:43,789 [myid:] - INFO [main:Environment@100] - Client environment:host.name=localhost
2020-04-29 10:43:43,789 [myid:] - INFO [main:Environment@100] - Client environment:java.version=1.8.0_131
2020-04-29 10:43:43,794 [myid:] - INFO [main:Environment@100] - Client environment:java.vendor=Oracle Corporation
2020-04-29 10:43:43,794 [myid:] - INFO [main:Environment@100] - Client environment:java.home=/usr/java/jdk1.8.0_131/jre
2020-04-29 10:43:43,794 [myid:] - INFO [main:Environment@100] - Client environment:java.class.path=/usr/local/zookeeper/zookeeper-3.4.14/bin/../zookeeper-server/target/classes:/usr/local/zookeeper/zookeeper-3.4.14/bin/../build/classes:/usr/local/zookeeper/zookeeper-3.4.14/bin/../zookeeper-server/target/lib/*.jar:/usr/local/zookeeper/zookeeper-3.4.14/bin/../build/lib/*.jar:/usr/local/zookeeper/zookeeper-3.4.14/bin/../lib/slf4j-log4j12-1.7.25.jar:/usr/local/zookeeper/zookeeper-3.4.14/bin/../lib/slf4j-api-1.7.25.jar:/usr/local/zookeeper/zookeeper-3.4.14/bin/../lib/netty-3.10.6.Final.jar:/usr/local/zookeeper/zookeeper-3.4.14/bin/../lib/log4j-1.2.17.jar:/usr/local/zookeeper/zookeeper-3.4.14/bin/../lib/jline-0.9.94.jar:/usr/local/zookeeper/zookeeper-3.4.14/bin/../lib/audience-annotations-0.5.0.jar:/usr/local/zookeeper/zookeeper-3.4.14/bin/../zookeeper-3.4.14.jar:/usr/local/zookeeper/zookeeper-3.4.14/bin/../zookeeper-server/src/main/resources/lib/*.jar:/usr/local/zookeeper/zookeeper-3.4.14/bin/../conf:/usr/java/jdk1.8.0_131/jre/lib/ext:/usr/java/jdk1.8.0_131/lib/tools.jar
2020-04-29 10:43:43,795 [myid:] - INFO [main:Environment@100] - Client environment:java.library.path=/usr/java/packages/lib/amd64:/usr/lib64:/lib64:/lib:/usr/lib
2020-04-29 10:43:43,795 [myid:] - INFO [main:Environment@100] - Client environment:java.io.tmpdir=/tmp
2020-04-29 10:43:43,795 [myid:] - INFO [main:Environment@100] - Client environment:java.compiler=<NA>
2020-04-29 10:43:43,795 [myid:] - INFO [main:Environment@100] - Client environment:os.name=Linux
2020-04-29 10:43:43,795 [myid:] - INFO [main:Environment@100] - Client environment:os.arch=amd64
2020-04-29 10:43:43,795 [myid:] - INFO [main:Environment@100] - Client environment:os.version=3.10.0-1062.1.2.el7.x86_64
2020-04-29 10:43:43,795 [myid:] - INFO [main:Environment@100] - Client environment:user.name=root
2020-04-29 10:43:43,796 [myid:] - INFO [main:Environment@100] - Client environment:user.home=/root
2020-04-29 10:43:43,796 [myid:] - INFO [main:Environment@100] - Client environment:user.dir=/usr/local/zookeeper/zookeeper-3.4.14/bin
2020-04-29 10:43:43,798 [myid:] - INFO [main:ZooKeeper@442] - Initiating client connection, connectString=192.168.199.11:2181 sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@67424e82
Welcome to ZooKeeper!
JLine support is enabled
2020-04-29 10:43:44,158 [myid:] - INFO [main-SendThread(192.168.199.11:2181):ClientCnxn$SendThread@1025] - Opening socket connection to server 192.168.199.11/192.168.199.11:2181. Will not attempt to authenticate using SASL (unknown error)
2020-04-29 10:43:44,331 [myid:] - INFO [main-SendThread(192.168.199.11:2181):ClientCnxn$SendThread@879] - Socket connection established to 192.168.199.11/192.168.199.11:2181, initiating session
2020-04-29 10:43:44,535 [myid:] - INFO [main-SendThread(192.168.199.11:2181):ClientCnxn$SendThread@1299] - Session establishment complete on server 192.168.199.11/192.168.199.11:2181, sessionid = 0x1000406ed350000, negotiated timeout = 30000
WATCHER::
WatchedEvent state:SyncConnected type:None path:null
[zk: 192.168.199.11:2181(CONNECTED) 0]
此时表示连接到集群节点为 192.168.199.11:2181的节点。我们可以设置下值
[zk: 192.168.199.11:2181(CONNECTED) 1] create /username haizhang
Created /username
[zk: 192.168.199.11:2181(CONNECTED) 2] get /username
haizhang
cZxid = 0x100000002
ctime = Wed Apr 29 10:45:25 CST 2020
mZxid = 0x100000002
mtime = Wed Apr 29 10:45:25 CST 2020
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 0
##2182的机器也可以查到
[zk: 192.168.199.11:2182(CONNECTED) 0] get /username
haizhang
cZxid = 0x100000002
ctime = Wed Apr 29 10:45:25 CST 2020
mZxid = 0x100000002
mtime = Wed Apr 29 10:45:25 CST 2020
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 0
##2183的机器也可以查到
[zk: 192.168.199.11:2183(CONNECTED) 0] get /username
haizhang
cZxid = 0x100000002
ctime = Wed Apr 29 10:45:25 CST 2020
mZxid = 0x100000002
mtime = Wed Apr 29 10:45:25 CST 2020
pZxid = 0x100000002
cversion = 0
dataVersion = 0
aclVersion = 0
ephemeralOwner = 0x0
dataLength = 8
numChildren = 0
可以看到在2181机器设置的username目录及节点数据,会同步到2182、2183这两个子节点。并且2181是从节点,从节点上只能够读,但是我们发现在连接从节点操作写命令也可以成功!这里就要涉及到ZAB原子广播协议了。
六、ZAB原子广播协议
ZAB原子广播协议包括两个方面:
下面我们将对ZAB原子广播协议进行分开讲解,首先讲解下集群消息广播(二阶段提交)保证数据一致性。
6.1 二阶段提交
为什么从节点只负责读但可以执行写命令?
集群处理请求示意图:
实际上写请求是发送到了从节点上,但是实际上从节点并不是真正的去执行写操作,而是将这个写命令转发到Leader节点中。
这里就是设计到了集群消息广播(二阶段提交)的一个概念
写请求就会有个zxid的东西,从服务器收到客户端的写请求之后就会将写请求转发到leader服务器上,leader接收到这个写命令的时候,就会生成一个按顺序的全局唯一的zxid(按请求到达leader服务器的先后顺序的)。实际上就相当于leader节点为所有写操作的请求按先后顺序(换句话讲按zxid大小)放入队列,谁先到就先执行谁。那么如果有并发的写请求到来,实际上leader底层也是会加锁并生成zxid。zxid可称为唯一的、全局的、顺序的 id。
所有对zk数据做修改新增操作的动作,leader都会为它生成一个zxid,也就是事务的id(写操作才叫说事务,会对数据做修改。读操作不能叫事务,不会对数据做修改)
那么当leader节点要处理某个写请求的时候,首先会将这个写请求的zxid通知到集群中的follower,并等待follwer的ack确认收到写事务id并准备好接收数据的回应。只要集群中超过半数以上的follower响应ack到leader节点中,leader节点就进行对数据的提交操作,将这个zxid的写请求数据Commit到响应ack命令的节点上。这就完成了数据同步的一个操作。
实际上follwer也是要执行写命令,才能让follwer拥有leader发送的节点数据的。
那为什么不一开始就让follwer执行写操作,而是要兜一个大圈子呢? 答案就是保证数据一致性
如果你多个写请求发送到zk的多个节点,这多个节点都并发的去写数据,那就可能出现数据不一致的情况,难以控制和管理。如果将这几个写请求统一发送给某台服务器做中转处理,那么就能够保证follower之间存在半数以上的机器拥有这个数据,数据就不容易丢失。而且更重要的一点是,保证性能。
6.2 集群节点中的leader宕机了,怎么办?
当zk的leader宕机了,就会被其他的follower发现,此时就会在剩下可用的follower中发起选票,选举一个follower成为为zk的leader。
那么选举的过程又是怎么样的呢?什么样的follower才可以成为leader呢?
假设我们存在三台zk集群的节点。
其中ZKNode2为leader节点,当ZKNode2接收到一个写请求的时候,假设leader为这个写请求生成的事务zxid=102。当把这个事务通知完ZKNode3做同步时,而还没来得及给ZKNode1同步这个事务,ZKNode2突然宕机了。此时ZKNode1就相当于没有接收到这个写事务。
上图说明的很明白,ZKnode1中存放的zxid最大值还是101 ,而ZKnode3变成了102。 当leader宕机了,follwer1和follwer2就会通知集群的其他节点开始选举leader。
这个时候,follower2就会通过选举投票端口发送自己的信息给其他follwer进行投票,这条信息的格式为(myid,节点最大zxid),其中myid则为你zk配置文件中保存在dataDir目录下的myid文件的内容,标志当前节点。
上图中follower2会发给follower1的消息即为(3,102) ,follower1回发给follower2的消息为(1,101)
在发送消息之前,各自都给自己投上一票。
如果follower节点收到其他其他follower节点转发它自己的消息,那就会在票数的基础上为自己再加一票,否则就会忽略这条消息。就像上面,zkNode1发现自己的zxid比zkNode3要小,那就会给把zkNode3的消息转发给候选节点,同样的也会发回给zkNode3,当zkNode3发现是别的集群节点发送自己的消息的时候,就会为自己再加上一票。
主节点计算公式:只要某个节点的选票大于集群中总机器数的一半,就可以成为集群的leader了,这个节点就会把自己的节点设置成主节点。
七、java客户端连接集群
很简单,做如下操作:
public class ZookeeperProSync implements Watcher {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
private static ZooKeeper zk = null;
private static Stat stat = new Stat();
public static void main(String[] args) {
//zookeeper配置数据存放路径,相当于在zk下新建一个存放数据节点的目录
String path = "/username";
try {
//连接zk并注册一个默认的监听器,其中192.168.199.11:2181是zk提供客户端连接的主机端口地址,集群机器使用,隔开。
//sessionTimeout的值表示客户端和服务端保持连接,心跳超时的时间。因为zk客户端和服务端是基于长连接的。所以服务器会和客户端使用心跳机制保持连接状态,一旦超过指定心态时间没回复,就把客户端剔除。
zk = new ZooKeeper("192.168.199.11:2183,192.168.199.11:2182,192.168.199.11:2181",5000,new ZookeeperProSync() );
//等待zk连接成功的通知
connectedSemaphore.await();
//获取path目录节点的配置数据,并注册默认的监听器。 其中zk.getData相当于zk的get命令,去指定的目录下获取数据。
//而该方法的第二个参数watch=true表明当前客户端设置了一个监听器在zk的path目录下,只要path目录有变动,就立刻发送一个监听事件回来通知客户端。
System.out.println(new String(zk.getData(path,true,stat)));
//模拟程序一直在运行
Thread.sleep(Integer.MAX_VALUE);
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
//zk监听事件处理逻辑。
public void process(WatchedEvent watchedEvent) {
//zk成功连接的通知事件
if(Event.KeeperState.SyncConnected == watchedEvent.getState()){
if(Event.EventType.None == watchedEvent.getType() && null == watchedEvent.getPath()){
connectedSemaphore.countDown();
}else if (watchedEvent.getType() == Event.EventType.NodeDataChanged){
//zk目录节点数据发生变化的通知事件
try {
System.out.println("配置已被修改,新值为:"+new String(zk.getData(watchedEvent.getPath(),true,stat)));
}catch (Exception e){
e.printStackTrace();
}
}
}
}
}