Zookeeper
1. Zookeeper入门
【1】.概述
Zookeeper是一个开源的分布式的,为分布式框架提供协调服务的Apache项目;
Zookeepr从设计模式角度看:是一个基于观察者模式设计的分布式服务管理框架,负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在Zookeeper上注册的那些观察者做出相应的反应;
Zookeeper = 文件系统 + 通知机制;
【2】.特点
1). Zookeeper:一个领导者,多个跟随者组成的集群;
2). 集群中只要有半数以上节点存活,Zookeeper集群就能够正常提供服务,所以Zookeeper适合安装奇数台服务器;
3). 全局数据一致,每个Server都保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的;
4). 更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行;
5). 数据更新原子性,一次数据更新要么成功,要么失败;
6). 实时性,在一定范围内,Client能够读到最新的数据;
【3】.数据结构
Zookeeper数据模型结构与Unix文件系统很类似,整体上可以看作一棵树,每个节点称做一个ZNode。每一个ZNode默认能够存储1MB的数据,每个ZNode都可以通过其路径唯一标识;
![]()
【4】.应用场景
1). 统一命名服务
在分布式环境下,经常需要对应用/服务进行统一命名,便于识别;
2). 统一配置管理
一般要求一个集群中,所有节点的配置信息是一致的,比如Kafka集群;
对配置文件修改后,希望能够快速同步到各个节点上;
EX:将配置信息写入到Zookeeper上的一个ZNode,然后各个客户端服务器都会监听这个ZNode;一旦ZNode中的数据被修改,Zookeeper将通知各个客户端服务器;
3). 统一集群管理
Zookeeper可以实现实时监控节点状态变化,可以将节点信息写入到Zookeeper上的一个ZNode,然后监听这个ZNode,即可以获取它的实时状态变化;
4). 服务器动态上下线
客户端能够实时观察到服务器的上下线的变化;
5). 软负载均衡
在Zookeeper中记录每台服务器的访问数,让访问数最少的服务器去处理最新的客户端请求;
2. Zookeeper安装
【1】.配置参数解读
1).tickTime=2000,通信心跳时间,Zookeeper服务器与客户端心跳时间,单位毫秒;
2).initLimit=10,LF初始通信时限;Leader和Follower初始连接时能容忍的最多心跳数;
3).syncLimit=5,LF同步通信时限;Leader和Follower之间的通信时间如果超过syncLimit * tickTime,则Leader认为follower死掉,从服务器列表中删除Follower;
4).dataDir:保存Zookeeper中的数据;注意:默认的tmp目录,容易被Linux系统定期删除,所以一般不用默认的tmp目录;
5).clientPort: 客户端连接端口,通常不做修改;
3. Zookeeper集群操作
【1】.集群安装
1). 在所有服务器上解压zookeeper安装包;
tar -zxvf apache-zookeeper-3.5.7-bin.tar.gz
2). 在zookeeper目录下创建zkData文件夹;
mkdir zkData
3). 在文件夹下创建myid文件,并添加server对应的编号;
vim myid
4). 将配置文件zoo-sample.cfg的名字修改为zoo.cfg;
mv zoo-sample.cfg zoo.cfg
5). 修改配置文件中数据的存储路径;
dataDir=/opt/zookeeper/apache-zookeeper-3.5.7-bin/zkData
6). 配置文件中增加如下配置;
server.2 = x.x.x.x:2888:3888
server.3 = x.x.x.x:2888:3888
server.4 = x.x.x.x:2888:3888
server.A=B:C:D
A - 是一个数字,表示是第几号服务器,与myid文件中的数字相对应;
B - 是服务器的地址;
C - 是服务器Follower与集群中的Leader服务器交换信息的端口;
D - 是万一集群中的Leader服务器挂了,需要一个端口来重新进行选举,选举出一个新的Leader,该端口是用来执行选举时服务器相互通信的端口;
【2】.Zookeepr第一次启动选举机制
(1).服务器1启动,发起选举,将选票投给自己,此时服务器1票数为1,不够半数,选举无法完成,服务器1的状态保持为LOOKING;
(2).服务器2启动,再次发起选举,服务器1和服务器2分别投自己1票并且交换选票信息,此时服务器1发现服务器2的myid比自己目前投票推举的服务器的myid大,因此更改选票为推荐服务器2,此时服务器1为0票,服务器2为2票,没有半数以上结果,选举无法完成,服务器1,2保持LOOKING状态;
(3).服务器3启动,发起选举,服务器1和2更改选票为服务器3,服务器3获得3票,超过半数,当选Leader,服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING;
(4).服务器4启动,发起一次选举,但由于服务器1,2,3不再是LOOKING状态,因此不会更改选票信息,服务器3依然为Leader,服务器4为FOLLOWER,并更改状态为FOLLOWING;
(5).服务器5同服务器4;
![]()
【3】.非第一次启动选举机制
(1).当Zookeeper集群中的一台服务器出现以下两种情况之一时,会开始进入Leader选举:
- 服务器初始化启动;
- 服务器运行期间,无法和Leader保持连接;
(2).当一台机器进入Leader选举流程时,当前集群可能会处于以下两种状态:
- 集群中本来存在Leader;
机器试图去选举Leader,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader建立连接,并进行状态同步即可;
- 集群中确实不存在Leader;
假设Zookeeper由5台服务器组成,SID分别为1,2,3,4,5,ZXID分别为8,8,8,7,7,此时服务器3为Leader,某一时刻,3和5服务器出现故障,因此开始进行选举;
选举规则:EPOCH大的直接胜出;
EPOCH相同的,ZXID大的胜出;
ZXID相同的,SID大的胜出;
【4】.zookeepr集群启动/停止脚本
1 #!/bin/bash
2
3 case $1 in
4 "start")
5 for i in 192.168.147.100 192.168.147.101 192.168.147.102
6 do
7 echo "--启动 $i zookeeper"
8 ssh $i "/opt/zookeeper/zookeeper/bin/zkServer.sh start"
9 done
10 ;;
11 "stop")
12 for i in 192.168.147.100 192.168.147.101 192.168.147.102
13 do
14 echo "--停止 $i zookeeper"
15 ssh $i "/opt/zookeeper/zookeeper/bin/zkServer.sh stop"
16 done
17 ;;
18 "status")
19 for i in 192.168.147.100 192.168.147.101 192.168.147.102
20 do
21 echo "--查询状态 $i zookeeper"
22 ssh $i "/opt/zookeeper/zookeeper/bin/zkServer.sh status"
23 done
24 ;;
25 esac
4. Zookeeper客户端命令行操作
【1】.节点信息(get -s /)
cZxid - 创建节点的事务zxid;
ctime - znode被创建的毫秒数;
mzxid - znode最后更新的事务zxid;
mtime - znode最后修改的毫秒数;
pZxid - znode最后更新的子节点zxid;
cversion - znode子节点变化号,znode子节点修改次数;
dataversion - znode数据变化号;
aclVersion - znode访问控制表的变化号;
ephemeralOwner - 如果是临时节点,这个是znode拥有者的session id,如果不是临时节点,则是0;
dataLength - znode的数据长度;
numChildren - znode的子节点数量;
![]()
【2】.节点类型
持久:客户端和服务端断开连接后,创建的节点不删除;
持久化目录节点/持久化顺序编号目录节点(有编号)
短暂:客户端和服务端断开连接后,创建的节点自己删除;
临时目录节点/临时顺序编号目录节点(有编号)
创建持久化节点: create /znode "z1"
创建带序号的持久化节点: create -s /znode "z2"
创建临时节点:create -e /znode "z3"
创建带序号的临时节点: create -e -s /znode "z4"
【3】.监听器及节点删除
监听器原理:
1). 首先有一个main()线程;
2). 在main()线程中创建一个zookeeper客户端,这里会创建两个线程,一个负责网络连接通信(connect),一个负责监听(listener);
3). 通过connect线程将注册的监听事件发送给zookeeper;
4). 在zookeeper的注册监听器列表中将注册的监听事件添加到列表中;
5). zookeeper监听到有数据或路径变化,会将这个消息发送给listener线程;
6). listener线程内部调用process()方法;
![]()
监听内容:
1). 监听节点数据的变化; get -w /znode NodeDataChanged
2). 监听子节点增减的变化; ls -w /znode NodeChildrenChanged
注册一次监听,则只能监听一次,如果想要再次监听,则需要重新注册;
节点删除及查看:
删除节点:delete /znode
递归删除节点:deleteall /znode
查看节点状态:stat /znode
设置节点内容:set /znode "XXX"
【4】.Idea创建节点
1). 创建zookeeper客户端;
@Test
public void init() throws IOException {
int sessionTimeout = 10000;
String connectString = "192.168.75.100:2181,192.168.75.101:2181,192.168.75.102:2181";
ZooKeeper zooKeeper = new ZooKeeper(connectString, sessionTimeout, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
}
});
}
2). 创建节点;
@Test
public void create() throws IOException, InterruptedException, KeeperException {
String node = zooKeeper.create("/atguigu", "songhongkang".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
3). 获取子节点并监听其变化;
@Test
public void getChildren() throws IOException, InterruptedException, KeeperException {
List<String> children = zooKeeper.getChildren("/atguigu", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
try {
List<String> children = zooKeeper.getChildren("/atguigu", new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
}
});
System.out.println(children);
} catch (KeeperException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
System.out.println(children);
Thread.sleep(Integer.MAX_VALUE);
}
4). 判断节点是否存在;
@Test
public void exist() throws IOException, InterruptedException, KeeperException {
Stat stat = zooKeeper.exists("/atguigu", false);
System.out.println(stat);
}
所需jar包:
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.5.3-beta</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-client</artifactId>
<version>4.0.1</version>
</dependency>
【5】.写数据原理
1). 写入请求直接发送给Leader节点;
![]()
2). 写入请求发送给follower节点;
![]()
5. 服务器动态上下线监听案例
![]()
【1】.服务器注册
public class DistributedServer {
private int sessionTimeout = 10000;
private String connectionString = "192.168.75.100:2181,192.168.75.101:2181,192.168.75.102:2181";
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
DistributedServer distributedServer = new DistributedServer();
【2】.客户端监听
public class DistributedClient {
private String connectionString = "192.168.75.100:2181,192.168.75.101:2181,192.168.75.102:2181";
private int sessionTimeout = 10000;
public static void main(String[] args) throws IOException, InterruptedException, KeeperException {
DistributedClient distributedClient = new DistributedClient();
【3】.测试
![]()
6. Zookeeper分布式锁案例
![]()
【1】.代码实现
public class DistributedLock {
private final String connectionString = "192.168.75.100:2181,192.168.75.101:2181,192.168.75.102:2181";
private final int sessionTimeout = 10000;
private CountDownLatch countDownLatch = new CountDownLatch(1);
private CountDownLatch waitDownLatch = new CountDownLatch(1);
public ZooKeeper zooKeeper;
private String previousPath;
private String currentNode;
public DistributedLock() throws IOException, InterruptedException, KeeperException {