1、Zookeeper简介
1.1 分布式系统定义
- 分布式系统是同时跨越多个物理主机,独立的多个软件所组成系统。
- 类比一下,分布式系统就是一群人一起干活
- 每个服务器的算力是有限的,但是通过分布式系统,由n个服务器组成起来的集群,算例是可以无限扩张的
1.2 分布式系统面临的问题
- 通信异常
- 网络分区
- 节点故障
- 三态
1.3 ZooKeeper如何解决分布式系统面临的问题
- 通过共享存储
- 通过ZooKeeper实现分布式协同的原理,ZooKeeper存储了任务的分配、完成情况等共享信息。
- 每个分布式引用的节点就是组员,订阅这些共享信息。
- 当主节点(组leader),对某个从节点的分工信息作出改变时,相关订阅的从节点得到zookeeper的通知,取得自己最新的任务分配
- 完成工作后,把完成情况存储到zookeeper。
- 主节点订阅了该任务的完成情况信息,所以将得到zookeeper的完成的通知
1.4 ZooKeeper的基本概念
- Zookeeper是一个开源的分布式协调服务
- 其设计目标是将那些复杂的且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一些简单的接口提供给用户使用
- zookeeper是一个典型的分布式数据一致性的解决方案,分布式应用程序可以基于它实现诸如数据订阅/发布、负载均衡、命名服务、集群管理、分布式锁和分布式队列等功能
1.4.1 集群角色
- 在Zookeeper中,有Leader、Follower、Observer三种角色
- Zookeeper集群中的所有机器通过Leader选举来选定一台被称为Leader的机器,Leader服务器为客户端提供读和写服务
- 除Leader外,其他机器包括Follower和Observer都能提供读服务,唯一的区别在于Observer不参与Leader选举过程,不参与写操作的过半写成功策略,因此Observer可以在不影响写性能的情况下提升集群的性能
1.4.2 会话(session)
- Session指客户端会话,一个客户端连接是指客户端和服务端之间的一个TCP长连接
- Zookeeper对外的服务端口默认为2181,客户端启动的时候,首先会与服务器建立一个TCP连接,从第一次连接建立开始,客户端会话的生命周期也开始了
- 通过这个连接,客户端能够心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来自服务器的Watch事件通知
1.4.3 数据节点(Znode)
- 在ZooKeeper中,"节点"分为两类,第一类同样是指构成集群的机器,我们称之为机器节点;第二类则是指数据模型中的数据单元,我们称之为数据节点——ZNode
- ZooKeeper将所有数据存储在内存中,数据模型是一棵树(ZNode Tree),由斜杠(/)进行分割的路径,就是一个Znode
- 例如/app/path1。每个ZNode上都会保存自己的数据内容,同时还会保存一系列属性信息
1.4.4 版本
- Zookeeper的每个Znode上都会存储数据,对于每个ZNode,Zookeeper都会为其维护一个叫作Stat的数据结构
- Stat记录了这个ZNode的三个数据版本,分别是:version(当前ZNode的版本)、cversion(当前ZNode子节点的版本)、aversion(当前ZNode的ACL版本)
1.4.5 Watcher(事件监听器)
- Wathcer(事件监听器),是Zookeeper中一个很重要的特性
- Zookeeper允许用户在指定节点上注册一些Watcher
- 在一些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端
- 该机制是Zookeeper实现分布式协调服务的重要特性
1.4.6 ACL
Zookeeper采用ACL(Access Control Lists)策略来进行权限控制,其定义了如下五种权限:
- CREATE:创建子节点的权限
- READ:获取节点数据和子节点列表的权限
- WRITE:更新节点数据的权限
- DELETE:删除子节点的权限
- ADMIN:设置节点ACL的权限
2、Zookeeper环境搭建
2.1 Zookeeper的搭建方式
Zookeeper安装方式有三种,单机模式和集群模式以及伪集群模式
- 单机模式:Zookeeper只运行在一台服务器上,适合测试环境
- 集群模式:Zookeeper运行于一个集群上,适合生产环境,这个计算机集群被称为一个"集合体"
- 伪集群模式:就是在一台服务器上运行多个Zookeeper实例
2.2 单机模式搭建
- 下载:http://archive.apache.org/dist/zookeeper/(以:zookeeper-3.4.14为例子)
- 上传:下载完成后,将zookeeper压缩包zookeeper-3.4.14.tar.gz上传到linux系统
- 解压缩压缩包 命令:
tar -zxvf zookeeper-3.4.14.tar.gz
- 进入zookeeper-3.4.14目录,命令:
cd zookeeper-3.4.14
- 创建data文件夹,命令:
mkdir data
- 修改配置文件名称,命令:
mv zoo_sample.cfg zoo.cfg
- 修改zoo.cfg中的data属性:
dataDir=/root/zookeeper-3.4.14/data
- 进入bin目录,启动服务输,命令:
./zkServer.sh start
显示如下信息数目启动成功:
10. 停止服务命令:./zkServer.sh stop
11. 查看服务状态命令:./zkServer.sh status
3.2 集群模式
- 创建目录zkcluster,命令:
mkdir zkcluster
- 解压zookeeper-3.4.14.tar.gz到zkcluster目录下,命令:
tar -zxvf zookeeper-3.4.14.tar.gz -C /zkcluster
- 修改名字,命令:
mv zookeeper-3.4.14 zookeeper01
- 复制并改名,命令:
cp -r zookeeper01/ zookeeper02
cp -r zookeeper01/ zookeeper03
- 分别在zookeeper01、zookeeper02、zookeeper03目录下创建data及logs目录
mkdir data
cd data
mkdir logs
- 修改文件名称
cd conf
mv zoo_sample.cfg zoo.cfg
- 配置每一个Zookeeper 的dataDir(zoo.cfg) clientPort分别为2181 2182 2183
clientPort=2181
dataDir=/root/zkcluster/zookeeper01/data
dataLogDir=/root/zkcluster/zookeeper01/data/logs
clientPort=2182
dataDir=/root/zkcluster/zookeeper02/data
dataLogDir=/root/zkcluster/zookeeper02/data/logs
clientPort=2183
dataDir=/root/zkcluster/zookeeper03/data
dataLogDir=/root/zkcluster/zookeeper03/data/logs
- 在每个zookeeper的 data目录下创建一个myid文件,内容分别是1、2、3 。这个文件就是记录每个服务器的ID
touch myid
- 在每一个zookeeper的zoo.cfg配置客户端访问端口(clientPort)和集群服务器IP列表
#server.服务器ID=服务器IP地址:服务器之间通信端口:服务器之间投票选举端口
server.1=ip:2881:3881
server.2=ip:2882:3882
server.3=ip:2883:3883
- 依次启动三个zk实例
3、Zookeeper基本使用
3.1 ZooKeeper系统模型
3.1.1 数据模型Znode
- 在ZooKeeper中数据信息被保存在一个个数据节点上,这些节点被称为znode
- ZNode是Zookeeper中小数据单位,在ZNode下面又可以再挂ZNode,这样一层层下去就形成了一个层次化命名空间ZNode树,我们称为 ZNode Tree
- Znode Tree采用了类似文件系统的层级树状结构进行管理
3.1.2 ZNode的类型
分为三大类
- 持久性节点(Persistent)
- 临时性节点(Ephemeral)
- 顺序性节点(Sequential)
创建节点的时候通过组合可以生成以下四种节点类型
- 持久节点:是Zookeeper中最常见的一种节点类型,所谓持久节点,就是指节点被创建后会一直存在服务器,直到删除操作主动清除
- 持久顺序节点:就是有顺序的持久节点,节点特性和持久节点是一样的,只是额外特性表现在顺序上。顺序特性实质是在创建节点的时候,会在节点名后面加上一个数字后缀,来表示其顺序
- 临时节点:就是会被自动清理掉的节点,它的生命周期和客户端会话绑在一起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建子节点
- 临时顺序节点:就是有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后面加上数字后缀
3.1.3 ZNode的内容
整个ZNode节点内容包括两部分:节点数据内容和节点状态信息,如下所示:
## 以下是节点数据信息
[quota]
## 以下是节点状态信息
## 就是Create ZXID,表示节点被创建时的事务ID
cZxid = 0x0
## 就是Create Time,表示节点创建时间
ctime = Thu Jan 01 08:00:00 CST 1970
## 就是Modified ZXID,表示节点最后一次被修改时的事务ID
mZxid = 0x0
## 就是Modified Time,表示节点最后一次被修改的时间
mtime = Thu Jan 01 08:00:00 CST 1970
## 表示该节点的子节点列表最后一次被修改时的事务ID。只有子节点列表变更才会更新 Zxid,子节点内容变更不会更新
pZxid = 0x0
## 表示子节点的版本号
cversion = -1
## 表示内容版本号
dataVersion = 0
## 标识acl版本
aclVersion = 0
## 表示创建该临时节点时的会话sessionID,如果是持久性节点那么值为0
ephemeralOwner = 0x0
## 表示数据长度
dataLength = 0
## 表示直系子节点数
numChildren = 1
3.1.4 Watcher(数据变更通知)
Watcher介绍
- Zookeeper使用Watcher机制实现分布式数据的发布/订阅功能
- 一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能够让多个订阅者同时监听某一个主题对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们能够做出相应的处理
- ZooKeeper中,引入了Watcher机制来实现这种分布式的通知功能
- ZooKeeper允许客户端向服务端注册一个Watcher监听,当服务端的一些指定事件触发了这个Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能
Zookeeper的Watcher机制的组成
- 客户端线程
- 客户端WatcherManager
- Zookeeper服务器
Watcher的工作流程 - 客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中
- 当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑
3.1.5 ACL(保障数据的安全)
可以从三个方面来理解ACL机制:权限模式(Scheme)、授权对象(ID)、权限(Permission),通常使用"scheme: id : permission"来标识一个有效的ACL信息
3.1.5.1 权限模式:Scheme
⽤来确定权限验证过程中使⽤的检验策略,有如下四种模式
-
IP
IP模式就是通过IP地址粒度来进行权限控制,如"ip:192.168.0.110"表示权限控制针对该IP地址
同时IP模式可以支持按照网段方式进行配置,如"ip:192.168.0.1/24"表示针对192.168.0.*这个网段进行权限控制 -
Digest
Digest是最常用的权限控制模式,要更符合我们对权限控制的认识
使用"username:password"形式的权限标识来进行权限配置,便于区分不同应用来进行权限控制
使用"username:password"形式配置了权限标识后,Zookeeper会先后对其进行SHA-1加密和BASE64编码 -
World
World是一种最开放的权限控制模式,这种权限控制方式几乎没有任何作用,数据节点的访问权限对所有用户开放
即所有用户都可以在不进行任何权限校验的情况下操作ZooKeeper上的数据
World模式也可以看作是一种特殊的Digest模式,它只有一个权限标识,即"world:anyone" -
Super
Super模式是一种特殊的Digest模式
在Super模式下,超级用户可以对任意ZooKeeper上的数据节点进行任何操作
3.1.5.2 授权对象:ID
授权模式 | 授权对象 |
---|---|
IP | 通常是一个IP地址或IP段:例如:192.168.10.110 或192.168.10.1/24 |
Digest | 自定义,通常是username:BASE64(SHA-1(username:password))例如:zm:sdfndsllndlksfn7c= |
Digest | 只有一个ID :anyone |
Super | 超级用户 |
3.1.5.2 权限:Permission
- CREATE:创建子节点的权限
- READ:获取节点数据和子节点列表的权限
- WRITE:更新节点数据的权限
- DELETE:删除子节点的权限
- ADMIN:设置节点ACL的权限
注:CREATE和DELETE这两种权限都是针对子节点的权限控制
3.2 ZooKeeper命令行操作
3.2.1 创建节点
使用create命令,可以创建一个Zookeeper节点,命令如下
create [-s][-e] path data acl
其中,-s指顺序节点,-e指临时节点,若不指定,则创建持久节点;acl⽤来进⾏权限控制。
- 创建顺序节点
在根节点下创建了一个叫做/zk-test的节点,该节点内容就是123,同时可以看到创建的zk-test节点后面添加了一串数字以示区别
create -s /zk-test 123
- 创建临时节点
临时节点在客户端会话结束后,就会⾃动删除
create -e /zk-temp 123
- 创建永久节点
永久节点不同于顺序节点,不会自动在后面添加一串数字
create /zk-permanent 123
3.2.2 读取节点
- ls 命令
ls命令可以列出Zookeeper指定节点下的所有子节点,但只能查看指定节点下的第一级的所有子节点
ls path
// 其中,path表示的是指定数据节点的节点路径
- get 命令
get命令可以获取Zookeeper指定节点的数据内容和属性信息
get path
// 其中,path表示的是指定数据节点的节点路径
3.2.3 更新节点
使用set命令,可以更新指定节点的数据内容
set path data [version]
// data就是要更新的新内容
// version表示数据版本
3.2.4 删除节点
使用delete命令可以删除Zookeeper上的指定节点
delete path [version]
// version表示数据版本
3.3 Zookeeper的api使用
3.3.1 简介
- Zookeeper作为一个分布式框架,主要用来解决分布式一致性问题,它提供了简单的分布式原语,并且对多种编程语言提供了API
- Zookeeper API共包含五个包
(1)org.apache.zookeeper
(2)org.apache.zookeeper.data
(3)org.apache.zookeeper.server
(4)org.apache.zookeeper.server.quorum
(5)org.apache.zookeeper.server.upgrade
3.3.2 api使用
导入jar包依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
</dependency>
- 创建会话,
CreateSession.java
注意
1)ZooKeeper 客户端和服务端会话的建立是一个异步的过程,也就是说在程序中,构造方法会在处理完客户端初始化操作后立即返回
2)在大多数情况下,此时并没有真正建立好一个可用的会话,在会话的生命周期中处于"CONNECTING"的状态
3)当该会话真正创建完毕后ZooKeeper服务端会向会话对应的客户端发送一个事件通知,以告知客户端,客户端只有在获取这个通知之后,才算真正建立了会话
package com.yuyz.api;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.util.concurrent.CountDownLatch;
public class CreateSession implements Watcher {
// 连接的zk的地址及端口号配置
private static String connectString =
"ip1:2181,ip2:2182,ip3:2183";
// countDownLatch这个类使⼀个线程等待,主要不让main⽅法结束
private static CountDownLatch countDownLatch = new CountDownLatch(1);
public static void main(String[] args) throws Exception {
/*
客户端可以通过创建⼀个zk实例来连接zk服务器
new Zookeeper(connectString,sesssionTimeOut,Wather)
connectString: 连接地址:IP:端⼝
sesssionTimeOut:会话超时时间:单位毫秒
Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端)
*/
ZooKeeper zooKeeper = new ZooKeeper(connectString, 5000, new CreateSession());
System.out.println(zooKeeper.getState());
countDownLatch.await();
// 表示会话真正建⽴
System.out.println("=========Client Connected tozookeeper==========");
}
public void process(WatchedEvent watchedEvent) {
// 当连接创建了,服务端发送给客户端SyncConnected事件
if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
countDownLatch.countDown();
}
}
}
- 创建节点,
CreateNode .java
package com.yuyz.api;
import org.apache.zookeeper.CreateMode;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooDefs;
import org.apache.zookeeper.ZooKeeper;
public class CreateNode implements Watcher {
// 连接的zk的地址及端口号配置
private static String connectString =
"ip1:2181,ip2:2182,ip3:2183";
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper(connectString, 5000, new CreateNode());
System.out.println(zooKeeper.getState());
Thread.sleep(Integer.MAX_VALUE);
}
public void process(WatchedEvent watchedEvent) {
// 当连接创建了,服务端发送给客户端SyncConnected事件
if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
System.out.println("服务端发送给客户端SyncConnected事件");
}
// 创建节点
try {
System.out.println("创建节点开始");
createNodeSync();
System.out.println("创建节点完成");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void createNodeSync() throws Exception {
// 创建持久节点
String path_persistent =
zooKeeper.create(
"/node_persistent",
"持久节点".getBytes("utf-8"),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT);
System.out.println("持久节点:" + path_persistent);
String path_ephemeral =
zooKeeper.create(
"/node_ephemeral",
"创建临时节点".getBytes("utf-8"),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.EPHEMERAL);
System.out.println("临时节点:" + path_ephemeral);
// 创建持久顺序节点
String path_persistent_sequential =
zooKeeper.create(
"/node_persistent_sequential",
"持久顺序节点".getBytes("utf-8"),
ZooDefs.Ids.OPEN_ACL_UNSAFE,
CreateMode.PERSISTENT_SEQUENTIAL);
System.out.println("持久顺序:" + path_persistent_sequential);
}
}
- 获取节点数据,
GetNodeDate .java
package com.yuyz.api;
import org.apache.zookeeper.KeeperException;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import java.util.List;
public class GetNodeDate implements Watcher {
// 连接的zk的地址及端口号配置
private static String connectString =
"ip1:2181,ip2:2182,ip3:2183";
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
/*
客户端可以通过创建⼀个zk实例来连接zk服务器
new Zookeeper(connectString,sesssionTimeOut,Wather)
connectString: 连接地址:IP:端⼝
sesssionTimeOut:会话超时时间:单位毫秒
Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端)
*/
zooKeeper = new ZooKeeper(connectString, 5000, new GetNodeDate());
System.out.println(zooKeeper.getState());
Thread.sleep(Integer.MAX_VALUE);
}
public void process(WatchedEvent watchedEvent) {
// 当连接创建了,服务端发送给客户端SyncConnected事件
if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
System.out.println("服务端发送给客户端SyncConnected事件");
}
try {
// 查询节点信息
getNode();
// 查询子节点列表
getChildren();
} catch (Exception e) {
e.printStackTrace();
}
}
private void getChildren() throws KeeperException, InterruptedException {
List<String> children = zooKeeper.getChildren("/node1", true);
System.out.println("查询子节点列表");
for (String child : children) {
System.out.println(child);
}
}
private void getNode() throws KeeperException, InterruptedException {
byte[] data = zooKeeper.getData("/node_persistent", true, null);
System.out.println("查询节点信息:" + new String(data));
}
}
- 修改节点数据,
UpdateNode .java
package com.yuyz.api;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
public class UpdateNode implements Watcher {
// 连接的zk的地址及端口号配置
private static String connectString =
"ip1:2181,ip2:2182,ip3:2183";
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper(connectString, 5000, new UpdateNode());
System.out.println(zooKeeper.getState());
Thread.sleep(Integer.MAX_VALUE);
}
public void process(WatchedEvent watchedEvent) {
// 当连接创建了,服务端发送给客户端SyncConnected事件
if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
System.out.println("服务端发送给客户端SyncConnected事件");
}
// 创建节点
try {
System.out.println("修改节点开始");
updateNodeSync();
System.out.println("修改节点完成");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void updateNodeSync() throws Exception {
byte[] data = zooKeeper.getData("/node_persistent", false, null);
System.out.println("修改前节点信息:" + new String(data));
Stat stat = zooKeeper.setData("/node_persistent", "持久节点_update".getBytes("utf-8"), -1);
byte[] data1 = zooKeeper.getData("/node_persistent", false, null);
System.out.println("修改后节点信息:" + new String(data1));
}
}
- 删除节点,
DeleteNode .java
package com.yuyz.api;
import org.apache.zookeeper.WatchedEvent;
import org.apache.zookeeper.Watcher;
import org.apache.zookeeper.ZooKeeper;
import org.apache.zookeeper.data.Stat;
public class DeleteNode implements Watcher {
// 连接的zk的地址及端口号配置
private static String connectString =
"ip1:2181,ip2:2182,ip3:2183";
private static ZooKeeper zooKeeper;
public static void main(String[] args) throws Exception {
zooKeeper = new ZooKeeper(connectString, 5000, new DeleteNode());
System.out.println(zooKeeper.getState());
Thread.sleep(Integer.MAX_VALUE);
}
public void process(WatchedEvent watchedEvent) {
// 当连接创建了,服务端发送给客户端SyncConnected事件
if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
System.out.println("服务端发送给客户端SyncConnected事件");
}
// 创建节点
try {
System.out.println("删除节点开始");
deleteNode();
System.out.println("删除节点完成");
} catch (Exception e) {
e.printStackTrace();
}
}
private static void deleteNode() throws Exception {
Stat exists = zooKeeper.exists("/node_persistent", false);
if (exists == null) {
System.out.println("节点/node_persistent不存在!");
} else {
System.out.println("节点/node_persistent存在!");
zooKeeper.delete("/node_persistent", -1);
}
Stat exists2 = zooKeeper.exists("/node_persistent", false);
if (exists2 == null) {
System.out.println("节点/node_persistent不存在!");
} else {
System.out.println("节点/node_persistent存在!");
}
}
}
3.4 Zookeeper的开源客户端
3.4.1 ZkClient客户端
ZkClient是Github上一个开源的zookeeper客户端,在Zookeeper原生API接口之上进行了包装,是一个更易用的Zookeeper客户端,同时,zkClient在内部还实现了诸如Session超时重连、Watcher反复注册等功能
导入jar包依赖
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.2</version>
</dependency>
API说明
- 创建会话
ZkClient zkClient = new ZkClient("ip:2181");
- 创建节点
通过ZkClient通过设置createParents参数为true可以递归的先创建父节点,再创建子节点
//createParents的值设置为true,可以递归创建节点
zkClient.createPersistent(path,true);
- 删除节点
ZkClient提供了递归删除节点的接口,先删除所有子节点(存在),再删除父节点
zkClient.deleteRecursive(path);
- 获取子节点
List<String> children = zkClient.getChildren(path);
- 更新内容
zkClient.writeData(path,"4567")
- 获取内容
Object obj = zkClient.readData(path)
- 监听
// 设置子节点发生变化监听(当前监听的节点创建、删除或者子节点列表发生改变时通知)
zkClient.subscribeChildChanges(
node_path,
new IZkChildListener() {
public void handleChildChange(String path, List<String> list) throws Exception {
System.out.println(path + "节点的子节点列表发生变化,变化后为:" + list);
}
});
// 设置节点发生变化监听(当前节点内容发生改变、被删除会通知)
zkClient.subscribeDataChanges(
node_path,
new IZkDataListener() {
public void handleDataChange(String path, Object o) throws Exception {
System.out.println(path + "节点内容发生变化,变化后内容:" + o);
}
public void handleDataDeleted(String path) throws Exception {
System.out.println(path + "节点被删除");
}
});
使用ZkClient客户端的demo
package com.yuyz.zkclient;
import org.I0Itec.zkclient.IZkChildListener;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.ZkClient;
import java.util.List;
public class ZkClientDemo {
// 连接的zk的地址及端口号配置
private static String connectString =
"ip1:2181,ip2:2181,ip3:2181";
public static void main(String[] args) throws InterruptedException {
// 1、创建会话
ZkClient zkClient = new ZkClient(connectString);
System.out.println("zk会话创建完成");
String node_path = "/zkclient-node-01/c1";
// 设置子节点发生变化监听(当前监听的节点创建、删除或者子节点列表发生改变时通知)
zkClient.subscribeChildChanges(
node_path,
new IZkChildListener() {
public void handleChildChange(String path, List<String> list) throws Exception {
System.out.println(path + "节点的子节点列表发生变化,变化后为:" + list);
}
});
// 设置节点发生变化监听(当前节点内容发生改变、被删除会通知)
zkClient.subscribeDataChanges(
node_path,
new IZkDataListener() {
public void handleDataChange(String path, Object o) throws Exception {
System.out.println(path + "节点内容发生变化,变化后内容:" + o);
}
public void handleDataDeleted(String path) throws Exception {
System.out.println(path + "节点被删除");
}
});
Thread.sleep(1000);
// 2、创建节点(createParents为true,会递归创建节点)
zkClient.createPersistent("/zkclient-node-01/c1/c11", true);
System.out.println("递归创建节点完成");
Thread.sleep(1000);
// 3.修改节点信息
zkClient.writeData(node_path, "123");
System.out.println(node_path + "节点内容修改完成");
Thread.sleep(1000);
// 4. 递归删除(删除当前path节点下的所有子节点)
zkClient.deleteRecursive("/zkclient-node-01/c1");
System.out.println("递归删除完成");
Thread.sleep(1000);
}
}
3.4.2 Curator客户端
curator是Netflix公司开源的一套Zookeeper客户端框架,和ZKClient一样,Curator解决了很多Zookeeper客户端非常底层的细节开发操作,包括连接重连,反复注册Watcher和NodeExistsException异常等,是最流行的Zookeeper客户端之一。从编码风格上来讲,它提供了基于Fluent的编程风格支持
导入jar包依赖
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
API说明
- 创建会话
方式一:
Curator创建客户端是通过CuratorFrameworkFactory工厂类来实现
public static CuratorFramework newClient(String connectString, RetryPolicy retryPolicy)
public static CuratorFramework newClient(String connectString, int sessionTimeoutMs, int connectionTimeoutMs, RetryPolicy retryPolicy)
// 1、参数connectString:连接地址ip:port
// 2、参数RetryPolicy:提供重试策略的接口,可以让实现自定义的重试策略,默认提供了以下实现
// 1)ExponentialBackoffRetry(基于backoff的重连策略)
// 2)RetryNTimes(重连N次策略)
// 3)RetryForever(永远重试策略)
// 3、参数sessionTimeoutMs:会话超时时间
// 4、参数connectionTimeoutMs:连接超时时间
通过调用CuratorFramework中的start()方法来启动会话
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
CuratorFramework client =
CuratorFrameworkFactory.newClient("127.0.0.1:2181",retryPolicy);
client.start();
方式二
还可以通过Fluent风格实现
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("127.0.0.1:2181") //server地址
.sessionTimeoutMs(5000) // 会话超时时间
.connectionTimeoutMs(3000) // 连接超时时间
.retryPolicy(retryPolicy) // 重试策略
.namespace("base") // 独立命名空间/base
.build(); //
client.start();
- 创建节点
1)创建一个内容为空的节点
client.create().forPath(path);
2)创建⼀个包含内容的节点
client.create().forPath(path,"我是内容".getBytes());
3)递归创建父节点,并选择节点类型
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
- 获取数据
获取节点数据内容API相当简单,同时Curator提供了传入一个Stat变量的方式来存储服务器端返回的最新的节点状态信息
// 普通查询
client.getData().forPath(path);
// 包含状态查询
Stat stat = new Stat();
client.getData().storingStatIn(stat).forPath(path);
- 更新数据
更新数据,如果未传入version参数,那么更新当前最新版本,如果传入version则更新指定version,如果version已经变更,则抛出异常
// 普通更新
client.setData().forPath(path,"新内容".getBytes());
// 指定版本更新
client.setData().withVersion(1).forPath(path,"新内容".getBytes);
使用Curator客户端的demo
package com.yuyz.curator;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.apache.zookeeper.data.Stat;
public class CuratorDemo {
// 连接的zk的地址及端口号配置
private static String connectString =
"ip1:2181,ip2:2181,1p3:2181";
public static void main(String[] args) throws Exception {
// 1、创建连接
// 1.1 创建连接方式一
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
CuratorFramework curatorFramework =
CuratorFrameworkFactory.newClient(connectString, retryPolicy);
curatorFramework.start();
System.out.println("方式一创建连接成功");
// 1.2 创建连接方式二
CuratorFramework curatorFramework_2 =
CuratorFrameworkFactory.builder()
.connectString(connectString)
.connectionTimeoutMs(50000)
.sessionTimeoutMs(30000)
.retryPolicy(retryPolicy)
.build();
curatorFramework_2.start();
System.out.println("方式二创建连接成功");
// 2 创建节点
String path = "/node_curator";
String rsPath = curatorFramework.create().forPath(path, "新节点".getBytes("utf-8"));
System.out.println(rsPath + "节点创建完成");
// 3. 获取节点内容和状态
byte[] bytes = curatorFramework.getData().forPath(path);
Stat stat = new Stat();
curatorFramework.getData().storingStatIn(stat).forPath(path);
System.out.println(path + "节点内容为:" + new String(bytes));
System.out.println(path + "节点状态为:" + stat);
// 4 修改内容
curatorFramework.setData().forPath(path, "修改后内容".getBytes("utf-8"));
byte[] bytes1 = curatorFramework.getData().forPath(path);
Stat stat1 = new Stat();
curatorFramework.getData().storingStatIn(stat1).forPath(path);
System.out.println(path + "节点修改后内容为:" + new String(bytes1));
System.out.println(path + "节点修改后状态为:" + stat1);
// 5 删除节点
curatorFramework.delete().forPath(path);
System.out.println(path + "节点删除成功");
}
}
4、Zookeeper应用场景
- ZooKeeper是一个典型的发布/订阅模式的分布式数据管理与协调框架
- 可以使用它来进行分布式数据的发布与订阅
- 通过对ZooKeeper中丰富的数据节点类型进行交叉使用,配合Watcher事件通知机制,可以非常方便地构建一系列分布式应用中都会涉及的核心功能
- 如数据发布/订阅、命名服务、集群管理、Master选举、分布式锁和分布式队列等
4.1 数据发布/订阅
4.1.1 数据发布/订阅(Publish/Subscribe)系统
数据发布/订阅系统,即所谓的配置中心,顾名思义就是发布者将数据发布到ZooKeeper的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置信息的集中式管理和数据的动态更新
4.1.2 数据发布/订阅系统的模式
- 推(Push)模式
在推模式中,服务端主动将数据更新发送给所有订阅的客户端 - 拉(Pull)模式
拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方法
4.1.3 Zookeeper采用推拉结合的方式
- 客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知
- 客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据
4.1.4 案例
- 配置存储
在进行配置管理之前,首先我们需要将初始化配置信息存储到Zookeeper上去,一般情况下,我们可以在Zookeeper上选取一个数据节点用于配置信息的存储,例如:/app1/database_config
将需要管理的配置信息写入到该数据节点中去,例如:
#数据库配置信息
#DBCP
dbcp.driverClassName=com.mysql.jdbc.Driver
dbcp.dbJDBCUrl=jdbc:mysql://127.0.0.1:3306/test
dbcp.username=zm
dbcp.password=1234
dbcp.maxActive=30
dbcp.maxIdle=10
- 配置获取
- 集群中每台机器在启动初始化阶段,首先会从上ZooKeeper配置节点上读取数据库信息
- 同时,客户端还需要在该配置节点上注册一个数据变更的Watcher监听
- 一旦发生节点数据变更,所有订阅的客户端都能够获取到数据变更通知
- 配置变更
- 在系统运行过程中,可能会出现需要进行数据库切换的情况,这个时候就需要进行配置变更
- 只需要对ZooKeeper上配置节点的内容进行更新,ZooKeeper就能够帮我们将数据变更的通知发送到各个客户端
- 每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获取
4.2 命名服务
4.2.1 命名服务(Name Service)
- 命名服务是分布式系统中比较常见的一类场景,是分布式系统最基本的公共服务之
- 在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等——这些我们都可以统称它们为名字(Name)
- 常见的就是一些分布式服务框架(如RPC、RMI)中的服务地址列表,通过使用命名服务,客户端应用能够根据指定名字来获取资源的实体、服务地址和提供者的信息等
4.2.2 使用ZooKeeper实现分布式全局唯一ID的分配机制
-
调用ZooKeeper节点创建的API接口可以创建一个顺序节点,并且在API返回值中会返回这个节点的完整名字。利用这个特性,我们就可以借助ZooKeeper来完成全局唯一的ID了,如下图:
-
使⽤ZooKeeper⽣成唯⼀ID的基本步骤
1)所有客户端都会根据自己的任务类型,在指定类型的任务下面通过调用create()接口来创建一个顺序节点,例如创建"job-“节点
2)节点创建完毕后,create()接口会返回一个完整的节点名,例如"job-0000000003”
3)客户端拿到这个返回值后,拼接上type类型,例如"type2-job-0000000003",这就可以作为一个全局唯一的ID了
备注说明:
1)在ZooKeeper中,每一个数据节点都能够维护一份子节点的顺序顺列
2)当客户端对其创建一个顺序子节点的时候,ZooKeeper会自动以后缀的形式在其子节点上添加一个序号
3)在这个场景中就是利用了ZooKeeper的这个特性
4.3 集群管理
4.3.1 集群管理
- 随着分布式系统规模的日益扩大,集群中的机器规模也随之变大,那如何更好地进行集群管理也显得越来越重要了
- 集群管理包含的内容:
集群监控:侧重对集群运行时状态的收集
集群控制:对集群进行操作与控制
4.3.2 基于Agent的分布式集群管理体系存在问题
-
大规模升级困难
以客户端形式存在的Agent,在大规模使用后,一旦遇上需要大规模升级的情况,就非常麻烦,在升级成本和升级进度的控制上面临巨大的挑战 -
统一的Agent无法满足多样的需求
对于机器的CPU使用率、负载(Load)、内存使用率、网络吞吐以及磁盘容量等机器基本的物理状态,使用统一的Agent来进行监控或许都可以满足。但是,如果需要深入应用内部,对一些业务状态进行监控,或者在一个分布式任务调度系统中,需要对每个机器上任务的执行情况进行监控。对于这些业务耦合紧密的监控需求,不适合由一个统一的Agent来提供 -
编程语言多样性
随着越来越多编程语言的出现,各种异构系统层出不穷。如果使用传统的Agent方法,那么需要提供各种语言的Agent客户端。另一方面,"监控中心"在对异构系统的数据进行整合上面临巨大挑战
4.3.3 Zookeeper的两大特性
- 客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知
- 对在Zookeeper上创建的临时节点,一旦客户端与服务器之间的会话失效,那么临时节点也会被自动删除
4.3.4 案例:分布式日志收集系统
-
分布式日志收集系统的核心作用就是收集分布在不同机器上的系统日志
-
大规模的分布式日志收集系统场景,通常需要解决两个问题:
变化的日志源机器:在生成环境中,伴随着机器的变动,每个应用的机器几乎每天都是在变化的,也就是说每个组别中的日志源机器通常是在不断变化的
变化的收集器机器:日志收集系统自身也会有机器的变更或扩容,于是会出现新的收集器机器加入或是老的收集器机器退出的情况 -
使用Zookeeper实现步骤:
1)注册收集器机器
在ZooKeeper上创建一个节点作为收集器的根节点,例如/logs/collector,每个收集器机器在启动的时候,都会在收集器节点下创建自己的节点,例如/logs/collector/[Hostname]
2)任务分发
待所有收集器机器都创建好自己对应的节点后,系统根据收集器节点下子节点的个数,将所有日志源机器分成对应的若干组,然后将分组后的机器列表分别写到这些收集器机器创建的子节点(例如/logs/collector/host1)上去。这样一来,每个收集器机器都能够从自己对应的收集器节点上获取日志志源机器列表,进行日志收集工作
3)任务分发
每个收集器机器在创建完自己的专属节点后,还需要在对应的子节点上创建个个状态子节点,例如/logs/collector/host1/status,每个收集器机器都需要定期向该节点写入自己的状态信息。这是一种心跳检测机制,收集器机器写入日志收集进度信息。日志系统根据该状态子节点的最后更新时间来判断对应的收集器机器是否存活
4)动态分配
如果收集器机器挂掉或是扩容了,就需要动态地进行收集任务的分配。在运行过程中,日志系统始终关注着/logs/collector这个节点下所有子节点的变更,一旦检测到有收集器机器停止汇报报或是有新的收集器机器加入,就要开始进行任务的重新分配
4.4 Master选举
4.4.1 概念
- Master选举是一个在分布式系统中非常常见的应用场景
- 分布式最核心的特性就是能够将具有独立计算能力的系统单元部署在不同的机器上,构成一个完整的分布式系统
- 实际场景中往往也需要在这些分布在不同机器上的独立系统单元中选出一个所谓的"老大",在计算机中,我们称之为Master
4.4.2 Master在分布式系统中的作用
- Master往往用来协调集群中其他系统单元,具有对分布式系统状态变更的决定权
- 有些场景中,客户端的写请求往往是由Master来处理的
- 有些场景中,Master则常常负责处理一些复杂的逻辑,并将处理结果同步给集群中其他系统单元
4.4.3 案例:广告投放系统后台场景
-
系统分为四个部分:客户端集群、分布式缓存系统、海量数据处理总线、ZooKeeper
-
运行机制:
1)Client集群每天定时会通过ZooKeeper来实现Master选举。
2)选举产生Master客户端之后,这个Master就会负责进行一系列的海量数据处理,最终计算得到一个数据结果,并将其放置在一个内存/数据库中
3)Master还需要通知集群中其他所有的客户端从这个内存/数据库中共享计算结果 -
Master选举过程
-
首先先会在ZooKeeper上创建一个日期节点,例如"2020-11-11"
-
客户端集群每天都会定时往ZooKeeper上创建一个临时节点,例如/master_election/2020-11-11/binding
-
在这个过程中,只有一个客户端能够成功创建这个节点,那么这个客户端所在的机器就成为了Master
-
同时,其他没有在ZooKeeper上成功创建节点的客户端,都会在节点/master_election/2020-11-11上注册一个子节点变更的Watcher,监控当前的Master机器是否存活
-
一旦发现当前的Master挂了,那么其余的客户端将会重新进行Master选举
4.5 分布式锁
- 分布式锁是控制分布式系统之间同步访问共享资源的一种。
- 如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要通过一些互斥手段来防止彼此之干扰,以保证一致性,在这种情况下,就需要使用分布式锁了
- 可以依赖于关系型数据库固有的排他性来实现不同进程之间的互斥,但是当前绝大多数大型分布式系统的性能瓶颈都集中在数据库操作上。因此,如果上层业务再给数据库添加一些额外的锁,例如行锁、表锁甚至是繁重的事务处理,那么就会让数据库更加不堪重负
4.5.1 排他锁
-
概念:
排他锁(Exclusive Locks,简称X锁)也称为写锁或独占锁,是一种基本的锁类型。如果事务T1对数据对象O1加上了排他锁,那么在整个加锁期间,只允许事务T1对O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作——直到T1释放了排他锁 -
Zookeeper实现排他锁
1)定义锁
通过ZooKeeper上的数据节点来表示一个锁,例如/exclusive_lock/lock节点就可以被定义为一个锁,如图:
2)获取锁
在需要获取排他锁时,所有的客户端都会试图通过调用create()接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock。最终只有一个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock节点上注册一个子节点变更的Watcher监听,以便实时监听到lock节点的变更情况
3)释放锁
/exclusive_lock/lock是一个临时节点,当前获取锁的客户端机器发生宕机或者正常执行完业务逻辑后客户端就会主动将自己创建的临时节点删除,这两种情况ZooKeeper上的这个临时节点就会被移除。节点被移除后ZooKeeper都会通知所有在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复"获取锁"过程,如下图:
4.5.2 共享锁
-
概念
共享锁(Shared Locks,简称S锁)也称为读锁。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放。 -
共享锁和排他锁的区别:加上排他锁后数据对象只对一个事务可见;加上共享锁后数据对所有事务都可见
-
Zookeeper实现共享锁
1)定义锁
通过ZooKeeper上的数据节点来表示一个锁,类似于"/shared_lock/[Hostname]-请求类型-序号"的临时顺序节点,例如/shared_lock/host1-R-0000000001,这个节点就代表了一个共享锁,如图所示:
2)获取锁
所有客户端都会到/shared_lock这个节点下面创建一个临时顺序节点,如果当前是读请求,那么就创建例如/shared_lock/host1-R-0000000001的节点;如果是写请求,那么就创建例如/shared_lock/host2-W-0000000002的节点
3)判断读写顺序
- 创建完节点后,获取/shared_lock节点下所有子节点,并对该节点变更注册监听。
- 确定自己的节点序号在所有子节点中的顺序。
- 对于读请求:若没有比自己序号小的子节点或所有比自己序号小的子节点都是读请求,那么表明自己已经成功获取到共享锁,同时开始执行读取逻辑
- 对于写请求:若自己不是序号最小的自节点,那么需要等待
- 接收到Watcher通知后,重复步骤1
4)其释放锁的流程与独占锁一致
4.5.3 羊群效应
- 为什么会发生羊群效应
host1客户端在移除自己的共享锁后,Zookeeper发送了子节点更变Watcher通知给所有机器,然而除了给host2产生影响外,对其他机器没有任何作用。大量的Watcher通知和字节点列表获取两个操作会重复运行,这样不仅会对zookeeper服务器造成巨大的性能影响影响和网络开销,更为严重的是,如果同一时间有多个节点对应的客户端完成事务或是事务中断引起节点消失,ZooKeeper服务器就会在短时间内向其余客户端发送大量的事件通知,这就是所谓的羊群效应,如图:
- 优化方案:
1)客户端调用create接口创建类似于"/shared_lock/[Hostname]-请求类型-序号"的临时顺序节点
2)客户端调用getChildren接口获取所有已经创建的子节点列表(不注册任何Watcher)
3)如果无法获取共享锁,就调用exist接口来对比自己小的节点注册Watcher。读请求是向比自己序号小的最后一个写请求节点注册Watcher监听;写请求是向比自己序号小的最后一个节点注册Watcher监听
4)等待Watcher通知,继续进行步骤2
4.6 分布式队列
4.6.1 模型分类
分布式队列可以简单分为两大类:FIFO先入先出队列模型、等待队列元素聚集后统一安排处理执行的Barrier模型
4.6.2 FIFO先入先出队列模型
-
概述
FIFO(First Input First Output先入先出)是一种非常典型且应用广泛的按序执行的队列模型,先进入队列的请求操作先完成后,才会开始处理后面的请求 -
使用ZooKeeper实现FIFO队列
1)所有客户端都会到/queue_fifo这个节点下面创建一个临时顺序节点,例如/queue_fifo/host1-00000001
2)通过调用getChildren接口来获取/queue_fifo节点的所有子节点,即获取队列中所有的元素
3)确定自己的节点序号在所有子节点中的顺序
4)如果自己的序号不是最小,那么需要等待,同时向比自己序号小的最后一个节点注册Watcher监听
5)接收到Watcher通知后,重复步骤1
4.6.3 Barrier模型
-
概述
Barrier原意是指障碍物、屏障,在分布式系统中,特指系统之间的一个协调条件,规定了一个队列的元素必须都集聚后才能统一进行安排,否则一直等待。这往往出现在那些大规模分布式并行计算的应用场景上:最终的合并计算需要基于很多并行计算的子结果来进行 -
使用ZooKeeper实现FIFO队列
1)开始时/queue_barrier节点是一个已经存在的默认节点,并且将其节点的数据内容赋值为一个数字n来代表Barrier值,例如n=10表示只有当/queue_barrier节点下的子节点个数达到10后,才会打开Barrier。之后,所有的客户端都会到/queue_barrie节点下创建一个临时节点,例如/queue_barrier/host1,如图所示
2)通过调用getData接口获取/queue_barrier节点的数据内容:10
3)通过调用getChildren接口获取/queue_barrier节点下的所有子节点,同时注册对子节点变更的Watcher监听
4)统计子节点的个数
5)如果子节点个数还不足10个,那么需要等待
6)接受到Wacher通知后,重复步骤2,如下图:
5、Zookeeper深入进阶
5.1 ZAB协议
5.1.1 概念
- ZAB(Zookeeper Atomic Broadcast):为zookeeper专门设计的一种支持崩溃恢复的原子广播协议
- 基于该协议,Zookeeper实现了一种主备模式的系统架构来保持集群中各副本之间的数据的一致性
- 使用一个单一的主进程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程中
- ZAB协议的主备模型架构保证了同一时刻集群中只能够有一个主进程来广播服务器的状态变更,因此能够很好地处理客户端大量的并发请求
- 考虑到主进程在任何时候都有可能出现崩溃退出或重启现象,因此ZAB协议还需要做到当前主进程当出现上述异常情况的时候,依旧能正常工作
5.1.2 ZAB核心
- ZAB协议的核心是定义了对于那些会改变Zookeeper服务器数据状态的事务请求的处理方式
- 所有事务请求必须由一个全局唯一的服务器来协调处理,这样的服务器被称为Leader服务器,余下的服务器则称为Follower服务器
- Leader服务器负责将一个客户端事务请求转化成一个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器
- Leader服务器需要等待所有Follower服务器的反馈,一旦超过半数的Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前一个Proposal进行提交
5.1.3 ZAB协议的两种基本的模式(消息广播、奔溃恢复)
消息广播模式
- ZAB协议的消息广播过程使用原子广播协议,类似于一个二阶段提交过程,针对客户端的事务请求,Leader服务器会为其生成对应的事务Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各自的选票,最后进行事务提交
- 在ZAB的二阶段提交过程中移除了中断逻辑,所有的Follower服务器要么正常反馈Leader提出的事务Proposal,要么就抛弃Leader服务器
- 在这种简化的二阶段提交模型下,无法处理因Leader服务器崩溃退出二带来的数据不一致问题,因此ZAB采用了崩溃恢复模式来解决此问题
崩溃恢复模式.
- 一旦在Leader服务器出现崩溃,或者由于网络原因导致Leader服务器失去了与过半Follower的联系,那么就会进入崩溃恢复模式
- 在ZAB协议中为了保证程序的正确运行,整个恢复过程结束后需要选举出一个新的Leader服务器,因此ZAB协议需要一个有效且可靠的Leader选举算法,从而保证能够快速地选举出新的Leader
- Leader选举算法不仅仅需要让Leader自身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产生出来的新Leader服务器
5.1.4 ZAB协议基本特性
- ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交
- ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务
5.1.5 ZAB协议数据同步
- 所有正常运行的服务器,要么成为Leader,要么成为Follower并和Leader保持同步
- Leader服务器会为每一个Follower服务器都准备一个队列,并将那些没有被各Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器
- 在每一个Proposal消息后面紧接着再发送一个Commit消息,以表示该事务已经被提交
- 等到Follower服务器将所有其尚未同步的事务Proposal都从Leader服务器上同步过来并成功应用到本地数据库中后,Leader服务器就会将该Follower服务器加入到真正的可用Follower列表中
5.1.6 ZAB协议运行时状态分析
- LOOKING:Leader选举阶段
- FOLLOWING:Follower服务器和Leader服务器保持同步状态
- LEADING:Leader服务器作为主进程领导状态
5.1.7 ZAB与Paxos的联系和区别
联系
- 都存在一个类似于Leader进程的角色,由其负责协调多个Follower进程的运行
- Leader进程都会等待超过半数的Follower做出正确的反馈后,才会将一个提议进行提交
- 在ZAB协议中每个Proposal中都包含了一个epoch值,用来代表当前的Leader周期,在Paxos算法中同样存在这样的一个标识,名字为Ballot
区别
- Paxos算法中,新选举产生的主进程会进行两个阶段的工作,第一阶段称为读阶段,新的主进程和其他进程通信来收集主进程提出的提议,并将它们提交。第二阶段称为写阶段,当前主进程开始提出自己的提议。
- ZAB协议在Paxos基础上添加了同步阶段,此时新的Leader会确保存在过半的Follower已经提交了之前的Leader周期中的所有事务Proposal。这一同步阶段的引入,能够有效地保证Leader在新的周期中提出事务Proposal之前,所有的进程都已经完成了对之前所有事务Proposal的提交。
- 总的来说,ZAB协议和Paxos算法的本质区别在于两者的设计目标不太一样,ZAB协议主要用于构建一个高可用的分布式数据主备系统,而Paxos算法则用于构建一个分布式的一致性状态机系统
5.2 服务器角色
5.2.1 Leader
主要工作
- 事务请求的唯一调度和处理者,保证集群事务处理的顺序性
- 集群内部各服务器的调度者
处理链
- 流程图
- 请求处理器说明
请求处理器 | 说明 |
---|---|
PrepRequestProcessor | 请求预处理器,也是leader服务器中的第一个请求处理器。在Zookeeper中,那些会改变服务器状态的请求称为事务请求(创建节点、更新数据、删除节点、创建会话等),PrepRequestProcessor能够识别出当前客户端请求是否是事务请求。对于事务请求,PrepRequestProcessor处理器会对其进行一系列预处理,如创建请求事务头、事务体、会话检查、ACL检查和版本检查等。 |
ProposalRequestProcessor | 事务投票处理器。也是Leader服务器事务处理流程的发起者,对于非事务性请求,ProposalRequestProcessor会直接将请求转发到CommitProcessor处理器,不再做任何处理,而对于事务性请求,处理将请求转发到CommitProcessor,还会根据请求类型创建对应的Proposal提议,并发送给所有的Follower服务器来发起一次集群内的事务投票。同时,ProposalRequestProcessor还会将事务请求交付给SyncRequestProcessor进行事务日志的记录。 |
SyncRequestProcessor | 事务日志记录处理器。用来将事务请求记录到事务日志文件中,同时会触发Zookeeper进行数据快照 |
AckRequestProcessor | 负责在SyncRequestProcessor完成事务日志记录后,向Proposal的投票收集器发送ACK反馈,以通知投票收集器当前服务器已经完成了对该Proposal的事务日志记录 |
CommitProcessor | 事务提交处理器。对于非事务请求该处理器会直接将其交付给下一级处理器处理;对于事务请求,其会等待集群内针对Proposal的投票直到该Proposal可被提交,利用CommitProcessor,每个服务器都可以很好地控制对事务请求的顺序处理 |
ToBeCommitProcessor | 该处理器有一个toBeApplied队列,用来存储那些已经被CommitProcessor处理过的可被提交的Proposal。其会将这些请求交付给FinalRequestProcessor处理器处理,待其处理完后,再将其从toBeApplied队列中移除 |
FinalRequestProcessor | 用来进行客户端请求返回之前的操作,包括创建客户端请求的响应,针对事务请求,该处理器还会负责将事务应用到内存数据库中 |
5.2.2 Follower
主要工作
- 处理客户端非事务性请求(读取数据),转发事务请求给Leader服务器
- 参与事务请求Proposal的投票
- 参与Leader选举投票
处理链
-
流程图
-
说明
和Leader服务器的请求处理链最大的不同点在于,Follower服务器的第一个处理器换成了FollowerRequestProcessor处理器,同时由于不需要处理事务请求的投票,因此也没有了ProposalRequestProcessor处理器
处理器 | 说明 |
---|---|
FollowerRequestProcessor | 其用作识别当前请求是否是事务请求,若是,那么Follower就会将该请求转发给Leader服务器,Leader服务器在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进?处理 |
SendAckRequestProcessor | 其承担了事务日志记录反馈的角色,在完成事务日志记录后,会向Leader服务器发送ACK消息以表明自身完成了事务日志的记录工作 |
5.2.3 Observer
主要工作
- Observer服务器在工作上和Follower基本是一致的,对于非事务请求,都可以进行独立的处理,而对于事务请求,则会转发给Leader服务器进行处理
- 和Follower唯一的区别在于,Observer不参与任何形式的投票(括事务请求Proposal的投票和Leader选举投票)
- 简单地讲,Observer服务器只提供非事务服务,通常用于在不影响集群事务处理能力的前提下提升集群的非事务处理能力
处理链
-
流程图
-
说明
需要注意的是,虽然在图中可以看到Observer服务器在初始化阶段会将SyncRequestProcessor处理器也组装上去,但是在实际运行过程中,Leader服务器不会将事务请求的投票发送给Observer服务器
5.3 服务器启动
5.3.1 服务端整体架构图
Zookeeper服务器的启动大致可以分为以下五个步骤
- 配置文件解析
- 初始化数据管理器
- 初始化网络I/O管理器
- 数据恢复
- 对外服务
5.3.2 单机版服务器启动
- 流程图
- 启动过程可以分为两步:预启动、初始化
预启动
1)统一由QuorumPeerMain作为启动类。无论单机或集群,在zkServer.cmd和zkServer.sh中都配置了QuorumPeerMain作为启动入口类
2)解析配置文件zoo.cfg。配置运行时的基本参数,如tickTime、dataDir、clientPort等
3)创建并启动历史文件清理器DatadirCleanupManager。对事务日志和快照数据文进行定时清理
4)判断当前是集群模式还是单机模式启动。若是单机模式,则委托给ZooKeeperServerMain进行启动
5)再次进行配置文件zoo.cfg的解析
6)创建服务器实例ZooKeeperServer。Zookeeper服务器首先会进行服务器实例的创建,然后对该服务器实例进行初始化,包括连接器、内存数据库、请求处理器等组件的初始化
初始化
1)创建服务器统计器ServerStats。ServerStats是Zookeeper服务器运行时的统计器
2)创建Zookeeper数据管理器FileTxnSnapLog。FileTxnSnapLog是Zookeeper上层服务器和底层数据存储之间的对接层,提供了一系列操作数据文件的接口,如事务日志文件和快照数据文件Zookeeper根据zoo.cfg文件中解析出的快照数据目录dataDir和事务日志目录dataLogDir来创建FileTxnSnapLog
3)设置服务器tickTime和会话超时时间限制
4)创建ServerCnxnFactory。通过配置系统属性zookeper.serverCnxnFactory来指定使用Zookeeper自己实现的NIO还是使用Netty框架作为Zookeeper服务端网络连接工厂
5)初始化ServerCnxnFactory。Zookeeper会初始化Thread作为ServerCnxnFactory的主线程,然后再初始化NIO服务器
6)启动ServerCnxnFactory主线程。进入Thread的run方法,此时服务端还不能处理客户端请求
7)恢复本地数据。启动时,需要从本地快照数据文件和事务日志文件进行数据恢复
8)创建并启动会话管理器。Zookeeper会创建会话管理器SessionTracker进行会话管理
9)初始化Zookeeper的请求处理链。Zookeeper请求处理方式为责任链模式的实现。会有多个请求处理器依次处理一个客户端请求,在服务器启动时,会将这些请求处理器串联成一个请求处理链
10)注册JMX服务。Zookeeper会将服务器运行时的一些信息以JMX的方法暴露给外部
11)注册Zookeeper服务器实例。将Zookeeper服务器实例注册给ServerCnxnFactory,之后Zookeeper就可以对外提供服务
5.3.3 集群服务器启动
-
流程图
-
启动过程分为预启动、初始化、Leader选举、Leader与Follower启动期交互、Leader与Follower启动
预启动
1)统一由QuorumPeerMain作为启动类
2)解析配置文科zoo.cfg
3)创建并启动历史文件清理器DatadirCleanupFactory
4)判断当前是集群模式还是单机模式的启动。在集群模式中,在zoo.cfg文件中配置了多个服务器地址,可以选择集群启动
初始化
1)创建ServerCnxnFactory
2)初始化ServerCnxnFactory
3)创建Zookeeper数据管理器FileTxnSnapLog
4)创建QuorumPeer实例。Quorum是集群模式下特有的对象,是Zookeeper服务器实例(ZooKeeperServer)的托管者,QuorumPeer代表了集群中的一台机器,在运行期间QuorumPeer会不断检测当前服务器实例的运行状态,同时根据情况发起Leader选举。
5)创建内存数据库ZKDatabase。ZKDatabase负责管理ZooKeeper的所有会话记录以及DataTree和事务日志的存储
6)初始化QuorumPeer。将核心组件如FileTxnSnapLog、ServerCnxnFactory、ZKDatabase注册到QuorumPeer中,同时配置QuorumPeer的参数,如服务器列表地址、Leader选举算法和会话超时时间限制等
7)恢复本地数据
8)启动ServerCnxnFactory主线程
Leader选举
1)初始化Leader选举
2)注册JMX服务
3)检测当前服务器状态运行期间,QuorumPeer会不断检测当前服务器状态。在正常情况下,Zookeeper服务器的状态在LOOKING、LEADING、FOLLOWING/OBSERVING之间进行切换。在启动阶段,QuorumPeer的初始状态是LOOKING,因此开始进行Leader选举
4)Leader选举,就是一个集群中所有的机器相互之间进行一系列投票,选举产生最合适的机器成为Leader,同时其余机器成为Follower或是Observer的集群机器角色初始化过程
Leader和Follower启动期交互过程
1)创建Leader服务器和Follower服务器。完成Leader选举后,每个服务器会根据自己服务器的角色创建相应的服务器实例,并进行各自角色的主流程
2)Leader服务器启动Follower接收器LearnerCnxAcceptor。运行期间,Leader服务器需要和所有其余的服务器(统称为Learner)保持连接以确集群的机器存活情况,LearnerCnxAcceptor负责接收所有非Leader服务器的连接请求
3)Learner服务器开始和Leader建立连接。所有Learner会找到Leader服务器,并与其建立连接
4)Leader服务器创建LearnerHandler。Leader接收到来自其他机器连接创建请求后,会创建一个LearnerHandler实例,每个LearnerHandler实例都对应一个Leader与Learner服务器之间的连接,其负责Leader和Learner服务器之间几乎所有的消息通信和数据同步
5)向Leader注册。Learner完成和Leader的连接后,会向Leader进行注册,即将Learner服务器的基本信息(LearnerInfo),包括SID和ZXID,发送给Leader服务器
6)Leader解析Learner信息,计算新的epoch。Leader接收到Learner服务器基本信息后,会解析出该Learner的SID和ZXID,然后根据ZXID解析出对应的epoch_of_learner,并和当前Leader服务器的epoch_of_leader进行比较,如果该Learner的epoch_of_learner更新,则更新Leader的epoch_of_leader = epoch_of_learner + 1。然后LearnHandler进行等待,直到过半Learner已经向Leader进行了注册,同时更新了epoch_of_leader后,Leader就可以确定当前集群的epoch了
7)发送Leader状态。计算出新的epoch后,Leader会将该信息以一个LEADERINFO消息的形式发送给Learner,并等待Learner的响应
8)Learner发送ACK消息。Learner接收到LEADERINFO后,会解析出epoch和ZXID,然后向Leader反馈一个ACKEPOCH响应
9)数据同步。Leader收到Learner的ACKEPOCH后,即可进行数据同步
10)启动Leader和Learner服务器。当有过半Learner已经完成了数据同步,那么Leader和Learner服务器实例就可以启动了
Leader和Follower启动
1)创建启动会话管理器
2)初始化Zookeeper请求处理链,集群模式的每个处理器也会在启动阶段串联请求处理链
3)注册JMX服务
4)集群启动完毕
5.4 leader选举
-
概述
Leader选举是zookeeper最重要的技术一,也是保证分布式数据一致性的关键所在 -
需要进行Leader选举的场景.
1)服务器初始化启动
2)服务器运行期间无法和Leader保持连接
5.4.1 服务器启动时期的Leader选举
-
每个Server发出一个投票
由于是初始情况,Server1(myid为1)和Server2(myid为2)都会将自己作为Leader服务器来进行投票,每次投票会包含所推举的服务器的myid和ZXID,使用(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各自将这个投票发给集群中其他机器 -
接受来自各个服务器的投票
集群的每个服务器收到投票后,首先判断该投票的有效性,如检查是否是本轮投票、是否来自LOOKING状态的服务器 -
处理投票
针对每一个投票,服务器都需要将别人的投票和自己的投票进行PK,比较ZXID,比较大的是Leader,如果ZXID相同则比较myid,myid大的作为Leader -
统计投票
每次投票后,服务器都会统计所有投票,判断是否已经有过半的机器接收到相同的投票信息,如果有就选举除了Leader -
改变服务器状态
一旦确定了Leader,每个服务器就会更新自己的状态:如果是Follower就变更为FOLLOWING;如果是Leader就变更为LEADING
5.4.2 服务器运行时期的Leader选举
当Leader所在的机器挂了,那么整个集群将暂时无法对外服务,需要进入新一轮的Leader选举
Leader挂后,余下的非Observer服务器都会将自己的服务器状态变更为LOOKING,然后开始进行Leader选举过程,后面的过程与服务器启动时期的Leader选举一致
6、Zookeeper源码分析
6.1 源码环境搭建
- zk源码下载地址:https://github.com/apache/zookeeper/tree/release-3.5.4
- 注意:因为zk是由ant来构建的,所以需要使用ant命令来转换成工厂,然后导入idea
- 将准备好的zookeeper-release-3.5.4导入idea中
6.1.1 启动服务端
运行主类org.apache.zookeeper.server.QuorumPeerMain,将zoo.cfg的完整路径配置在Program arguments
6.1.2 启动客户端
通过运行ZooKeeperServerMain得到的?志,可以得知ZooKeeper服务端已经启动,服务的地址为127.0.0.1:2181
启动客户端来进行连接测试。客户端的启动类为org.apache.zookeeper.ZooKeeperMain
6.2 zookeeper源码分析之单机模式服务端启动
6.2.1 ZooKeeperServerMain里面的main函数代码
public static void main(String[] args) {
ZooKeeperServerMain main = new ZooKeeperServerMain();
main.initializeAndRun(args);
}
protected void initializeAndRun(String[] args) throws ConfigException, IOException, AdminServerException {
ServerConfig config = new ServerConfig();
//如果形参只有一个,则认为是配置文件的路径
if (args.length == 1) {
config.parse(args[0]);
} else {
//否则是各个参数
config.parse(args);
}
runFromConfig(config);
}
//省略部分代码,只保留了核心逻辑
public void runFromConfig(ServerConfig config)
throws IOException, AdminServerException {
LOG.info("Starting server");
FileTxnSnapLog txnLog = null;
try {
//初始化日志文件
txnLog = new FileTxnSnapLog(config.dataLogDir, config.dataDir);
// 初始化zkServer对象
final ZooKeeperServer zkServer = new ZooKeeperServer(txnLog,
config.tickTime, config.minSessionTimeout, config.maxSessionTimeout, null);
// 服务结束钩子,用于知道服务器错误或关闭状态更改。
final CountDownLatch shutdownLatch = new CountDownLatch(1);
zkServer.registerServerShutdownHandler(
new ZooKeeperServerShutdownHandler(shutdownLatch));
// 创建admin服务,用于接收请求(创建jetty服务)
adminServer = AdminServerFactory.createAdminServer();
// 设置zookeeper服务
adminServer.setZooKeeperServer(zkServer);
// AdminServer是3.5.0之后支持的特性,启动了一个jettyserver,默认端口是8080,访问此端口可以获取Zookeeper运行时的相关信息
adminServer.start();
boolean needStartZKServer = true;
//---启动ZooKeeperServer
//判断配置文件中 clientportAddress是否为null
if (config.getClientPortAddress() != null) {
//ServerCnxnFactory是Zookeeper中的重要组件,负责处理客户端与服务器的连接
//初始化server端IO对象,默认是NIOServerCnxnFactory:Java原生NIO处理网络IO事件
cnxnFactory = ServerCnxnFactory.createFactory();
//初始化配置信息
cnxnFactory.configure(config.getClientPortAddress(), config.getMaxClientCnxns(), false);
//启动服务:此方法除了启动ServerCnxnFactory,还会启动ZooKeeper
cnxnFactory.startup(zkServer);
// zkServer has been started. So we don't need to start it again in secureCnxnFactory.
needStartZKServer = false;
}
if (config.getSecureClientPortAddress() != null) {
secureCnxnFactory = ServerCnxnFactory.createFactory();
secureCnxnFactory.configure(config.getSecureClientPortAddress(), config.getMaxClientCnxns(), true);
secureCnxnFactory.startup(zkServer, needStartZKServer);
}
// 定时清除容器节点
//container ZNodes是3.6版本之后新增的节点类型,Container类型的节点会在它没有子节点时
// 被删除(新创建的Container节点除外),该类就是用来周期性的进行检查清理工作
containerManager = new ContainerManager(zkServer.getZKDatabase(), zkServer.firstProcessor,
Integer.getInteger("znode.container.checkIntervalMs", (int) TimeUnit.MINUTES.toMillis(1)),
Integer.getInteger("znode.container.maxPerMinute", 10000)
);
containerManager.start();
// ZooKeeperServerShutdownHandler处理逻辑,只有在服务运行不正常的情况下,才会往下执行
shutdownLatch.await();
// 关闭服务
shutdown();
if (cnxnFactory != null) {
cnxnFactory.join();
}
if (secureCnxnFactory != null) {
secureCnxnFactory.join();
}
if (zkServer.canShutdown()) {
zkServer.shutdown(true);
}
} catch (InterruptedException e) {
// warn, but generally this is ok
LOG.warn("Server interrupted", e);
} finally {
if (txnLog != null) {
txnLog.close();
}
}
}
1、注册jmx
2、解析ServerConfig配置对象
3、根据配置对象,单机zk服务
4、创建管理事务日志和快照FileTxnSnapLog对象,zookeeperServer对象,并设置zkServer的统计对象
5、设置zk服务钩子,原理是通过设置CountDownLatch,调用ZooKeeperServerShutdownHandler的handle方法,可以将触发shutdownLatch.await方法继续执行,即调用shutdown关闭单机服务
6、基于jetty创建zk的admin服务
7、创建连接对象cnxnFactory和secureCnxnFactory(安全连接才创建该对象),用于处理客户端的请求
8、创建定时清除容器节点管理器,用于处理容器节点下不存在子节点的清理容器节点工作等
6.2.2 解析配置入口:NIOServerCnxnFactory.configure方法
public void configure(InetSocketAddress addr, int maxcc, boolean secure) throws IOException {
if (secure) {
throw new UnsupportedOperationException("SSL isn't supported in NIOServerCnxn");
}
configureSaslLogin();
maxClientCnxns = maxcc;
//会话超时时间
sessionlessCnxnTimeout = Integer.getInteger(
ZOOKEEPER_NIO_SESSIONLESS_CNXN_TIMEOUT, 10000);
//过期队列
cnxnExpiryQueue =
new ExpiryQueue<NIOServerCnxn>(sessionlessCnxnTimeout);
expirerThread = new ConnectionExpirerThread();
//根据CPU个数计算selector线程的数量
int numCores = Runtime.getRuntime().availableProcessors();
numSelectorThreads = Integer.getInteger(
ZOOKEEPER_NIO_NUM_SELECTOR_THREADS,
Math.max((int) Math.sqrt((float) numCores/2), 1));
if (numSelectorThreads < 1) {
throw new IOException("numSelectorThreads must be at least 1");
}
//计算woker线程的数量
numWorkerThreads = Integer.getInteger(
ZOOKEEPER_NIO_NUM_WORKER_THREADS, 2 * numCores);
//worker线程关闭时间
workerShutdownTimeoutMS = Long.getLong(
ZOOKEEPER_NIO_SHUTDOWN_TIMEOUT, 5000);
LOG.info("Configuring NIO connection handler with "
+ (sessionlessCnxnTimeout/1000) + "s sessionless connection"
+ " timeout, " + numSelectorThreads + " selector thread(s), "
+ (numWorkerThreads > 0 ? numWorkerThreads : "no")
+ " worker threads, and "
+ (directBufferBytes == 0 ? "gathered writes." :
("" + (directBufferBytes/1024) + " kB direct buffers.")));
//初始化selector线程
for(int i=0; i<numSelectorThreads; ++i) {
selectorThreads.add(new SelectorThread(i));
}
this.ss = ServerSocketChannel.open();
ss.socket().setReuseAddress(true);
LOG.info("binding to port " + addr);
ss.socket().bind(addr);
ss.configureBlocking(false);
//初始化accept线程,这里看出accept线程只有一个,里面会注册监听ACCEPT事件
acceptThread = new AcceptThread(ss, addr, selectorThreads);
}
6.2.3 启动入口:NIOServerCnxnFactory.startup方法
//启动分了好几块,一个一个看
@Override
public void startup(ZooKeeperServer zks, boolean startServer)
throws IOException, InterruptedException {
// 1、启动相关线程
start();
setZooKeeperServer(zks);
//启动服务
if (startServer) {
// 2、加载数据到zkDataBase
zks.startdata();
// 3、启动定时清除session的管理器,注册jmx,添加请求处理器
zks.startup();
}
}
// 1、启动相关线程
public void start() {
stopped = false;
//初始化worker线程池
if (workerPool == null) {
workerPool = new WorkerService(
"NIOWorker", numWorkerThreads, false);
}
//挨个启动Selector线程(处理客户端请求线程),
for(SelectorThread thread : selectorThreads) {
if (thread.getState() == Thread.State.NEW) {
thread.start();
}
}
// ensure thread is started once and only once
//启动acceptThread线程(处理接收连接进行事件)
if (acceptThread.getState() == Thread.State.NEW) {
acceptThread.start();
}
// ExpirerThread(处理过期连接)
if (expirerThread.getState() == Thread.State.NEW) {
expirerThread.start();
}
}
// 2、初始化数据结构(加载数据到zkDataBase)
public void startdata() throws IOException, InterruptedException {
//初始化ZKDatabase,该数据结构用来保存ZK上面存储的所有数据
//check to see if zkDb is not null
if (zkDb == null) {
//初始化数据数据,这里会加入一些原始节点,例如/zookeeper
zkDb = new ZKDatabase(this.txnLogFactory);
}
//加载磁盘上已经存储的数据,如果有的话
if (!zkDb.isInitialized()) {
loadData();
}
}
// 3、启动定时清除session的管理器,注册jmx,添加请求处理器
public synchronized void startup() {
//初始化session追踪器
if (sessionTracker == null) {
createSessionTracker();
}
//启动session追踪器
startSessionTracker();
//建立请求处理链路
setupRequestProcessors();
//注册jmx
registerJMX();
setState(State.RUNNING);
notifyAll();
}
// 这里可以看出,单机模式下请求的处理链路为:
// PrepRequestProcessor -> SyncRequestProcessor -> FinalRequestProcessor
protected void setupRequestProcessors() {
RequestProcessor finalProcessor = new FinalRequestProcessor(this);
RequestProcessor syncProcessor = new SyncRequestProcessor(this, finalProcessor);
((SyncRequestProcessor)syncProcessor).start();
firstProcessor = new PrepRequestProcessor(this, syncProcessor);
((PrepRequestProcessor)firstProcessor).start();
}
6.3 zookeeper源码分析之Leader选举
6.3.1 总体框架图
Election源码分析
package org.apache.zookeeper.server.quorum;
import org.apache.zookeeper.server.quorum.Vote;
public interface Election {
// 序号Leader
public Vote lookForLeader() throws InterruptedException;
// 关闭
public void shutdown();
}
6.3.2 Zookeeper中默认的选举策略FastLeaderElection
- 类的继承关系
// FastLeaderElection实现了Election接口,重写了接?中定义的lookForLeader方法和shutdown方法
public class FastLeaderElection implements Election {}
-
几个概念
1)外部投票:特指其他服务器发来的投票
2)内部投票:服务器?身当前的投票
3)选举轮次:ZooKeeper服务器Leader选举的轮次,即logical clock(逻辑时钟)
4)PK:指对内部投票和外部投票进行一个对比来确定是否需要变更内部投票。选票管理
5)sendqueue:选票发送队列,用于保存待发送的选票
6)recvqueue:选票接收队列,用于保存接收到的外部投票 -
FastLeaderElection的基本结构
-
lookForLeader函数
当ZooKeeper服务器检测到当前服务器状态变成LOOKING时,就会触发Leader选举,即调用lookForLeader方法来进行Leader选举
5. 核心源码FastLeaderElection.java
中的lookForLeader方法
public Vote lookForLeader() throws InterruptedException {
try {
self.jmxLeaderElectionBean = new LeaderElectionBean();
MBeanRegistry.getInstance().register(
self.jmxLeaderElectionBean, self.jmxLocalPeerBean);
} catch (Exception e) {
LOG.warn("Failed to register with JMX", e);
self.jmxLeaderElectionBean = null;
}
if (self.start_fle == 0) {
self.start_fle = Time.currentElapsedTime();
}
try {
HashMap<Long, Vote> recvset = new HashMap<Long, Vote>();
HashMap<Long, Vote> outofelection = new HashMap<Long, Vote>();
int notTimeout = finalizeWait;
synchronized(this){
// 首先会将逻辑时钟自增,每进行一轮新的leader选举,都需要更新逻辑时钟
logicalclock.incrementAndGet();
// 更新选票(初始化选票)
updateProposal(getInitId(), getInitLastLoggedZxid(), getPeerEpoch());
}
LOG.info("New election. My id = " + self.getId() +
", proposed zxid=0x" + Long.toHexString(proposedZxid));
// 向其他服务器发送自己的选票(已更新的选票)
sendNotifications();
/*
* Loop in which we exchange notifications until we find a leader
*/
while ((self.getPeerState() == ServerState.LOOKING) &&
(!stop)){
// 从recvqueue接受队列中获取投票
Notification n = recvqueue.poll(notTimeout,
TimeUnit.MILLISECONDS);
if(n == null){ // 无法获取投票
if(manager.haveDelivered()){ // manager已经发送了所有选票消息(表示有连接)
// 向所有其他服务器发送消息
sendNotifications();
} else {// 还未发送消息(表示无连接)
// 连接其他每个服务器
manager.connectAll();
}
/*
* Exponential backoff
*/
int tmpTimeOut = notTimeout*2;
notTimeout = (tmpTimeOut < maxNotificationInterval?
tmpTimeOut : maxNotificationInterval);
LOG.info("Notification time out: " + notTimeout);
}
else if (validVoter(n.sid) && validVoter(n.leader)) {
switch (n.state) {
case LOOKING:
if (n.electionEpoch > logicalclock.get()) {// 其选选举周期大于逻辑时钟
// 重新赋值逻辑时钟
logicalclock.set(n.electionEpoch);
// 清空所有接受到的所有选票
recvset.clear();
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
getInitId(), getInitLastLoggedZxid(), getPeerEpoch())) {// 进行PK,选择出较优的服务器
// 更新选票
updateProposal(n.leader, n.zxid, n.peerEpoch);
} else {// 无法选择出较优的服务器
// 更新选票
updateProposal(getInitId(),
getInitLastLoggedZxid(),
getPeerEpoch());
}
// 发送本服务器的内部选票信息
sendNotifications();
} else if (n.electionEpoch < logicalclock.get()) {// 选举周期小于逻辑时钟,不做处理,直接忽略
if(LOG.isDebugEnabled()){
LOG.debug("Notification election epoch is smaller than logicalclock. n.electionEpoch = 0x"
+ Long.toHexString(n.electionEpoch)
+ ", logicalclock=0x" + Long.toHexString(logicalclock.get()));
}
break;
} else if (totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)) {// PK,选择出较优的服务器
// 更新选票
updateProposal(n.leader, n.zxid, n.peerEpoch);
// 发送消息
sendNotifications();
}
if(LOG.isDebugEnabled()){
LOG.debug("Adding vote: from=" + n.sid +
", proposed leader=" + n.leader +
", proposed zxid=0x" + Long.toHexString(n.zxid) +
", proposed election epoch=0x" + Long.toHexString(n.electionEpoch));
}
// recvset用于记录当前服务器在本轮次的Leader选举中收到的所有外部投票
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
if (termPredicate(recvset,
new Vote(proposedLeader, proposedZxid,
logicalclock.get(), proposedEpoch))) {// 若能选出leader
// Verify if there is any change in the proposed leader
while((n = recvqueue.poll(finalizeWait,
TimeUnit.MILLISECONDS)) != null){// 遍历已经接收的投票集合
if(totalOrderPredicate(n.leader, n.zxid, n.peerEpoch,
proposedLeader, proposedZxid, proposedEpoch)){// 选票有变更,比之前提议的Leader有更好的选票加入
// 将更优的选票放在recvset中
recvqueue.put(n);
break;
}
}
if (n == null) {// 表示之前提议的Leader已经是最优的
// 设置服务器状态
self.setPeerState((proposedLeader == self.getId()) ?
ServerState.LEADING: learningState());
// 最终的选票
Vote endVote = new Vote(proposedLeader,
proposedZxid, proposedEpoch);
// 清空recvqueue队列的选票
leaveInstance(endVote);
// 返回选票
return endVote;
}
}
break;
case OBSERVING:
LOG.debug("Notification from observer: " + n.sid);
break;
case FOLLOWING:
case LEADING:
if(n.electionEpoch == logicalclock.get()){
recvset.put(n.sid, new Vote(n.leader, n.zxid, n.electionEpoch, n.peerEpoch));
if(termPredicate(recvset, new Vote(n.leader,
n.zxid, n.electionEpoch, n.peerEpoch, n.state))
&& checkLeader(outofelection, n.leader, n.electionEpoch)) {
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
Vote endVote = new Vote(n.leader, n.zxid, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
}
outofelection.put(n.sid, new Vote(n.leader,
IGNOREVALUE, IGNOREVALUE, n.peerEpoch, n.state));
if (termPredicate(outofelection, new Vote(n.leader,
IGNOREVALUE, IGNOREVALUE, n.peerEpoch, n.state))
&& checkLeader(outofelection, n.leader, IGNOREVALUE)) {
synchronized(this){
logicalclock.set(n.electionEpoch);
self.setPeerState((n.leader == self.getId()) ?
ServerState.LEADING: learningState());
}
Vote endVote = new Vote(n.leader, n.zxid, n.peerEpoch);
leaveInstance(endVote);
return endVote;
}
break;
default:
LOG.warn("Notification state unrecoginized: " + n.state
+ " (n.state), " + n.sid + " (n.sid)");
break;
}
} else {
if (!validVoter(n.leader)) {
LOG.warn("Ignoring notification for non-cluster member sid {} from sid {}", n.leader, n.sid);
}
if (!validVoter(n.sid)) {
LOG.warn("Ignoring notification for sid {} from non-quorum member sid {}", n.leader, n.sid);
}
}
}
return null;
} finally {
try {
if(self.jmxLeaderElectionBean != null){
MBeanRegistry.getInstance().unregister(
self.jmxLeaderElectionBean);
}
} catch (Exception e) {
LOG.warn("Failed to unregister with JMX", e);
}
self.jmxLeaderElectionBean = null;
LOG.debug("Number of connection processing threads: {}",
manager.getConnectionThreadCount());
}
}
6.4 zookeeper源码分析之集群模式服务端
- 源码分析入口
QuorumPeer.java
的start方法
public synchronized void start() {
// 校验serverid如果不在peer列表中,抛异常
if (!getView().containsKey(myid)) {
throw new RuntimeException("My id " + myid + " not in the peer list");
}
// 加载zk数据库:载入之前持久化的一些信息
loadDataBase();
// 启动连接服务端
startServerCnxnFactory();
try {
adminServer.start();
} catch (AdminServerException e) {
LOG.warn("Problem starting AdminServer", e);
System.out.println(e);
}
// 启动之后马上进行选举,主要是创建选举必须的环境,比如:启动相关线程
startLeaderElection();
// 执行选举逻辑
super.start();
}
QuorumPeer.java
中的startLeaderElection方法
synchronized public void startLeaderElection() {
try {
// 所有节点启动的初始状态都是LOOKING,因此这里都会是创建一张投自己为Leader的票
if (getPeerState() == ServerState.LOOKING) {
currentVote = new Vote(myid, getLastLoggedZxid(), getCurrentEpoch());
}
} catch(IOException e) {
RuntimeException re = new RuntimeException(e.getMessage());
re.setStackTrace(e.getStackTrace());
throw re;
}
// if (!getView().containsKey(myid)) {
// throw new RuntimeException("My id " + myid + " not in the peer list");
//}
if (electionType == 0) {
try {
udpSocket = new DatagramSocket(myQuorumAddr.getPort());
responder = new ResponderThread();
responder.start();
} catch (SocketException e) {
throw new RuntimeException(e);
}
}
//初始化选举算法,electionType默认为3
this.electionAlg = createElectionAlgorithm(electionType);
}
QuorumPeer.java
中的createElectionAlgorithm方法
protected Election createElectionAlgorithm(int electionAlgorithm){
Election le=null;
switch (electionAlgorithm) {
case 0:
le = new LeaderElection(this);
break;
case 1:
le = new AuthFastLeaderElection(this);
break;
case 2:
le = new AuthFastLeaderElection(this, true);
break;
case 3:
//electionAlgorithm默认是3,直接走到这里
qcm = createCnxnManager();
//监听选举事件的listener
QuorumCnxManager.Listener listener = qcm.listener;
if(listener != null){
//开启监听器
listener.start();
//初始化选举算法
FastLeaderElection fle = new FastLeaderElection(this, qcm);
//发起选举
fle.start();
le = fle;
} else {
LOG.error("Null listener when initializing cnx manager");
}
break;
default:
assert false;
}
return le;
}
QuorumPeer.java
类中start方法,因为QuorumPeer.java
本身也是一个线程类,执行的是run方法
public void run() {
// .... 省略部分代码
try {
while (running) {
// 根据当前节点的状态执行不同流程
switch (getPeerState()) {
case LOOKING:
LOG.info("LOOKING");
if (Boolean.getBoolean("readonlymode.enabled")) {
LOG.info("Attempting to start ReadOnlyZooKeeperServer");
// Create read-only server but don't start it immediately
final ReadOnlyZooKeeperServer roZk =
new ReadOnlyZooKeeperServer(logFactory, this, this.zkDb);
Thread roZkMgr = new Thread() {
public void run() {
try {
// lower-bound grace period to 2 secs
sleep(Math.max(2000, tickTime));
if (ServerState.LOOKING.equals(getPeerState())) {
roZk.startup();
}
} catch (InterruptedException e) {
LOG.info("Interrupted while attempting to start ReadOnlyZooKeeperServer, not started");
} catch (Exception e) {
LOG.error("FAILED to start ReadOnlyZooKeeperServer", e);
}
}
};
try {
roZkMgr.start();
reconfigFlagClear();
if (shuttingDownLE) {
shuttingDownLE = false;
startLeaderElection();
}
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
} finally {
roZkMgr.interrupt();
roZk.shutdown();
}
} else {
try {
reconfigFlagClear();
if (shuttingDownLE) {
shuttingDownLE = false;
startLeaderElection();
}
// 寻找Leader节点
setCurrentVote(makeLEStrategy().lookForLeader());
} catch (Exception e) {
LOG.warn("Unexpected exception", e);
setPeerState(ServerState.LOOKING);
}
}
break;
case OBSERVING:
try {
// 当前节点启动模式为Observer
setObserver(makeObserver(logFactory));
// 与Leader节点进行数据同步
observer.observeLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e );
} finally {
observer.shutdown();
setObserver(null);
updateServerState();
}
break;
case FOLLOWING:
try {
// 当前节点启动模式为Follower
setFollower(makeFollower(logFactory));
// 与Leader节点进行数据同步
follower.followLeader();
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
follower.shutdown();
setFollower(null);
updateServerState();
}
break;
case LEADING:
LOG.info("LEADING");
try {
// 当前节点启动模式为Leader
setLeader(makeLeader(logFactory));
// 发送自己成为Leader的通知
leader.lead();
setLeader(null);
} catch (Exception e) {
LOG.warn("Unexpected exception",e);
} finally {
if (leader != null) {
leader.shutdown("Forcing shutdown");
setLeader(null);
}
updateServerState();
}
break;
}
start_fle = Time.currentElapsedTime();
}
} finally {
LOG.warn("QuorumPeer main thread exited");
MBeanRegistry instance = MBeanRegistry.getInstance();
instance.unregister(jmxQuorumBean);
instance.unregister(jmxLocalPeerBean);
for (RemotePeerBean remotePeerBean : jmxRemotePeerBean.values()) {
instance.unregister(remotePeerBean);
}
jmxQuorumBean = null;
jmxLocalPeerBean = null;
jmxRemotePeerBean = null;
}
}