1.Zookeeper简介
1.1 分布式系统面临的问题
分布式系统就是将一个独立的系统拆分成很多个模块,每个子模块能够单独的运行,这个就是分布式系统。但是分布式系统存在的一个很大的缺点就是各个子模块之间协调工作,所谓分布式系统协同工作就是通过某种方式,来让每个节点的信息能共同步和共享。
对于分布式系统来说,达到协同工作有两种通信方式:
1、通过网络共享信息
例子:这就像现实中,开发leader在会上把任务传达下去,组员通过听Leader命令或者看Leader的邮件知道自己要⼲什么。当任务分配有变化时,leader会单独告诉组员,或者再次召开会议。信息通过⼈与⼈之间的直接沟通,完成传递。
2、通过共享存储的方式
例子:这就好比开发leader按照约定的时间和路径,把任务分配表放到了svn上,组员每天去svn上拉取最新的任务分配表,然后干活。其中svn就是共享存储。更好一点的做法是,当svn⽂件版本更新时,触发邮件通知,每个组员再去拉取最新的任务分配表。这样做更好,因为每次更新,组员都能第一时间得到消
息,从而让自己手中的任务分配表永远是最新的。此种方式依赖于中央存储。
[图片上传失败...(image-4214c1-1612341123589)]
1.2 Zookeeper如何解决分布式之间的协调问题
Zookeeper对分布式系统的协调,使用的是第二种方式,共享存储。其实共享存储,分布式应用也需要和存储进行网路络通信。
实际上,通过Zookeeper实现分布式协同的原理,和项⽬组通过SVN同步工作任务的例子是一样的。Zookeeper就像是svn,存储了任务的分配、完成情况等共享信息。每个分布式应用的节点就是组员,订阅这些共享信息。当主节点(组leader),对某个从节点的分⼯信息作出改变时,相关订阅的从节点得到Zookeeper的通知,取得自己最新的任务分配。完成⼯作后,把完成情况存储到Zookeeper。主节点订阅了该任务的完成情况信息,所以将得到Zookeeper的完⼯的通知。参考下图,是不是和前面项⽬组通过svn分配⼯作的例子一模一样?仅仅是把svn和邮件系统合⼆为一,以Zookeeper代替
[图片上传失败...(image-6ce569-1612341123589)]
注:Slave节点要想获取Zookeeper的更新通知,需事先在关心的数据节点上设置观察点。
大多数分布式系统中出现的问题,都源于信息的共享出了问题。如果各个节点间信息不能及时共享和同步,那么就会在协作过程中产生各种问题。Zookeeper解决协同问题的关键,就是在于保证分布式系统信息的一致性。
1.3 Zookeeper基础概念
1、Zookeeper定义
Zookeeper是一个开源的分布式协调服务,其设计⽬标是将那些复杂的且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一些简单的接⼝提供给用户使用。Zookeeper是一个典型的分布式数据一致性的解决⽅案,分布式应用程序可以基于它实现诸如注册中心、数据订阅/发布、负载均衡、命名服务、集群管理、分布式锁和分布式队列等功能。
2、Zookeeper中集群的角色
在分布式系统中,构成集群的每一台集群都有自己的角色,最典型的就是Master/Slaver模式(主备模式),在该集群模式下把能够处理所有写操作的集群称之为Master,通过异步复制方式获取最新数据,并提供读服务的机器称为Slave。
在Zookeeper中不是使用Master/Slave这种主备模式,而是使用的Leader、Follower、Observer这三种角色。在集群中通过选举的方法选举出一台Leader的机器,除了Leader之外其他机器包含有Follower和Observer都可以提供读服务,但是Observer不能参与Leader的选举,不参与写操作的过半写成功策略,因此Observer可以在不影响写性能的情况下提升集群的性能。
3、会话(Session)
Session指客户端会话,一个客户端连接是指客户端和服务端之间的一个TCP⻓连接,Zookeeper对外的
服务端⼝默认为2181,客户端启动的时候,首先会与服务器建⽴一个TCP连接,从第一次连接建⽴开始,客户端会话的生命周期也开始了,通过这个连接,客户端能够心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来⾃服务器的Watch事件通知。
4、数据节点(Znode)
在谈到分布式的时候,我们通常说的“节点”是指组成集群的每一台机器。然而,在Zookeeper中,“节
点”分为两类,第一类同样是指构成集群的机器,我们称之为机器节点;第⼆类则是指数据模型中的数据单元,我们称之为数据节点——ZNode。Zookeeper将所有数据存储在内存中,数据模型是一棵树(ZNode Tree),由斜杠(/)进行分割的路径,就是一个Znode,例如/app/path1。每个ZNode上都会保存⾃⼰的数据内容,同时还会保存一系列属性信息。
节点特性
1、ttl和容器节点,ttl添加过期事件的容器节点在没有子节点的情况下过一段事件会自动删除
2、临时节点不能存在子节点
3、同一级别不能存在相同名称的节点
5、版本
Zookeeper的每个Znode上都会存储数据,对于每个ZNode,Zookeeper都会为其维护一个叫作Stat的数据结构,Stat记录了这个ZNode的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode子节点的版本)、aversion(当前ZNode的ACL版本)。
6、Watcher(事件监听器)
Wathcer(事件监听器),是Zookeeper中一个很重要的特性,Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端,该机制是Zookeeper实现分布式协调服务的重要特性。
7、ACL
Zookeeper采用ACL(Access Control Lists)策略来进行权限控制,其定义了如下五种权限:
· CREATE:创建子节点的权限。
· READ:获取节点数据和子节点列表的权限。
· WRITE:更新节点数据的权限。
· DELETE:删除子节点的权限。
· ADMIN:设置节点ACL的权限。
其中需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。
2.Zookeeper环境搭建
2.1 Zookeeper单机版环境搭建
Zookeeper安装⽅式有三种,单机模式和集群模式以及伪集群模式。
■ 单机模式:Zookeeper只运行在一台服务器上,适合测试环境;
■ 集群模式:Zookeeper运行于一个集群上,适合生产环境,这个计算机集群被称为一个“集合体”
■ 伪集群模式:就是在一台服务器上运行多个Zookeeper 实例;
单机版安装步骤:
1、下载地址:https://Zookeeper.apache.org/releases.html
2、下载好之后上传到自己的linux服务器上。
3、去Zookeeper所在目录的conf目录下 cp zoo_sample.cfg zoo.cfg 拷贝配置文件
4、定位到bin目录下,启动命令:./zkServer.sh start
5、查看Zookeeper启动状态 ./zkServer.sh status【如果显示Mode:standalone 表示启动成功】
2.2 Zookeeper伪集群搭建
1、通过https://Zookeeper.apache.org/releases.html 下载好之后上传到linux服务器上,解压并在Zookeeper的根目录下创建文件夹data在data里边创建一个logs文件夹 存放日志。
2、拷贝三个zk的安装目录 cp -r zk01/ zk02/ cp -r zk01/ zk03/
3、修改配置文件
dataDir=/opt/zkCluster/zk01/data/dataLogDir=/opt/zkCluster/zk01/data/logsclientPort=2181dataDir=/opt/zkCluster/zk02/data/dataLogDir=/opt/zkCluster/zk02/data/logsclientPort=2182dataDir=/opt/zkCluster/zk03/data/dataLogDir=/opt/zkCluster/zk03/data/logsclientPort=2183
4、给各个zk设置id
vim /opt/zkCluster/zk01/data/myid [设置为1]vim /opt/zkCluster/zk02/data/myid [设置为2]vim /opt/zkCluster/zk03/data/myid [设置为3]
5、在配置文件中配置集群的通信和选举
server.myid=服务器IP地址:服务器之间通信端⼝:服务器之间投票选举端⼝【集群的通信id默认port多加一个8】
server.1=192.168.186.10:2881:3881server.2=192.168.186.10:2882:3882server.3=192.168.186.10:2883:3883
6、分别启动三台zk
./zk01/bin/zkServer.sh start./zk02/bin/zkServer.sh start./zk03/bin/zkServer.sh start
通过查看状态 Mode: follower或者Mode: leader来查看是否启动成功。
3.Zookeeper基本使用
3.1 Zookeeper系统模型
1、Zookeeper数据模型Znode
在Zookeeper中,数据信息被保存在一个个数据节点上,这些节点被称为ZNode。ZNode 是Zookeeper 中最小的数据单位,在ZNode下面又可以再挂 ZNode,这样一层层下去就形成了一个层次化命名空间 ZNode 树,我们称为 ZNode Tree,它采用了类似⽂件系统的层级树状结构进行管理。
[图片上传失败...(image-683a0d-1612341123589)]
说明:在Zookeeper中,每一个数据节点都是一个 ZNode,上图根目录下有两个节点,分别是:app1 和app2,其中 app1 下面⼜有三个子节点,所有ZNode按层次化进行组织,形成这么一颗树,ZNode的节点路径标识方式和Unix文件系统路径非常相似,都是由一系列使用斜杠(/)进行分割的路径表示,开发⼈员可以向这个节点写入数据,也可以在这个节点下面创建子节点。
2、ZNode类型
Zookeeper 节点类型可以分为三大类:
- 持久性节点(Persistent)
- 临时性节点(Ephemeral)
- 顺序性节点(Sequential)
在开发中在创建节点的时候通过组合可以生成以下四种节点类型:持久节点、持久顺序节点、临时节
点、临时顺序节点。不同类型的节点则会有不同的生命周期。
持久节点:是Zookeeper中最常⻅的一种节点类型,所谓持久节点,就是指节点被创建后会一直存在服务器,直到删除操作主动清除。
持久顺序节点:就是有顺序的持久节点,节点特性和持久节点是一样的,只是额外特性表现在顺序上。顺序特性实质是在创建节点的时候,会在节点名后面加上一个数字后缀,来表示其顺序。
临时节点:就是会被⾃动清理掉的节点,它的生命周期和客户端会话绑在一起,客户端会话结束,节点
会被删除掉。与持久性节点不同的是,临时节点不能创建子节点。
临时顺序节点:就是有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后面加上数字后缀。
3、事务ID
首先,事务是对物理和抽象的应用状态上的操作集合。往往在现在的概念中,狭义上的事务通常指的是数据库事务,一般包含了一系列对数据库有序的读写操作,这些数据库事务具有所谓的ACID特性,即原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability)。
而在Zookeeper中,事务是指能够改变Zookeeper服务器状态的操作,我们也称为事务操作或更新操作,一般包括数据节点创建与删除、数据节点内容更新等操作。对于每一个事务请求,Zookeeper都会为其分配一个全局唯一的事务ID,用 ZXID 来表示,通常是一个64位的数字。每一个 ZXID 对应一次更新操作,从这些ZXID中可以间接地识别出Zookeeper处理这些更新操作请求的全局顺序。【每次增删改就是一次事务操作,就会产生 一个全局唯一事务ID->ZXID】
4、ZNode 的状态信息
整个 ZNode 节点内容包括两部分:节点数据内容和节点状态信息。图中quota 是数据内容,其他的属
于状态信息。那么这些状态信息都有什么含义呢?
cZxid 就是 Create ZXID,表示节点被创建时的事务ID。ctime 就是 Create Time,表示节点创建时间。mZxid 就是 Modified ZXID,表示节点最后一次被修改时的事务ID。mtime 就是 Modified Time,表示节点最后一次被修改的时间。pZxid 表示该节点的子节点列表最后一次被修改时的事务 ID。只有子节点列表变更才会更新 pZxid,子节点内容变更不会更新。cversion 表示子节点的版本号。dataVersion 表示内容版本号。aclVersion 标识acl版本ephemeralOwner 表示创建该临时节点时的会话 sessionID,如果是持久性节点那么值为 0dataLength 表示数据⻓度。numChildren 表示直系子节点数。
5、Watcher-数据变更通知
Zookeeper使用Watcher机制实现分布式数据的发布/订阅功能。
一个典型的发布/订阅模型系统定义了一种一对多的订阅关系,能够让多个订阅者同时监听某一个主题
对象,当这个主题对象自身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。
在Zookeeper中,引入了 Watcher 机制来实现这种分布式的通知功能。Zookeeper允许客户端向服务端注册一个 Watcher 监听,当服务端的一些指定事件触发了这个 Watcher,那么就会向指定客户端发送一个事件通知来实现分布式的通知功能。
Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager、Zookeeper服务器三部分。
具体工作流程为:客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执行回调逻辑。
6、ACL-保障数据的安全
Zookeeper作为一个分布式协调框架,其内部存储了分布式系统运行时状态的元数据,这些元数据会直接影响基于Zookeeper进行构造的分布式系统的运行状态,因此,如何保障系统中数据的安全,从而避免因误操作所带来的数据随意变更而导致的数据库异常⼗分重要,在Zookeeper中,提供了一套完善的ACL(Access Control List)权限控制机制来保障数据的安全。
我们通常从三个方面来理解ACL机制:权限模式(Scheme)、授权对象(ID)、权限(Permission),通常使用"scheme: id : permission"来标识一个有效的ACL信息。
权限模式(Scheme)用来来确定权限验证过程中使用的检验策略,有如下四种模式:
1、IP:IP模式就是通过IP地址粒度来进行权限控制,如"ip:192.168.0.110"表示权限控制针对该IP地址,同时IP模式也可以按照网段⽅式进行配置,如"ip:192.168.0.1/24"表示针对192.168.0.*这个网段进行权限控制。
2、Digest:Digest是最常用的权限控制模式,要更符合我们对权限控制的认识,其使用"username:password"形式的权限标识来进行权限配置,便于区分不同应用来进行权限控制。当我们通过“username:password”形式配置了权限标识后,Zookeeper会先后对其进行SHA-1加密和BASE64编码。
3、World:World是一种最开放的权限控制模式,这种权限控制方式⼏乎没有任何作用,数据节点的访问权限对所有用户开放,即所有用户都可以在不进行任何权限校验的情况下操作Zookeeper上的数据。另外,World模式也可以看作是一种特殊的Digest模式,它只有一个权限标识,即“world:anyone”。
4、Super:Super模式,顾名思义就是超级用户的意思,也是一种特殊的Digest模式。在Super模式下,超级用户可以对任意Zookeeper上的数据节点进行任何操作。
授权对象(ID)指的是权限赋予的用户或一个指定实体,例如 IP 地址或是机器等。在不同的权限模式下,授
权对象是不同的,表中列出了各个权限模式和授权对象之间的对应关系。
授权模式授权对象IP通常是一个IP地址或IP段:例如:192.168.10.110 或192.168.10.1/24Digest⾃定义,通常是username:BASE64(SHA-1(username:password))例如:zm:sdfndsllndlksfn7c=World只有一个ID :anyoneSuper超级用户
**权限 **指那些通过权限检查后可以被允许执行的操作。在ZooKeeper中,所有对数据的操作权限分为以下五⼤类:
- CREATE(C):数据节点的创建权限,允许授权对象在该数据节点下创建子节点。
- DELETE(D):子节点的删除权限,允许授权对象删除该数据节点的子节点。
- READ(R):数据节点的读取权限,允许授权对象访问该数据节点并读取其数据内容或子节点列表等。
- WRITE(W):数据节点的更新权限,允许授权对象对该数据节点进行更新操作。
- ADMIN(A):数据节点的管理权限,允许授权对象对该数据节点进行 ACL 相关的设置操作。
3.2 Zookeeper命令操作
连接客户端,首先通过cd定位到zk安装目录的bin文件夹下。通过一下命令进行启动。
./zkcli.sh 连接本地的zookeeper服务器./zkCli.sh -server ip:port 连接指定的服务器
对命令进行基本操作。
1、添加节点
语法:create [-s][-e] path data acl
说明: -s可选参数,标识是否为顺序节点;-e可选参数代表临时节点,若不指定,则创建持久节点;acl用来进行权限控制。
例子:create /test helloworld 【创建的是持久节点】
create -e /test2 helloworldTmp 【创建的是临时节点】
2、查看节点
ls命令可以列出Zookeeper指定节点下的所有子节点,但只能查看指定节点下的第一级的所有子节点。
get命令 可以获取Zookeeper指定节点的数据内容和属性信息。
ls2 命令能查看所属路径节点的内容和属性
语法说明例子ls path获取path路径下的节点内容信息ls /testget path获取path下的节点内容get /testls2 path获取path下的节点内容和属性信息ls2 /teststat path获取某个节点的状态信息stat /test
注意:在进行节点读取的时候path目录必须从/跟目录下开始。
3、修改节点
语法:set path data [version]
说明:data代表需要更新的节点内容 version可选参数用来指定节点的版本号,节点是有版本概念的也就是说只能修改指定版本的节点数据。
例子:set /test testtestt 2 可以通过ls2命令行来进行查看。
4、删除节点
语法:delete path [version]
说明:version可选参数删除指定版本的节点,如果不选默认删除最后一个版本的节点内容
例子:delete /test 2
注意:如果删除节点存在子节点,那么⽆法删除。该节点,必须先删除子节点,再删除⽗节点。(不能嵌套删除)。
3.3Zookeeper 原生客户端API使用
1、创建回话
引入POM
创建会话
public class CreateSession implements Watcher {
public static void main(String[] args) throws Exception {
ZooKeeper zooKeeper = new ZooKeeper(
"192.168.159.128:2181", //连接的地址
5000,//连接的超时时间
new CreateSession()); //回调的watcher
System.out.println(zooKeeper.getState());
//对线程进行休眠
TimeUnit.SECONDS.sleep(Integer.MAX_VALUE);
}
@Override
public void process(WatchedEvent watchedEvent) {
if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
System.out.println("zookeeper启动成功!");
}
}
}
注意,Zookeeper客户端和服务端会话的建⽴是一个异步的过程,也就是说在程序中,构造⽅法会在处理完客户端初始化⼯作后⽴即返回,在大多数情况下,此时并没有真正建⽴好一个可用的会话,在会话的生命周期中处于“CONNECTING”的状态。 当该会话真正创建完毕后ZooKeeper服务端会向会话对应的客户端发送一个事件通知,以告知客户端,客户端只有在获取这个通知之后,才算真正建⽴了会话。
2、添加节点
//创建节点
private void createNode() {
try {
zooKeeper.create(
"/test2", //新节点路径
"测试创建节点".getBytes(), //节点内容
ZooDefs.Ids.OPEN_ACL_UNSAFE, //ACL 相当于world任何人都可以访问
CreateMode.PERSISTENT//节点的模式-持久节点
);
//ACL说明
//ZooDefs.Ids.OPEN_ACL_UNSAFE 任何人都有权限
//ZooDefs.Ids.CREATOR_ALL_ACL 只有创建人有权限
//ZooDefs.Ids.READ_ACL_UNSAFE 任何人都有读权限
//节点类型说明
// CreateMode.PERSISTENT 持久节点
// CreateMode.PERSISTENT_SEQUENTIAL 持久顺序节点
// CreateMode.EPHEMERAL 临时节点
// CreateMode.EPHEMERAL_SEQUENTIAL 临时顺序节点
} catch (Exception e) {
e.printStackTrace();
}
}
3、获取节点
@Override
public void process(WatchedEvent watchedEvent) {
try {
if (watchedEvent.getState() == Event.KeeperState.SyncConnected) {
System.out.println("zookeeper启动成功!");
}
//演示下如果给一个节点创建一个新的节点的话会触发process方法
if (watchedEvent.getType() == Event.EventType.NodeChildrenChanged) {
//获取路径下所有子节点信息
List
"/test",//路径
true);//是否开启监听
System.out.println(children);
}
//创建节点
readNode();
} catch (Exception e) {
e.printStackTrace();
}
}
//创建节点
private void readNode() {
try {
//获取单个节点
byte[] data = zooKeeper.getData(
"/test", //路径
true, //是否开启watch监听(当节点发生变动的时候会出发process方法)
null //节点状态信息 null表示获取到最新的节点信息
);
System.out.println(new String(data));
//获取路径下所有子节点信息
List
"/test",//路径
true);//是否开启监听
System.out.println(children);
} catch (Exception e) {
e.printStackTrace();
}
}
4、修改节点
//修改节点内容
private void updateNode() {
try {
zooKeeper.setData(
"/test/01", //修改的路径
"修改节点".getBytes(), //修改的内容
-1); //版本好 -1 代表最新的版本
} catch (Exception e) {
e.printStackTrace();
}
}
5、删除节点
//删除节点内容
private void deleteNode() {
try {
zooKeeper.delete(
"/test/01", //修改的路径
-1); //版本好 -1 代表最新的版本
} catch (Exception e) {
e.printStackTrace();
}
}
3.4开源客户端-ZkClient
1、对节点的操作API
public static void main(String[] args) {
//获取连接 转换成同步的连接方式
ZkClient zkClient = new ZkClient("192.168.159.128:2181");
System.out.println("启动成功");
//添加节点操作【可以进行递归添加】 true表示递归创建
zkClient.createPersistent("/zkClient/01", true);
//删除节点【可以进行递归删除】 true标识递归删除
zkClient.deleteRecursive("/zkClient");
//获取所有子节点
List
System.out.println(children);
//给子节点注册监听事件
zkClient.subscribeChildChanges("/zkClient", new IZkChildListener() {
//s表示parentPath 父节点Path list修改之后的子节点的地址
@Override
public void handleChildChange(String s, List
System.out.println(s);
System.out.println(list);
System.out.println("------");
}
});
//测试监听的事件
zkClient.createPersistent("/zkClient", true);
zkClient.createPersistent("/zkClient/01/011", true);
}
2、对节点数据操作的API
public static void main(String[] args) {
//创建连接
String path = "/zkClientNodeData";
ZkClient zkClient = new ZkClient("192.168.159.128:2181");
//判断指定下的路径是否存在
boolean exists = zkClient.exists(path);
if (!exists) {
//不存在的话创建节点数据【这里不能递归创建】
zkClient.createEphemeral(path,"zkClient节点数据");
}
//注册监听
zkClient.subscribeDataChanges(path, new IZkDataListener() {
//监听内容更新
@Override
public void handleDataChange(String s, Object o) throws Exception {
System.out.println("节点:" + s + "内容发生了变化");
System.out.println(o);
System.out.println("--------");
}
//监听是否被删除
@Override
public void handleDataDeleted(String s) throws Exception {
System.out.println("节点" + s + "被删除了");
}
});
//读取节点内容数据
Object obj = zkClient.readData(path);
System.out.println(obj);
//修改节点内容数据
zkClient.writeData(path,"update");
System.out.println("修改之后" + zkClient.readData(path));
//删除节点
zkClient.delete(path);
}
3.5 Curator客户端
curator是Netflix公司开源的一套Zookeeper客户端框架,和ZKClient一样,Curator解决了很多Zookeeper客户端非常底层的细节开发⼯作,包括连接重连,反复注册Watcher和NodeExistsException异常等,是最流行的Zookeeper客户端之一。从编码风格上来讲,它提供了基于Fluent【类似于构建者模式】的编程⻛格⽀持。
1、创建会话
添加POM
2、其他API
public class CuratorUseDemo {
public static void main(String[] args) throws Exception {
//创建一个失败重试机制 1000ms内最多重试3次
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3);
//方式1创建会话
CuratorFramework client = CuratorFrameworkFactory
.newClient("192.168.159.128:2181",retryPolicy);
client.start();
//方式2创建会话 采用fluent方式来创建会话
CuratorFramework client2 = CuratorFrameworkFactory.builder()
.connectString("192.168.159.128:2181")
.sessionTimeoutMs(50000) //会话超时时间
.connectionTimeoutMs(30000) //连接超时时间
.retryPolicy(retryPolicy)
.namespace("curator") //命名空间 在创建节点的时候都会基于这个创建 /curator
.build();
client2.start();
String path = "/demo/01";
//创建节点
client2.create()
.creatingParentsIfNeeded()//递归创建
.withMode(CreateMode.PERSISTENT) //指定节点类型PERSISTENT 持久节点 EPHEMERAL 临时节点
.forPath(path);
//删除节点
client2.delete()
.deletingChildrenIfNeeded() //递归删除
.forPath(path);
//获取节点数据
Stat stat = new Stat();
byte[] bytes = client2.getData()
.storingStatIn(stat) //保存当前节点的状态信息
.forPath(path);
System.out.println(new String(bytes));
//修改节点
client2.setData()
.withVersion(stat.getVersion()) //指定版本来修改 假如版本不对就抛出异常也可以不用指定版本修改
.forPath(path, "hello".getBytes());
}
}
4.Zookeeper应用场景
4.1数据发布/订阅
1、发布订阅模式介绍
数据发布/订阅(Publish/Subscribe)系统,即所谓的配置中心,顾名思义就是发布者将数据发布到
ZooKeeper的一个或一系列节点上,供订阅者进行数据订阅,进而达到动态获取数据的目的,实现配置
信息的集中式管理和数据的动态更新。
发布/订阅系统一般有两种设计模式,分别是推(Push)模式和拉(Pull)模式。在推模式中,服务端主动将数据更新发送给所有订阅的客户端;而拉模式则是由客户端主动发起请求来获取最新数据,通常客户端都采用定时进行轮询拉取的方式。
2、Zookeeper中如何实现
Zookeeper采用的是推拉相结合的方式:客户端向服务端注册自己需要关注的节点,一旦该节点的数据发生变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。
如果将配置信息存放到Zookeeper上进行集中管理,那么通常情况下,应用在启动的时候都会主动到Zookeeper服务端上进行一次配置信息的获取,同时,在指定节点上注册一个Watcher监听,这样一来,但凡配置信息发生变更,服务端都会实时通知到所有订阅的客户端,从而达到实时获取最新配置信息的目的。
通过一个“配置管理”的实际案例来展示Zookeeper在“数据发布/订阅”场景下的使用方式。
在我们平常的应用系统开发中,经常会碰到这样的需求:系统中需要使用一些通用的配置信息,例如机器列表信息、运行时的开关配置、数据库配置信息等。这些全局配置信息通常具备以下3个特性。
- 数据量通常比较小。
- 数据内容在运行时会发生动态变化。
- 集群中各机器共享,配置一致。
对于这类配置信息,一般的做法通常可以选择将其存储在本地配置文件或是内存变量中。无论采用哪种方式,其实都可以简单地实现配置管理,在集群机器规模步伐、配置变更不是特别频繁的情况下,无论刚刚提到的哪种方式,都能够⾮常方便地解决配置管理的问题。但是,一旦机器规模变大,且配置信息变更越来越频繁后,我们发现依靠现有的这两种方式解决配置管理就变得越来越困难了。我们既希望能够快速地做到全局配置信息的变更,同时希望变更成本足够小,因此我们必须寻求一种更为分布式化的解决方案。
案例:“数据库切换”。
- 配置存储:在进行配置管理之前,首先我们需要将初始化配置信息存储到Zookeeper上去,一般情况下,我们可以在Zookeeper上选取一个数据节点用于配置信息的存储,例如:/app1/database_config
[图片上传失败...(image-690231-1612341123588)]
我们将需要管理的配置信息写入到该数据节点中去,例如:
数据库配置信息#DBCPdbcp.driverClassName=com.mysql.jdbc.Driverdbcp.dbJDBCUrl=jdbc:mysql://127.0.0.1:3306/ testdbcp.username=rootdbcp.password=rootdbcp.maxActive=30dbcp.maxIdle=10
- 配置获取:集群中每台机器在启动初始化阶段,首先会从上面提到的Zookeeper配置节点上读取数据库信息,同时,客户端还需要在该配置节点上注册一个数据变更的 Watcher监听,一旦发生节点数据变更,所有订阅的客户端都能够获取到数据变更通知。
- 配置变更:在系统运行过程中,可能会出现需要进行数据库切换的情况,这个时候就需要进行配置变更。借助Zookeeper,我们只需要对Zookeeper上配置节点的内容进行更新,Zookeeper就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进行最新数据的获取。
4.2命名服务
命名服务(Name Service):在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等——这些我们都可以统称它们为名字(Name),其中较为常见的就是一些分布式服务框架(如RPC、RMI)中的服务地址列表,通过使用命名服务,客户端应用能够根据指定名字来获取资源的实体、服务地址和提供者的信息等。
Zookeeper提供的命名服务功能能够帮助应用系统通过一个资源引用的方式来实现对资源的定位与使用。另外,广义上命名服务的资源定位都不是真正意义的实体资源——在分布式环境中,上层应用仅仅需要一个全局唯一的名字,类似于数据库中的唯一主键。
在我们熟悉的关系型数据库中,各个表都需要一个主键来唯一标识每条数据库记录,这个主键就是这样的唯一ID。在过去的单库单表型系统中,通常可以使用数据库字段自带的auto_increment属性来⾃动为每条数据库记录生成一个唯一的ID,数据库会保证生成的这个ID在全局唯一。但是随着数据库数据规模的不断增⼤,分库分表随之出现,而auto_increment属性仅能针对单一表中的记录⾃动生成ID,因此在这种情况下,就⽆法再依靠数据库的auto_increment属性来唯一标识一条记录了。于是,我们必须寻求一种能够在分布式环境下生成全局唯一ID的⽅法。
全局唯一ID可以用UUID来实现,但是UUID也存在缺点:
1、长度过长:UUID 最大的问题就在于生成的字符串过长。显然,和数据库中的 INT 类型相比,存储一个UUID需要花费更多的空间。
2、含义不明:随机生成的uuid不能表达一个明确的含义。
我们使用Zookeeper来生成一个全局唯一ID。
说明:对于一个任务列表的主键,使用ZooKeeper生成唯一ID的基本步骤:
1、所有客户端都会根据自己的任务类型,在指定类型的任务下面通过调用create()接⼝来创建一个顺序节点,例如创建“job-”节点。
2、节点创建完毕后,create()接⼝会返回一个完整的节点名,例如“job-0000000003”。
3、客户端拿到这个返回值后,拼接上 type 类型,例如“type2-job-0000000003”,这就可以作为一个
全局唯一的ID了。
总结: 在ZooKeeper中,每一个数据节点都能够维护一份子节点的顺序,当客户端对其创建一个顺序子节点的时候 ZooKeeper 会⾃动以后缀的形式在其子节点上添加一个序号,在这个场景中就是利用了ZooKeeper的这个特性。
4.3集群管理
随着分布式系统规模的日益扩大,集群中的机器规模也随之变大,那如何更好地进行集群管理也显得越来越重要了。所谓集群管理,包括集群监控与集群控制两⼤块,集群监控侧重对集群运行时状态的收集,集群控制则是对集群进行操作与控制。
在日常的运维过程中,我们可能会收到类似这样的需求:如何快速的统计出当前生产环境下一共有多少台机器;如何快速的获取到机器上下线的情况;如何实时监控集群中每台主机的运行时状态;
1、传统的解决方案以及问题分析
在传统的基于Agent的分布式集群管理体系中,都是通过在集群中的每台机器上部署一个 Agent,由这个 Agent 负责主动向指定的一个监控中心系统(监控中心系统负责将所有数据进行集中处理,形成一系列报表,并负责实时报警,以下简称“监控中心”)汇报自己所在机器的状态。在集群规模适中的场景下,这确实是一种在生产实践中广泛使用的解决⽅案,能够快速有效地实现分布式环境集群监控,但是一旦系统的业务场景增多,集群规模变大之后,该解决⽅案的弊端也就显现出来了。
大规模升级困难:以客户端形式存在的 Agent,在⼤规模使用后,一旦遇上需要⼤规模升级的情况,就非常麻烦,在升级成本和升级进度的控制上面临巨大的挑战。
统一的Agent无法满足多样的需求:对于机器的CPU使用率、负载(Load)、内存使用率、网络吞吐以及磁盘容量等机器基本的物理状态,使用统一的Agent来进行监控或许都可以满⾜。但是,如果需要深入应用内部,对一些业务状态进行监控,例如,在一个分布式消息中间件中,希望监控到每个消费者对消息的消费状态;或者在一个分布式任务调度系统中,需要对每个机器上任务的执行情况进行监控。很显然,对于这些业务耦合紧密的监控需求,不适合由一个统一的Agent来提供。
编程语言多样性:随着越来越多编程语言的出现,各种异构系统层出不穷。如果使用传统的Agent⽅式,那么需要提供各种语言的 Agent 客户端。另⼀⽅面,“监控中心”在对异构系统的数据进行整合上面临巨⼤挑战。
2、Zookeeper解决方案
Zookeeper两大特性:
1.客户端如果对Zookeeper的数据节点注册Watcher监听,那么当该数据节点的内容或是其子节点
列表发生变更时,Zookeeper服务器就会向订阅的客户端发送变更通知。
2.对在Zookeeper上创建的临时节点,⼀旦客户端与服务器之间的会话失效,那么临时节点也会被⾃动删除。
利用其两⼤特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册⼀个Watcher监听,那么但凡进行动态添加机器的操作,就会在/clusterServers节点下创建⼀个临时节点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。
通过分布式⽇志收集系统来进行解析:
分布式日志收集系统
分布式日志收集系统的核心工作就是收集分布在不同机器上的系统⽇志,在这里我们重点来看分布式日志系统(以下简称“日志系统”)的收集器模块。
在⼀个典型的日志系统的架构设计中,整个日志系统会把所有需要收集的日志机器(我们以“日志源机器”代表此类机器)分为多个组别,每个组别对应⼀个收集器,这个收集器其实就是⼀个后台机器(我们以“收集器机器”代表此类机器),用于收集日志对于大规模的分布式日志收集系统场景,通常需要解决两个问题:
变化的日志源机器:在生产环境中,伴随着机器的变动,每个应用的机器几乎每天都是在变化的(机器硬件问题、扩容、机房迁移或是网络问题等都会导致⼀个应用的机器变化),也就是说每个组别中的⽇志源机器通常是在不断变化的。
变化的收集器机器:日志收集系统自身也会有机器的变更或扩容,于是会出现新的收集器机器加入或是老的收集器机器退出的情况。
无论是日志源机器还是收集器机器的变更,最终都可以归结为如何快速、合理、动态地为每个收集器分配对应的⽇志源机器。这也成为了整个⽇志系统正确稳定运转的前提,也是⽇志收集过程中最⼤的技术挑战之⼀,在这种情况下,我们就可以引入zookeeper了,下⾯我们就来看ZooKeeper在这个场景中的使用。
① 注册收集器机器:使用Zookeeper来进行⽇志系统收集器的注册,典型做法是在Zookeeper上创建⼀节点作为收集器的根节点,例如/logs/collector(下⽂我们以“收集器节点”代表该数据节点),每个收集器机器启动的时候,都会在收集器节点下创建⾃⼰的节点,例如/logs/collector/[Hostname]
② 任务分发:待所有收集器机器都创建好⾃⼰对应的节点后,系统根据收集器节点下子节点的个数,将所有日志源机器分成对应的若干组,然后将分组后的机器列表分别写到这些收集器机器创建的子节点(例如/logs/collector/host1)上去。这样⼀来,每个收集器机器都能够从⾃⼰对应的收集器节点上获取⽇志源机器列表,进而开始进行⽇志收集⼯作。
③ 状态汇报:完成收集器机器的注册以及任务分发后,我们还要考虑到这些机器随时都有挂掉的可能。因此,针对这个问题,我们需要有⼀个收集器的状态汇报机制:每个收集器机器在创建完⾃⼰的专属节点后,还需要在对应的子节点上创建⼀个状态子节点,例如/logs/collector/host1/status,每个收集器机器都需要定期向该节点写入⾃⼰的状态信息。我们可以把这种策略看作是⼀种心跳检测机制,通常收集器机器都会在这个节点中写入⽇志收集进度信息。⽇志系统根据该状态子节点的最后更新时间来判断对应的收集器机器是否存活。
④ 动态分配:如果收集器机器挂掉或是扩容了,就需要动态地进行收集任务的分配。在运行过程中,⽇志系统始终关注着/logs/collector这个节点下所有子节点的变更,⼀旦检测到有收集器机器停⽌汇报或是有新的收集器机器加入,就要开始进行任务的重新分配。⽆论是针对收集器机器停⽌汇报还是新机器加入的情况,⽇志系统都需要将之前分配给该收集器的所有任务进行转移。
为了解决这个问题,通常有两种做法:
全局动态分配:这是⼀种简单粗暴的做法,在出现收集器机器挂掉或是新机器加入的时候,⽇志系统需要根据新的收集器机器列表,⽴即对所有的⽇志源机器重新进行⼀次分组,然后将其分配给剩下的收集器机器。
局部动态分布:全局动态分配⽅式虽然策略简单,但是存在⼀个问题:⼀个或部分收集器机器的变更,就会导致全局动态任务的分配,影响⾯⽐较⼤,因此⻛险也就⽐较⼤。所谓局部动态分配,顾名思义就是在⼩范围内进行任务的动态分配。在这种策略中,每个收集器机器在汇报⾃⼰⽇志收集状态的同时,也会把⾃⼰的负载汇报上去。请注意,这⾥提到的负载并不仅仅只是简单地指机器CPU负载(Load),而是⼀个对当前收集器任务执行的综合评估,这个评估算法和ZooKeeper本身并没有太大的关系,这里不再赘述。
在这种策略中,如果⼀个收集器机器挂了,那么⽇志系统就会把之前分配给这个机器的任务重新分配到
那些负载较低的机器上去。同样,如果有新的收集器机器加入,会从那些负载⾼的机器上转移部分任务给这个新加入的机器。
注意事项:
1、节点类型:在/logs/collector节点下创建临时节点可以很好的判断机器是否存活,但是,若机器挂了,其节点会被删除,记录在节点上的⽇志源机器列表也被清除,所以需要选择持久节点来标识每⼀台机器,同时在节点下分别创建/logs/collector/[Hostname]/status节点来表征每⼀个收集器机器的状态,这样,既能实现对所有机器的监控,同时机器挂掉后,依然能够将分配任务还原。
2、日志系统节点监听:若采用Watcher机制,那么通知的消息量的网络开销⾮常⼤,需要采用⽇志系统主动轮询收集器节点的策略,这样可以节省网络流量,但是存在⼀定的延时。
4.4Master选举
Master选举是⼀个在分布式系统中⾮常常⻅的应用场景。分布式最核心的特性就是能够将具有独⽴计算能⼒的系统单元部署在不同的机器上,构成⼀个完整的分布式系统。而与此同时,实际场景中往往也需要在这些分布在不同机器上的独立系统单元中选出⼀个所谓的“⽼⼤”,在计算机中,我们称之为Master。
在分布式系统中,Master往往用来协调集群中其他系统单元,具有对分布式系统状态变更的决定权。例如,在⼀些读写分离的应用场景中,客户端的写请求往往是由Master来处理的;而在另⼀些场景中,Master则常常负责处理⼀些复杂的逻辑,并将处理结果同步给集群中其他系统单元。Master选举可以说是ZooKeeper最典型的应用场景了,接下来,我们就结合“⼀种海量数据处理与共享模型”这个具体例子来看看 ZooKeeper在集群Master选举中的应用场景。
在分布式环境中,经常会碰到这样的应用场景:集群中的所有系统单元需要对前端业务提供数据,⽐如⼀个商品 ID,或者是⼀个网站轮播广告的广告 ID(通常出现在⼀些广告投放系统中)等,而这些商品ID或是广告ID往往需要从⼀系列的海量数据处理中计算得到——这通常是⼀个⾮常耗费 I/O 和 CPU资源的过程。鉴于该计算过程的复杂性,如果让集群中的所有机器都执行这个计算逻辑的话,那么将耗费⾮常多的资源。⼀种⽐较好的⽅法就是只让集群中的部分,甚⾄只让其中的⼀台机器去处理数据计算,⼀旦计算出数据结果,就可以共享给整个集群中的其他所有客户端机器,这样可以⼤⼤减少重复劳动,提升性能。 这⾥我们以⼀个简单的广告投放系统后台场景为例来讲解这个模型。
整个系统⼤体上可以分成客户端集群、分布式缓存系统、海量数据处理总线和 ZooKeeper四个部分。
首先我们来看整个系统的运行机制。图中的Client集群每天定时会通过ZooKeeper来实现Master选举。选举产生Master客户端之后,这个Master就会负责进行⼀系列的海量数据处理,最终计算得到⼀个数据结果,并将其放置在⼀个内存/数据库中。同时,Master还需要通知集群中其他所有的客户端从这个内存/数据库中共享计算结果。
利用ZooKeeper的强⼀致性,能够很好保证在分布式⾼并发情况下节点的创建⼀定能够保证全局唯⼀性,即ZooKeeper将会保证客户端⽆法重复创建⼀个已经存在的数据节点。也就是说,如果同时有多个客户端请求创建同⼀个节点,那么最终⼀定只有⼀个客户端请求能够创建成功。利用这个特性,就能很容易地在分布式环境中进行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分布式锁
简介:分布式锁是控制分布式系统之间同步访问共享资源的一种方式。如果不同的系统或是同⼀个系统的不同
主机之间共享了⼀个或⼀组资源,那么访问这些资源的时候,往往需要通过⼀些互斥⼿段来防⽌彼此之间的⼲扰,以保证⼀致性,在这种情况下,就需要使用分布式锁了。
1、排他锁
简介:排他锁(Exclusive Locks,简称 X 锁),⼜称为写锁或独占锁,是⼀种基本的锁类型。如果事务 T1对数据对象 O1加上了排他锁,那么在整个加锁期间,只允许事务 T1对 O1进行读取和更新操作,其他任何事务都不能再对这个数据对象进行任何类型的操作——直到T1释放了排他锁。(相当于悲观锁)
实现:
① 定义锁: ZooKeeper上的数据节点来表示⼀个锁,例如/exclusive_lock/lock节点就可以被定义为⼀个锁,如图:
② 获取锁:在需要获取锁时,所有的客户端都会试图通过调用 create()接口,在/exclusive_lock节点下创建临时子节点/exclusive_lock/lock。在前⾯,我们也介绍了,ZooKeeper 会保证在所有的客户端中,最终只有⼀个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock 节点上注册⼀个子节点变更的Watcher监听,以便实时监听到lock节点的变更情况。
③释放锁: 有两种情况可以释放锁:1、当前获取锁的客户端机器发生宕机,那么ZooKeeper上的这个临时节点就会被移除。2、正常执行完业务逻辑后,客户端就会主动将⾃⼰创建的临时节点删除。 无论在什么情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了子节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程。
2、共享锁
介绍:共享锁(Shared Locks,简称S锁),⼜称为读锁,同样是⼀种基本的锁类型。如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进行读取操作,其他事务也只能对这个数据对象加共享锁,直到该数据对象上的所有共享锁都被释放。
共享锁和排他锁最根本的区别在于,加上排他锁后,数据对象只对⼀个事务可见,而加上共享锁后,数据对所有事务都可⻅。
实现:①定义锁 和排他锁⼀样,同样是通过 ZooKeeper 上的数据节点来表示⼀个锁,是⼀个类似于“/shared_lock/[Hostname]-请求类型-序号”的临时顺序节点,例如/shared_lock/host1-R-0000000001,那么,这个节点就代表了⼀个共享锁,如图所示:
[图片上传失败...(image-b785b1-1612341123587)]
② 获取锁:在需要获取共享锁时,所有客户端都会到/shared_lock 这个节点下⾯创建⼀个临时顺序节点,如果当前是读请求,那么就创建例如/shared_lock/host1-R-0000000001的节点;如果是写请求,那么就创建例
如/shared_lock/host2-W-0000000002的节点。
判断读写顺序:通过Zookeeper来确定分布式读写顺序,⼤致分为四步
1. 创建完节点后,获取/shared_lock节点下所有子节点,并对该节点变更注册监听。2. 确定⾃⼰的节点序号在所有子节点中的顺序。3. 对于读请求:若没有⽐⾃⼰序号⼩的子节点或所有⽐⾃⼰序号⼩的子节点都是读请求,那么表明⾃⼰已经成功获取到共享锁,同时开始执行读取逻辑,若有写请求,则需要等待。对于写请求:若⾃⼰不是序号最⼩的子节点,那么需要等待。4. 接收到Watcher通知后,重复步骤1
③释放锁:其释放锁的流程与排他锁⼀致。
3、羊群效应
简介:在共享锁的实现上我们着重来看上⾯“判断读写顺序”过程的步骤3,结合下⾯的图,看看实际运行中的情况。
针对如上图所示的情况进行分析:
1. host1首先进行读操作,完成后将节点/shared_lock/host1-R-00000001删除。
2. 余下4台机器均收到这个节点移除的通知,然后重新从/shared_lock节点上获取⼀份新的子节点列表。
3. 每台机器判断⾃⼰的读写顺序,其中host2检测到⾃⼰序号最⼩,于是进行写操作,余下的机器则继续等待。
4.继续第一步
可以看到,host1客户端在移除⾃⼰的共享锁后,Zookeeper发送了子节点更变Watcher通知给所有机器,然而除了给host2产生影响外,对其他机器没有任何作用。⼤量的Watcher通知和子节点列表获取两个操作会重复运行,这样不仅会对zookeeper服务器造成巨⼤的性能影响影响和网络开销,更为严重的是,如果同⼀时间有多个节点对应的客户端完成事务或是事务中断引起节点消失,ZooKeeper服务器就会在短时间内向其余客户端发送⼤量的事件通知,这就是所谓的羊群效应。
上⾯这个ZooKeeper分布式共享锁实现中出现⽺群效应的根源在于,没有找准客户端真正的关注点。我们再来回顾⼀下上⾯的分布式锁竞争过程,它的核心逻辑在于:判断⾃⼰是否是所有子节点中序号最⼩的。于是,很容易可以联想到,每个节点对应的客户端只需要关注⽐⾃⼰序号⼩的那个相关节点的变更情况就可以了——而不需要关注全局的子列表变更情况。
改进后的分布式锁实现:
首先,我们需要肯定的⼀点是,上⾯提到的共享锁实现,从整体思路上来说完全正确。这⾥主要的改动在于:每个锁竞争者,只需要关注/shared_lock节点下序号⽐⾃⼰⼩的那个节点是否存在即可,具体实现如下。
1. 客户端调用create接⼝常⻅类似于/shared_lock/[Hostname]-请求类型-序号的临时顺序节点。
2. 客户端调用getChildren接⼝获取所有已经创建的子节点列表(不注册任何Watcher)。
3. 如果无法获取共享锁,就调用exist接⼝来对⽐⾃⼰⼩的节点注册Watcher。对于读请求:向⽐⾃⼰
序号⼩的最后⼀个写请求节点注册Watcher监听。对于写请求:向⽐⾃⼰序号⼩的最后⼀个节点注册Watcher监听。
4. 等待Watcher通知,继续进入步骤2。
此⽅案改动主要在于:每个锁竞争者,只需要关注/shared_lock节点下序号⽐⾃⼰⼩的那个节点是否存在即可。
注意:在多线程并发编程实践中,我们会去尽量缩⼩锁的范围——对于分布式锁实现的改进其实也是同样的思路。那么对于开发⼈员来说,是否必须按照改进后的思路来设计实现⾃⼰的分布式锁呢?答案是否定的。在具体的实际开发过程中,我们提倡根据具体的业务场景和集群规模来选择适合⾃⼰的分布式锁实现:在集群规模不大、网络资源丰富的情况下,第⼀种分布式锁实现方式是简单实用的选择;而如果集群规模达到⼀定程度,并且希望能够精细化地控制分布式锁机制,那么就可以试试改进版的分布式锁实现。
4.6分布式队列
简介:分布式队列可以简单分为两⼤类:⼀种是常规的FIFO先进先出队列模型,还有⼀种是 等待队列元素聚集后统⼀安排处理执行的Barrier模型。
① FIFO先入先出
FIFO(First Input First Output,先入先出), FIFO 队列是⼀种⾮常典型且应用广泛的按序执行的队列模型:先进入队列的请求操作先完成后,才会开始处理后⾯的请求。
实现思路:使用ZooKeeper实现FIFO队列,和之前提到的共享锁的实现⾮常类似。FIFO队列就类似于⼀个全写的共享锁模型,⼤体的设计思路其实⾮常简单:所有客户端都会到/queue_fifo 这个节点下⾯创建⼀个临时顺序节点,例如如/queue_fifo/host1-00000001。
创建完节点后,根据如下4个步骤来确定执行顺序。
1. 通过调用getChildren接⼝来获取/queue_fifo节点的所有子节点,即获取队列中所有的元素。
2. 确定自己的节点序号在所有子节点中的顺序。
3. 如果自己的序号不是最⼩,那么需要等待,同时向⽐⾃⼰序号⼩的最后⼀个节点注册Watcher监听。
4. 接收到Watcher通知后,重复步骤1。
② Barrier:分布式屏障
Barrier在分布式系统中,特指系统之间的⼀个协调条件,规定了⼀个队列的元素必须都集聚后才能统⼀进行安排,否则⼀直等待。这往往出现在那些大规模分布式并行计算的应用场景上:最终的合并计算需要基于很多并行计算的子结果来进行。这些队列其实是在 FIFO 队列的基础上进行了增强,⼤致的设计思想如下:开始时,/queue_barrier 节点是⼀个已经存在的默认节点,并且将其节点的数据内容赋值为⼀个数字n来代表Barrier值,例如n=10表示只有当/queue_barrier节点下的子节点个数达到10后,才会打开Barrier。之后,所有的客户端都会到/queue_barrie节点下创建⼀个临时节点,例如/queue_barrier/host1,如图所示。
创建完节点后,按照如下步骤执行。
1. 通过调用getData接⼝获取/queue_barrier节点的数据内容:10。
2. 通过调用getChildren接⼝获取/queue_barrier节点下的所有子节点,同时注册对子节点变更的Watcher监听。
3. 统计子节点的个数。
4. 如果子节点个数还不⾜10个,那么需要等待。
5. 接受到Wacher通知后,重复步骤2
5. Zookeeper深入进阶
5.1 ZAB协议
1、相关概念:
在深入了解zookeeper之前,zookeeper是paxos算法的⼀个实现,但事实上,zookeeper并没有完全采用paxos算法,而是使用了⼀种称为Zookeeper Atomic Broadcast(ZAB,Zookeeper原子消息广播协议)的协议作为其数据⼀致性的核心算法。
ZAB协议并不像Paxos算法那样是⼀种通用的分布式⼀致性算法,它是⼀种特别为zookeeper专门设计
的⼀种⽀持崩溃恢复的原子广播协议
在zookeeper中,主要就是依赖ZAB协议来实现分布式数据的⼀致性,基于该协议,Zookeeper实现了⼀种主备模式的系统架构来保持集群中各副本之间的数据的⼀致性,表现形式就是使用⼀个单⼀的主进程来接收并处理客户端的所有事务请求,并采用ZAB的原子广播协议,将服务器数据的状态变更以事务Proposal的形式广播到所有的副本进程中,ZAB协议的主备模型架构保证了同⼀时刻集群中只能够有⼀个主进程来广播服务器的状态变更,因此能够很好地处理客户端⼤量的并发请求。但是,也要考虑到主进程在任何时候都有可能出现崩溃退出或重启现象,因此,ZAB协议还需要做到当前主进程当出现异常情况的时候,依旧能正常⼯作。
2、ZAB核心
ZAB协议的核心是定义了对于那些会改变Zookeeper服务器数据状态的事务请求的处理⽅式。
即:所有事务请求必须由⼀个全局唯⼀的服务器来协调处理,这样的服务器被称为Leader服务器,余下的服务器则称为Follower服务器,Leader服务器负责将⼀个客户端事务请求转化成⼀个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器,之后Leader服务器需要等待所有Follower服务器的反馈,⼀旦超过半数的Follower服务器进行了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前⼀个Proposal进行提交。
[图片上传失败...(image-b79de6-1612341123587)]
3、ZAB协议介绍
ZAB协议包括两种基本的模式:消息广播和崩溃恢复
(1)消息广播模式
当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进
入消息广播模式,当⼀台同样遵守ZAB协议的服务器启动后加入到集群中,如果此时集群中已经存在⼀个Leader服务器在负责进行消息广播,那么加入的服务器就会自觉地进入数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后⼀起参与到消息广播流程中去。Zookeeper只允许唯⼀的⼀个Leader服务器来进行事务请求的处理,Leader服务器在接收到客户端的事务请求后,会生成对应的事务提议并发起⼀轮广播协议,而如果集群中的其他机器收到客户端的事务请求后,那么这些⾮Leader服务器会首先将这个事务请求转发给Leader服务器。
具体过程
ZAB协议的消息广播过程使用原子广播协议,类似于⼀个⼆阶段提交过程,针对客户端的事务请求,Leader服务器会为其生成对应的事务Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各自的选票,最后进行事务提交。
[图片上传失败...(image-47c006-1612341123587)]
在ZAB的⼆阶段提交过程中,移除了中断逻辑,所有的Follower服务器要么正常反馈Leader提出的事务Proposal,要么就抛弃Leader服务器,同时,ZAB协议将⼆阶段提交中的中断逻辑移除意味着我们可以在过半的Follower服务器(已经返回ACK)可以开始提交事务Proposal了,而不需要等待集群中所有的Follower服务器都反馈响应,但是,在这种简化的⼆阶段提交模型下,⽆法处理因Leader服务器崩溃退出而带来的数据不⼀致问题,因此ZAB采用了崩溃恢复模式来解决此问题。
整个消息广播协议是基于具有FIFO特性的TCP协议来进行网络通信的,因此能够很容易保证消息广播过程中消息接受与发送的顺序性。
在整个消息广播过程中,Leader服务器会为每个事务请求⽣成对应的Proposal来进行广播,并且在广播事务Proposal之前,Leader服务器会首先为这个事务Proposal分配⼀个全局单调递增的唯⼀ID,称之为事务ID(ZXID),由于ZAB协议需要保证每个消息严格的因果关系,因此必须将每个事务Proposal按照其ZXID的先后顺序来进行排序和处理。
具体的过程:在消息广播过程中,Leader服务器会为每⼀个Follower服务器都各自分配⼀个单独的队列,然后将需要广播的事务 Proposal 依次放⼊这些队列中去,并且根据 FIFO策略进行消息发送。每⼀个Follower服务器在接收到这个事务Proposal之后,都会首先将其以事务日志的形式写入到本地磁盘中去,并且在成功写⼊后反馈给Leader服务器⼀个Ack响应。当Leader服务器接收到超过半数Follower的Ack响应后,就会广播⼀个Commit消息给所有的Follower服务器以通知其进行事务提交,同时Leader自身也会完成对事务的提交,而每⼀个Follower服务器在接收到Commit消息后,也会完成对事务的提交。
(2)崩溃恢复模式
ZAB协议的这个基于原⼦⼴播协议的消息⼴播过程,在正常情况下运⾏⾮常良好,但是⼀旦在Leader服务器出现崩溃,或者由于⽹络原因导致Leader服务器失去了与过半Follower的联系,那么就会进⼊崩溃恢复模式。
在ZAB协议中,为了保证程序的正确运⾏,整个恢复过程结束后需要选举出⼀个新的Leader服务器,因此,ZAB协议需要⼀个⾼效且可靠的Leader选举算法,从⽽保证能够快速地选举出新的Leader,同时,Leader选举算法不仅仅需要让Leader⾃身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产⽣出来的新Leader服务器。
具体过程
ZAB协议规定了如果⼀个事务Proposal在⼀台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。接下来我们看看在崩溃恢复过程中,可能会出现的两个数据不⼀致性的隐患及针对这些情况ZAB协议所需要保证的特性。
ZAB协议需要确保已经在Leader服务器上提交的事务最终被所有服务器都提交。
假设⼀个事务在 Leader 服务器上被提交了,并且已经得到过半 Folower服务器的Ack反馈,但是在Leadrer服务器将Commit消息发送给所有Follower机器之前,宕机了。
场景说明:
图中的消息C2就是⼀个典型的例⼦:在集群正常运⾏过程中的某⼀个时刻,Server1 是 Leader 服务器,其先后⼴播了消息 P1、P2、C1、P3 和 C2,其中,当Leader服务器将消息C2发出后就⽴即崩溃退出了。针对这种情况,ZAB协议就需要确保事务Proposal2最终能够在所有的服务器上都被提交成功,否则将出现不⼀致。
ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务。
如果在崩溃恢复过程中出现⼀个需要被丢弃的提案,那么在崩溃恢复结束后需要跳过该事务Proposal。
[图片上传失败...(image-a8f841-1612341123587)]
在图所示的集群中,假设初始的Leader服务器Server1在提出了⼀个事务P3之后就崩溃退出了,从⽽导致集群中的其他服务器都没有收到这个事务P3。于是,当 Server1 恢复过来再次加⼊到集群中的时候,ZAB 协议需要确保丢弃P3这个事务。
结合上面提到的这两个崩溃恢复过程中需要处理的特殊情况,就决定了ZAB协议必须设计一个能够确保提交已经被 Leader 提交的事务 Proposal,同时丢弃已经被跳过的事务Proposal的Leader算法。针对这个要求,如果让Leader选举算法能够保证新选举出来的Leader服务器拥有集群中所有机器最⾼编号(即ZXID最⼤)的事务Proposal,那么就可以保证这个新选举出来的Leader⼀定具有所有已经提交的提案。更为重要的是,如果让具有最⾼编号事务Proposal 的机器来成为 Leader,就可以省去 Leader 服务器检查Proposal的提交和丢弃⼯作的这⼀步操作了。
数据同步
完成Leader选举之后,在进入消息广播模式之前,Leader服务器会⾸先确认事务⽇志中的所有Proposal是否都已经被集群中过半的机器提交了,即是否完成数据同步。来具体了解下ZAB协议数据同步过程。
所有正常运⾏的服务器,要么成为 Leader,要么成为 Follower 并和 Leader 保持同步。Leader服务器需要确保所有的Follower服务器能够接收到每⼀条事务Proposal,并且能够正确地将所有已经提交了的事务Proposal应⽤到内存数据库中去。
具体的做法是Leader服务器会为每⼀个Follower服务器都准备⼀个队列,并将那些没有被各Follower服务器同步的事务以Proposal消息的形式逐个发送给Follower服务器,并在每⼀个Proposal消息后⾯紧接着再发送⼀个Commit消息,以表示该事务已经被提交。等到Follower服务器将所有其尚未同步的事务 Proposal 都从 Leader 服务器上同步过来并成功应⽤到本地数据库中后,Leader服务器就会将该Follower服务器加⼊到真正的可⽤Follower列表中。
4、运行时状态分析
在ZAB协议的设计中,每个进程都有可能处于如下三种状态之⼀
- LOOKING:Leader选举阶段。
- FOLLOWING:Follower服务器和Leader服务器保持同步状态。
- LEADING:Leader服务器作为主进程领导状态。
所有进程初始状态都是LOOKING状态,此时不存在Leader,接下来,进程会试图选举出⼀个新的Leader,之后,如果进程发现已经选举出新的Leader了,那么它就会切换到FOLLOWING状态,并开始和Leader保持同步,处于FOLLOWING状态的进程称为Follower,LEADING状态的进程称为Leader,当Leader崩溃或放弃领导地位时,其余的Follower进程就会转换到LOOKING状态开始新⼀轮的Leader选举。
⼀个Follower只能和⼀个Leader保持同步,Leader进程和所有的Follower进程之间都通过⼼跳检测机制来感知彼此的情况。若Leader能够在超时时间内正常收到⼼跳检测,那么Follower就会⼀直与该Leader保持连接,⽽如果在指定时间内Leader⽆法从过半的Follower进程那⾥接收到⼼跳检测,或者TCP连接断开,那么Leader会放弃当前周期的领导,并转换到LOOKING状态,其他的Follower也会选择放弃这个Leader,同时转换到LOOKING状态,之后会进⾏新⼀轮的Leader选举
5、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 服务器角色
Leader
Leader服务器是Zookeeper集群工作的核心,其主要⼯作有以下两个:
(1) 事务请求的唯⼀调度和处理者,保证集群事务处理的顺序性。
(2) 集群内部各服务器的调度者。
请求处理链
使⽤责任链来处理每个客户端的请求是Zookeeper的特⾊,Leader服务器的请求处理链如下:
[图片上传失败...(image-3ea5e5-1612341123587)]
从prepRequestProcessor到FinalRequestProcessor前后⼀共7个请求处理器组成了Leader服务器的请求处理链。
(1) PrepRequestProcessor。请求预处理器,也是leader服务器中的第⼀个请求处理器。在Zookeeper中, PrepRequestProcessor能够识别出当前客户端请求是否是事务请求。对于事务请求,PrepRequestProcessor处理器会对其进行一系列预处理,如创建请求事务头、事务体、会话检查、ACL检查和版本检查等。
(2) ProposalRequestProcessor。事务投票处理器。也是Leader服务器事务处理流程的发起者,对于⾮事务性请求,ProposalRequestProcessor会直接将请求转发到CommitProcessor处理器,而对于事务性请求,处理将请求转发到CommitProcessor外,还会根据请求类型创建对应的Proposal提议,并发送给所有的Follower服务器来发起⼀次集群内的事务投票。同时,ProposalRequestProcessor还会将事务请求交付给SyncRequestProcessor进⾏事务⽇志的记录。
(3) SyncRequestProcessor。事务⽇志记录处理器。⽤来将事务请求记录到事务⽇志⽂件中,同时会触发Zookeeper进⾏数据快照。
(4) AckRequestProcessor。负责在SyncRequestProcessor完成事务⽇志记录后,向Proposal的投票收集器发送ACK反馈,以通知投票收集器当前服务器已经完成了对该Proposal的事务⽇志记录。
(5) CommitProcessor。事务提交处理器。对于⾮事务请求,该处理器会直接将其交付给下⼀级处理器处理;对于事务请求,其会等待集群内 针对Proposal的投票直到该Proposal可被提交,利⽤CommitProcessor,每个服务器都可以很好地控制对事务请求的顺序处理。
(6) ToBeCommitProcessor。该处理器有⼀个toBeApplied队列,用来存储那些已经被CommitProcessor处理过的可被提交的Proposal。其会将这些请求交付给FinalRequestProcessor处理器处理,待其处理完后,再将其从toBeApplied队列中移除。
(7) FinalRequestProcessor。⽤来进⾏客户端请求返回之前的操作,包括创建客户端请求的响应,针对事务请求,该处理器还会负责将事务应⽤到内存数据库中。
Follower
Follower服务器是Zookeeper集群状态中的跟随者,其主要⼯作有以下三个:
(1) 处理客户端⾮事务性请求(读取数据),转发事务请求给Leader服务器。
(2) 参与事务请求Proposal的投票。
(3) 参与Leader选举投票。
和leader⼀样,Follower也采⽤了责任链模式组装的请求处理链来处理每⼀个客户端请求,由于不
需要对事务请求的投票处理,因此Follower的请求处理链会相对简单,其处理链如下
[图片上传失败...(image-2b2e2c-1612341123587)]
跟Leader 服务器的请求处理链最⼤的不同点在于,Follower 服务器的第⼀个处理器换成了FollowerRequestProcessor处理器,同时由于不需要处理事务请求的投票,因此也没有了ProposalRequestProcessor处理器。
(1) FollowerRequestProcessor:其⽤作识别当前请求是否是事务请求,若是,那么Follower就会将该请求转发给Leader服务器,Leader服务器在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进行处理。
(2) SendAckRequestProcessor其承担了事务⽇志记录反馈的角色,在完成事务⽇志记录后,会向Leader服务器发送ACK消息以表明⾃身完成了事务⽇志的记录⼯作。
Observer
Observer是ZooKeeper⾃3.3.0版本开始引⼊的⼀个全新的服务器角色。从字⾯意思看,该服务器充当
了⼀个观察者的角色——其观察ZooKeeper集群的最新状态变化并将这些状态变更同步过来。
Observer服务器在⼯作原理上和Follower基本是⼀致的,对于⾮事务请求,都可以进⾏独⽴的处理,⽽
对于事务请求,则会转发给Leader服务器进⾏处理。和Follower唯⼀的区别在于,Observer不参与任何形式的投票,包括事务请求Proposal的投票和Leader选举投票。简单地讲,Observer服务器只提供⾮事务服务,通常⽤于在不影响集群事务处理能⼒的前提下提升集群的⾮事务处理能⼒。
Observer的请求处理链路和Follower服务器也⾮常相近,其处理链如下
[图片上传失败...(image-fa5a8c-1612341123587)]
注意:虽然在图中可以看到,Observer 服务器在初始化阶段会将SyncRequestProcessor处理器也组装上去,但是在实际运⾏过程中,Leader服务器不会将事务请求的投票发送给Observer服务器。
5.3服务器启动
服务端整体架构图
[图片上传失败...(image-b2e8e3-1612341123587)]
Zookeeper服务器的启动,⼤致可以分为以下五个步骤
1. 配置⽂件解析
2. 初始化数据管理器
3. 初始化⽹络I/O管理器
4. 数据恢复
5. 对外服务
单机版服务器启动流程
1. 预启动
1. 统⼀由QuorumPeerMain作为启动类。⽆论单机或集群,在zkServer.cmd和zkServer.sh中都配置了QuorumPeerMain作为启动⼊⼝类。
2. 解析配置⽂件zoo.cfg。zoo.cfg配置运⾏时的基本参数,如tickTime、dataDir、clientPort等参数。
3. 创建并启动历史⽂件清理器DatadirCleanupManager。对事务⽇志和快照数据⽂件进⾏定时清理。
4. 判断当前是集群模式还是单机模式启动。若是单机模式,则委托给ZooKeeperServerMain进⾏启动。
5. 再次进⾏配置⽂件zoo.cfg的解析。
6. 创建服务器实例ZooKeeperServer。Zookeeper服务器⾸先会进⾏服务器实例的创建,然后对该服务器实例进⾏初始化,包括连接器、内存数据库、请求处理器等组件的初始化。
2. 初始化
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就可以对外提供服务。
集群服务器启动
[图片上传失败...(image-3ffe87-1612341123587)]
上图的过程可以分为预启动、初始化、Leader选举、Leader与Follower启动期交互、Leader与
Follower启动等过程
1. 预启动
1. 统⼀由QuorumPeerMain作为启动类。
2. 解析配置⽂件zoo.cfg。
3. 创建并启动历史⽂件清理器DatadirCleanupFactory。
4. 判断当前是集群模式还是单机模式的启动。在集群模式中,在zoo.cfg⽂件中配置了多个服务器地址,可以选择集群启动。
2. 初始化
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主线程
3. Leader选举
1. 初始化Leader选举。
集群模式特有,Zookeeper⾸先会根据⾃身的服务器ID(SID)、最新的ZXID(lastLoggedZxid)和当前的服务器epoch(currentEpoch)来⽣成⼀个初始化投票,在初始化过程中,每个服务器都会给⾃⼰投票。然后,根据zoo.cfg的配置,创建相应Leader选举算法实现,Zookeeper提供了三种默认算法(LeaderElection、AuthFastLeaderElection、FastLeaderElection),可通过zoo.cfg中的electionAlg属性来指定,但现只⽀持FastLeaderElection选举算法。在初始化阶段,Zookeeper会创建Leader选举所需的⽹络I/O层QuorumCnxManager,同时启动对Leader选举端⼝的监听,等待集群中其他服务器创建连接。
2. 注册JMX服务。
3. 检测当前服务器状态
运⾏期间,QuorumPeer会不断检测当前服务器状态。在正常情况下,Zookeeper服务器的状态在LOOKING、LEADING、FOLLOWING/OBSERVING之间进⾏切换。在启动阶段,QuorumPeer的初始状态是LOOKING,因此开始进⾏Leader选举。
4. Leader选举
ZooKeeper的Leader选举过程,简单地讲,就是⼀个集群中所有的机器相互之间进⾏⼀系列投票,选举产⽣最合适的机器成为Leader,同时其余机器成为Follower或是Observer的集群机器⻆⾊初始化过程。关于Leader选举算法,简而言之,就是集群中哪个机器处理的数据越新(通常我们根据每个服务器处理过的最⼤ZXID来⽐较确定其数据是否更新),其越有可能成为Leader。当然,如果集群中的所有机器处理的ZXID⼀致的话,那么SID最⼤的服务器成为Leader,其余机器称为Follower和Observer。
4. Leader和Follower启动期交互过程
到这⾥为⽌,ZooKeeper已经完成了Leader选举,并且集群中每个服务器都已经确定了⾃⼰的⻆⾊——
通常情况下就分为 Leader 和 Follower 两种⻆⾊。下⾯我们来对 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服
务器实例就可以启动了
5. Leader和Follower启动
1. 创建启动会话管理器。
2. 初始化Zookeeper请求处理链,集群模式的每个处理器也会在启动阶段串联请求处理链。
3. 注册JMX服务。
5.4Leader选举
1、Leader选举概述
Leader选举是zookeeper最重要的技术之⼀,也是保证分布式数据⼀致性的关键所在。当Zookeeper集群中的⼀台服务器出现以下两种情况之⼀时,需要进⼊Leader选举。
(1) 服务器初始化启动。
(2) 服务器运⾏期间⽆法和Leader保持连接。
下⾯就两种情况进⾏分析讲解。
2、服务器启动时期的Leader选举
若进⾏Leader选举,则⾄少需要两台机器,这⾥选取3台机器组成的服务器集群为例。在集群初始化阶
段,当有⼀台服务器Server1启动时,其单独⽆法进⾏和完成Leader选举,当第⼆台服务器Server2启动时,此时两台机器可以相互通信,每台机器都试图找到Leader,于是进⼊Leader选举过程。选举过程如下:
(1) 每个Server发出⼀个投票
由于是初始情况,Server1(假设myid为1)和Server2假设myid为2)都会将⾃⼰作为Leader服务器来进⾏投票,每次投票会包含所推举的服务器的myid和ZXID,使⽤(myid, ZXID)来表示,此时Server1的投票为(1, 0),Server2的投票为(2, 0),然后各⾃将这个投票发给集群中其他机器
(2) 接受来⾃各个服务器的投票
集群的每个服务器收到投票后,⾸先判断该投票的有效性,如检查是否是本轮投票、是否来⾃LOOKING状态的服务器。
(3) 处理投票
针对每⼀个投票,服务器都需要将别⼈的投票和⾃⼰的投票进⾏PK,PK规则如下
- 优先检查ZXID。ZXID⽐较⼤的服务器优先作为Leader。
- 如果ZXID相同,那么就⽐较myid。myid较⼤的服务器作为Leader服务器。
现在我们来看Server1和Server2实际是如何进⾏投票处理的。对于Server1来说,它⾃⼰的投票是(1,0),⽽接收到的投票为(2,0)。⾸先会对⽐两者的ZXID,因为都是0,所以⽆法决定谁是Leader。接下来会对⽐两者的myid,很显然,Server1发现接收到的投票中的myid是2,⼤于⾃⼰,于是就会更新⾃⼰的投票为(2,0),然后重新将投票发出去。⽽对于Server2来说,不需要更新⾃⼰的投票
(4) 统计投票
每次投票后,服务器都会统计所有投票,判断是否已经有过半的机器接收到相同的投票信息。对于Server1和Server2服务器来说,都统计出集群中已经有两台机器接受了(2,0)这个投票信息。这⾥我们需要对“过半”的概念做⼀个简单的介绍。所谓“过半”就是指⼤于集群机器数量的⼀半,即⼤于或等于(n/2+1)。对于这⾥由3台机器构成的集群,⼤于等于2台即为达到“过半”要求。
那么,当Server1和Server2都收到相同的投票信息(2,0)的时候,即认为已经选出了Leader。
(5) 改变服务器状态
⼀旦确定了 Leader,每个服务器就会更新⾃⼰的状态:如果是 Follower,那么就变更为FOLLOWING,如果是Leader,那么就变更为LEADING。
3、服务器运行时期的Leader选举
在ZooKeeper集群正常运⾏过程中,⼀旦选出⼀个Leader,那么所有服务器的集群⻆⾊⼀般不会再发⽣变化——也就是说,Leader服务器将⼀直作为集群的Leader,即使集群中有⾮Leader机器挂了或是有新机器加⼊集群也不会影响Leader。但是⼀旦Leader所在的机器挂了,那么整个集群将暂时⽆法对外服务,⽽是进⼊新⼀轮的Leader选举。服务器运⾏期间的Leader选举和启动时期的Leader选举基本过程是⼀致的。
我们还是假设当前正在运⾏的 ZooKeeper 机器由 3 台机器组成,分别是 Server1、Server2和Server3,当前的Leader是Server2。假设在某⼀个瞬间,Leader挂了,这个时候便开始了Leader选举。
(1) 变更状态
Leader挂后,余下的⾮Observer服务器都会将⾃⼰的服务器状态变更为LOOKING,然后开始进⼊Leader选举过程。
(2) 每个Server会发出⼀个投票
在运⾏期间,每个服务器上的ZXID可能不同,此时假定Server1的ZXID为123,Server3的ZXID为122;
在第⼀轮投票中,Server1和Server3都会投⾃⼰,产⽣投票(1, 123),(3, 122),然后各⾃将投票发送给集群中所有机器。
(3)投票
接收来⾃各个服务器的投票,与启动时过程相同。
(4) 处理投票
与启动时过程相同,此时,Server1将会成为Leader。
(5)统计投票
与启动时过程相同。
(6) 状态修改
改变服务器的状态。与启动时过程相同。
6.源码分析
6.1源码构建
zk源码下载地址: https://github.com/apache/zookeeper/tree/release-3.5.4
需要下载ant进行构建通过eclise导入到idea
服务端启动配置:QuorumPeerMain 类 main方法
VM options: -Dlog4j.configuration=file: :[your path] /log4j.propertiesProgram arguments :[your path]\conf\zoo.cfg
客户端启动配置:ZooKeeperMain 类 main方法
VM options: -Dlog4j.configuration=file:[your path]/log4j.propertiesProgram arguments -server 127.0.0.1:2182 [get /lg]
6.2 单机模式服务端启动
[图片上传失败...(image-f08da3-1612341123587)]