目录
一. ZooKeeper 概述
-
什么是Zookeeper: 分布式协调工具,(java语言编写的开源框架)
-
Zookeeper的使用场景:
注册中心(命名服务,将服务以持久节点.临时节点存入Zookeeper中),
基于Zookeeper 实现负载均衡
基于Zookeeper 实现分布式锁
Zookeeper实现分布式配置中心,动态的管理配置文件(Apollo配置中心,SpringCloud 中有 config配置中心,)
Zookeeper 实现分布式事务管理
Zookeeper 选举策略(哨兵机制)
Zookeeper 实现本地负载均衡(Dubbo 负载均衡原理)
Zookeeper 实现消息中间件集群管理 -
Zookeeper 数据结构: 整体可以看为一棵树,每个节点称为一个ZNode, 每个ZNode上面默认存储1M数据,通过节点路径进行唯一标识
-
Zookeeper 可以简单理解为文件系统+通知工具,从设计模式来讲基于观察者模式的分布式管理框架,负责存储和管理数据,接受观察者的注册,当被Zookeeper存储管理的数据发生改变时,负责通知已经注册到Zookeeper上的观察者作出相应的处理,
-
以Zookeeper作为dubbo注册中心来讲,当服务提供方启动时,会以自己全路径名为持久节点,分布式环境下每个该服务的ip为该持久节点下的临时节点存储到zookeeper上,服务消费方在第一次调用时,会通过需要调用的服务生提供者的全类名,在Zookeeper上寻找到对应的持久节点,进而获取到了相应的服务的IP地址列表,缓存在本地,然后通过本地进行负载均衡选在调用指定的服务
-
Zookeeper会对服务提供方存储的节点数据进行监听,当发生改变,或有监听的服务宕机下线时,通过一个Listener监听线程调用Watcher中的process()方法进行指定的操作,例如调用服务消费方更新拿到的服务提供方数据
二. ZooKeeper Windows 单机版安装
- Zookeeper下载地址 : https://zookeeper.apache.org/
- 将下载后的压缩包解压到非中文路径下
- 在解压后的文件夹中创建data文件夹,与log文件夹(data用来存储数据,log用来记录日志)
- 找到cnf文件夹,复制zoo_sample.cfg副本,重命名为zoo.cfg
- 打开zoo.cfg文件,修改 dataDir值指向新创建的data文件夹路径,添加dataLogDir值为新创建的log文件夹路径,注意"/"
- 查看zoo.cfg文件中的 clientPort 端口号是否正确,是否被占用
- (集群环境下zoo.cfg文件中添加多个zookeeper server 通信ip+端口号,并且在每个data文件夹中创建myid.txt文件,去掉后缀,编辑,第一台输入1, 第二台输入2…)
- 启动 找到bin文件夹,点击zkServer.Cmd 文件
三. ZooKeeper 集群环境下选举过程
- Zookeeper 集群环境下半数以上存活则可正常选举,正常运行(所以推荐奇数)
- 在集群环境中启动Zookeeper时,每一台机器都会尝试找到Leader,并以自己为Leader进入选举状态,进行投票,每个服务器会传递一个包含锁选举的服务器的myid与ZXID,通过这两个数据判断选择为leader的服务器是哪一台,
- 集群的每一条服务器接收到来自各个服务器的投票,首先判断该投票是否有效,是否是本轮投票,是否来自LOOKING状态的服务器
- 每一台服务器将其它服务器的投票与自己的进行比较,ZXID比较大的服务器优先作为Leader,如果ZXID相同,则比较myid较大的服务器作为Leader服务器
- 服务器统计投票信息,判断是否已经有过半机器接受到相同的投票信息,当统计出集群中已经有半数以上的同意,此时便认为已经选出了Leader
- 确定了leader后,其它服务器会改变自身状态,除了选举出来的leader,其它都变为follwer
- 当Leader节点宕机,会进入新一轮Leader选举,首先整个Zookeeper集群将暂停对外服务,其它存活的follwer节点会变更状态为LOOKING,进入选举
四. ZooKeeper 存储数据的过程
当向Zookeeper中注册存储数据时,客户端发送一个"写"请求,而Zookeeper集群时分为Leader与Follwer,假设接收请求的是Follwer zk1,当前节点会将请求转发给Leader,Leader再将这个"写"请求广播给所有的Follwer,所有Follewer都会接收请求执行"写"存储数据的操作(各个Zookeeper上存储了相同的数据),执行成功后通知Leader,当Leader接收到Follwer写成功的结果超过半数以上则说明写成功了,在将请求结果发送给zk1
五. ZooKeeper 监听
Zookeeper内部会创建创建启动两个线程,一个正常的业务逻辑线程Connect线程,出了可以实现对数据的增删改查以外,还可以设置监听,还有一个专门用来监听的线程Listener,该线程运行监控设置开启监听的数据,当发生监听行为时,会自动调用Zookeeper 中持有的Watcher中的process()方法,该方法中可以拿到监听的事件WatchedEvent对象,通过该对象可以获取到当前发生的是什么类型的事件,事件状态等
六. java 操作 ZooKeeper
- ZooKeeper API 方法: ZooKeeper API文档
- java 中操作 Zookeeper 需要引入依赖,创建Zookeeper 连接对象,通过连接对象实现对节点数据的增删改查,事件通知等
<!--zookeeper依赖-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.13</version>
</dependency>
<!--管理zookeeper需要用到的-->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.0.1</version>
</dependency>
基本用法
- ZooKeeper 连接的创建
- 创建ZooKeeper对象时传递的Watcher中的process()通知方法
- 设置监听指定节点是否存活
- 设置监听指定节点下的数据变化
- 创建节点(注意持久节点,与临时节点,重名节点与上层节点不存在的问题)
- 获取指定节点的数据
- 关闭ZooKeeper连接
public class Test {
//信号量,阻塞程序执行,用户等待zookeeper连接成功,发送成功信号
//设置信号量为1,通过信号量来控制代码执行顺序
//当zookeeper连接获取成功事件监听成功时 CountDownLatch 调用 countDown()
//对信号量做累减操作,在后续Zookeeper创建节点时首先通过CountDownLatch 调用
//await() 判断如果不为0,则阻塞等待,为0才继续向下执行
private static final CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) throws IOException, KeeperException, InterruptedException {
ZooKeeper zooKeeper = null;
try{
//1.创建Zookeeper 连接
//参数一: Zookeeper 服务器连接地址
//参数二: 连接超时时间
//参数三: Watcher,通过Watcher监听节点数据
zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new Watcher() {
//2.当被设置开启监听是否存在的的节点消失或启动时
//当被设置开启监听的节点数据发生改变时,都会执行该方法
//可以在该方法中自定义被监听者发生变化时需要完成的功能,例如通知消费方重新获取节点上的数据
@Override
public void process(WatchedEvent watchedEvent) {
// 获取事件状态
Event.KeeperState keeperState = watchedEvent.getState();
// 获取事件类型
Event.EventType eventType = watchedEvent.getType();
if (Event.KeeperState.SyncConnected == keeperState) {
if (Event.EventType.None == eventType) {
//信号量累减操作(1-1变为0)
countDownLatch.countDown();
System.out.println("zk 启动连接...");
}
}
//==========可以编写触发监听执行时的业务代码(下方只是模拟可能会报错)===========
children = zooKeeper .getChildren("/", true);
for (String child : children) {
System.out.println(child);
}
//==================模拟触发监听执行的逻辑end==========================
}
});
//信号量判断是否为0,不为0,说明Zookeeper连接未创建成功则阻塞等待
countDownLatch.await();
String node = "/test";
//3.获取并设置监听node节点是否存在 true为设置开启监听
Stat stat = zooKeeper.exists(node,true);
//4.获取node节点下的所有子节点,并设置开启监听node节点下的变化
List<String> childrenList = zooKeeper.getChildren(node, true);
//5.创建节点数据
//参数一: 创建节点名称(注意点:假设当天添加"/aaa/test"节点,
// 但是"/test'上层的父节点不存在,则会报错)
//参数二: 存放的数据
//参数三: 设置当前存放的数据的允许访问权限Ids.OPEN_ACL_UNSAFE 为开放
//参数四: 当前创建的节点类型 CreateMode
//节点类型: CreateMode 常量值
//EPHEMERAL 临时节点 (注意点:临时节点在连接断开时会自动消失)
//EPHEMERAL_SEQUENTIAL 临时节点,
// 与EPHEMERAL的不同之处在于,假设当
// 前以及存储了一个"/test"临时节点数据,
// 后续再去存储"/test"向同的临时节点,
// 该类型情况下会自动使用id自增去设置唯一性
// 第二个"/test"实际存储的就可能是"/test0001"
//PERSISTENT 持久节点,持久节点会永久的保存在硬盘上
// PERSISTENT_SEQUENTIAL 持久节点,与上面一样,会使用id进行唯一设置
String nodeResult = zooKeeper.create(node+"/test", "测试数据".getBytes(),
ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
//6.获取指定节点的数据
byte[] resultByte = zooKeeper.getData(node+"/test", true, null);
}catch (Exception e){
e.printStackTrace();
}finally {
//7.关闭连接
if(null != zooKeeper){
zooKeeper.close();
}
}
}
}
模拟服务节点动态上下线,动态通知消费服务
1. 分析
- 服务提供方启动时,使用一个可以标识当前服务的变量,存储到ZooKeeper上,以dubbo为例: dubbo/全类名/providers/实际ip, dubbo/全类名/providers为持久节点,实际ip为临时节点
- 开启监听,监听临时节点是否存在(临时节点是跟随连接的,连接断开临时节点消失,也就是服务提供方宕机下线了),监听指定节点数据变化以dubbo为例,监听"dubbo/全类名/providers"下的所有数据,
- 服务消费方启动时,将消费方存储到Zookeeper上,以dubbo为例,服务消费方中要调用服务提供方的API,存储的是"dubbo/调用的服务提供方的全类名/customers/实际ip"
- ZooKeeper 的 Watcher接口中的回调方法process()中,编写当监控的服务提供方发生改变,上下线时,需要执行的逻辑代码,例如获取Zookeeper上的服务消费方,进行更新等
2. 创建服务提供方
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
public class ServerTest {
//main方法执行
public static void main(String[] args) throws Exception {
//1.获取ZooKeeper连接
ZooKeeper zooKeeper = getConnect();
//2.创建服务提供方
ServerTest server = new ServerTest(zooKeeper, "/register.server", "/provider", "1.1.1.1");
//3.向ZooKeeper上注册,也就是存储能代表当前服务的数据
//服务消费方启动后获取这些数据进行调用
server.registerServer();
//4.模拟程序运行(防止程序运行完毕连接断开后临时节点消失)
server.serverRun();
}
//ZooKeeper 连接
public ZooKeeper zooKeeper;
//当前服务提供方的全类名
public String classPathName;
//服务提供方节点
public String provider;
//当前服务提供方IP,ZooKeeper上存储的真实数据
//服务消费方获取这个ip调用到指定的服务器
public String ip;
public ServerTest(ZooKeeper zooKeeper, String classPathName, String provider, String ip) {
this.zooKeeper = zooKeeper;
this.classPathName = classPathName;
this.provider = provider;
this.ip = ip;
}
//模拟向ZooKeeper注册服务提供方
public void registerServer() throws Exception {
//1.判断当前是否存在classPathName节点,如果不存在再创建,防止同名节点报错
Stat stat = zooKeeper.exists(classPathName, false);
if (null == stat) {
//2.第一层节点创建为公开的,持久节点例如dubbo中的全类名
zooKeeper.create(classPathName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
//3.向节点中存储当前服务提供方的实际Ip,为临时节点,原因是临时节点会跟随连接消失,如果消失了说明宕机了
//可以通过监听做到动态通知
zooKeeper.create(classPathName + provider, ip.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
}
//模拟服务运行
public void serverRun() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
//获取Zookeeper连接
public static ZooKeeper getConnect() throws IOException {
ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 10000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// 获取事件状态
Event.KeeperState keeperState = watchedEvent.getState();
// 获取事件类型
Event.EventType eventType = watchedEvent.getType();
if (Event.KeeperState.SyncConnected == keeperState) {
if (Event.EventType.None == eventType) {
System.out.println("zk 启动连接...");
}
//节点不存在事件
if (Event.EventType.NodeDataChanged == eventType) {
System.out.println("zk 节点不存在事件...");
}
//节点数据修改事件
if (Event.EventType.NodeChildrenChanged == eventType) {
System.out.println("zk 节点数据修改事件...");
}
}
}
});
return zooKeeper;
}
}
3. 创建服务消费方
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ClientTest {
//main方法运行
public static void main(String[] args) throws Exception {
//1.获取连接
ZooKeeper zooKeeper = getConnect();
//2.创建服务消费方
ClientTest client = new ClientTest(zooKeeper, "/register.server", "/customer", "/provider", "2.2.2.2");
//3.注册服务消费方
client.registerClient();
//4.开启监听
client.monitor();
//5.输出消费方获取的服务方ip
client.infoId();
client.clientRun();
}
//ZooKeeper 连接
public ZooKeeper zooKeeper;
//当前服务消费方需要调用非服务提供方的全类名(消费方持有提供方)
public String classPathName;
//服务消费方节点
public String customer;
//服务消费方ip,当监听到数据有变化时,获取该Ip将变化后的数据发送给指定的消费方
public String ip;
//服务提供方节点路径,消费方与对应的提供方都是存储在同一个全类名下
//在监听时只要监听提供方的即可
public String provider;
//获取服务提供方ip存储到该容器中
public List<String> serverIpList = new ArrayList<>();
public ClientTest(ZooKeeper zooKeeper, String classPathName, String customer, String provider, String ip) {
this.zooKeeper = zooKeeper;
this.classPathName = classPathName;
this.customer = customer;
this.provider = provider;
this.ip = ip;
}
//模拟向ZooKeeper中注册服务消费方
public void registerClient() throws Exception {
Stat stat = zooKeeper.exists(classPathName, false);
if (null == stat) {
zooKeeper.create(classPathName, null, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
}
zooKeeper.create(classPathName + customer, ip.getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
}
//设置开启监听
public void monitor() throws Exception {
//1.设置监听"classPathName+provider" 节点是否存在
zooKeeper.exists(classPathName + provider, true);
//2.设置监听"classPathName+provider"节点的数据变化,并获取节点中的数据,也就是服务提供方的真实Ip
List<String> childrenList = zooKeeper.getChildren(classPathName, true);
//3.将获取到的服务提供方ip存储给服务消费方
for (String children : childrenList) {
if (provider.equals("/" + children)) {
byte[] result = zooKeeper.getData(classPathName + provider, true, null);
serverIpList.add(new String(result));
}
}
}
//模拟服务运行
public void clientRun() throws InterruptedException {
Thread.sleep(Long.MAX_VALUE);
}
public void infoId() {
serverIpList.stream().forEach(sIp -> System.out.println("获取到的服务提供方ip为:" + sIp));
}
//获取Zookeeper连接
public static ZooKeeper getConnect() throws IOException {
ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 10000, new Watcher() {
@Override
public void process(WatchedEvent watchedEvent) {
// 获取事件状态
Event.KeeperState keeperState = watchedEvent.getState();
// 获取事件类型
Event.EventType eventType = watchedEvent.getType();
if (Event.KeeperState.SyncConnected == keeperState) {
//启动事件
if (Event.EventType.None == eventType) {
System.out.println("zk 启动连接...");
}
//删除节点事件
if (Event.EventType.NodeDeleted == eventType) {
System.out.println("zk 节点不存在事件...");
}
//节点数据修改事件
if (Event.EventType.NodeChildrenChanged == eventType) {
System.out.println("zk 节点数据修改事件...");
}
}
}
});
return zooKeeper;
}
}