赞
踩
ZooKeeper最为主要的使⽤场景,是作为分布式系统的分布式协同服务。
我们将分布式系统定义为:分布式系统是同时跨越多个物理主机,独⽴运⾏的多个软件所组成系统。类⽐⼀下,分布式系统就是⼀群⼈⼀起⼲活。⼈多⼒量⼤,每个服务器的算⼒是有限的,但是通过分布式系统,由n个服务器组成起来的集群,算⼒是可以⽆限扩张的。
优点显⽽易⻅,⼈多⼲活快,并且互为备份。但是缺点也很明显,那就是信息如何在多个系统间进行同步,信息透明。
如果能做到信息同步,那么每个⼈在⼲什么,⼤家都是清楚的,⼲到什么程度也是清晰的,⽆论谁离职也不会产⽣问题。分配的⼯作,能够及时清晰的同步给每个组员,确保每个组员收到的任务分配没有冲突。
分布式系统的协调⼯作就是通过某种⽅式,让每个节点的信息能够同步和共享。这依赖于服务进程之间的通信。通信⽅式有两种:
ZooKeeper对分布式系统的协调,使⽤的是第⼆种⽅式,共享存储。其实共享存储,分布式应⽤也需要和存储进⾏⽹络通信。
ZooKeeper就像是svn,存储了任务的分配、完成情况等共享信息。每个分布式应⽤的节点就是组员,订阅这些共享信息。当主节点(组leader),对某个从节点的分⼯信息作出改变时,相关订阅的从节点得到zookeeper的通知,取得⾃⼰最新的任务分配。完成⼯作后,把完成情况存储到zookeeper。主节点订阅了该任务的完成情况信息,所以将得到zookeeper的完⼯的通知。参考下图
注:Slave节点要想获取ZooKeeper的更新通知,需事先在关⼼的数据节点上设置观察点。
⼤多数分布式系统中出现的问题,都源于信息的共享出了问题。如果各个节点间信息不能及时共享和同步,那么就会在协作过程中产⽣各种问题。ZooKeeper解决协同问题的关键,就是在于保证分布式系统信息的⼀致性。
Zookeeper是⼀个开源的分布式协调服务,其设计⽬标是将那些复杂的且容易出错的分布式⼀致性服务封装起来,构成⼀个⾼效可靠的原语集,并以⼀些简单的接⼝提供给⽤户使⽤。zookeeper是⼀个典型的分布式数据⼀致性的解决⽅案,分布式应⽤程序可以基于它实现诸如数据订阅/发布、负载均衡、命名服务、集群管理、分布式锁和分布式队列等功能。
① 集群⻆⾊
通常在分布式系统中,构成⼀个集群的每⼀台机器都有⾃⼰的⻆⾊,最典型的集群就是Master/Slave模式(主备模式),此情况下把所有能够处理写操作的机器称为Master机器,把所有通过异步复制⽅式获取最新数据,并提供读服务的机器为Slave机器。
⽽在Zookeeper中,这些概念被颠覆了。它没有沿⽤传递的Master/Slave概念,⽽是引⼊了Leader
、Follower
、Observer
三种⻆⾊。
Zookeeper集群中的所有机器通过Leader选举来选定⼀台被称为Leader的机器,Leader服务器为客户端提供读和写服务,除Leader外,其他机器包括Follower和Observer,Follower和Observer都能提供读服务,唯⼀的区别在于Observer不参与Leader选举过程**,**不参与写操作的过半写成功策略,因此Observer可以在不影响写性能的情况下提升集群的性能。
② 会话(session)
Session指客户端会话,⼀个客户端连接是指客户端和服务端之间的⼀个TCP⻓连接。Zookeeper对外的服务端⼝默认为2181,客户端启动的时候,⾸先会与服务器建⽴⼀个TCP连接,从第⼀次连接建⽴开始,客户端会话的⽣命周期也开始了,通过这个连接,客户端能够⼼跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接受来⾃服务器的Watch事件通知。
③ 数据节点(Znode)
在谈到分布式的时候,我们通常说的“节点”是指组成集群的每⼀台机器。然⽽,在ZooKeeper中,“节 点”分为两类,第⼀类同样是指构成集群的机器,我们称之为机器节点;第⼆类则是指数据模型中的数据单元,我们称之为数据节点——ZNode
。ZooKeeper将所有数据存储在内存中,数据模型是⼀棵树(ZNode Tree),由斜杠(/)进⾏分割的路径,就是⼀个Znode,例如/app/path1。每个ZNode上都会保存⾃⼰的数据内容,同时还会保存⼀系列属性信息。
④ 版本
Zookeeper的每个Znode上都会存储数据,对于每个ZNode,Zookeeper都会为其维护⼀个叫作Stat的数据结构,Stat记录了这个ZNode的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode⼦节点的版本)、aversion(当前ZNode的ACL版本)。
⑤ Watcher(事件监听器)
Wathcer(事件监听器),是Zookeeper中⼀个很重要的特性,Zookeeper允许⽤户在指定节点上注册⼀些Watcher,并且在⼀些特定事件触发的时候,Zookeeper服务端会将事件通知到感兴趣的客户端,该机制是Zookeeper实现分布式协调服务的重要特性。
⑥ ACL
Zookeeper采⽤ACL(Access Control Lists)策略来进⾏权限控制,其定义了如下五种权限:
其中需要注意的是,CREATE和DELETE这两种权限都是针对⼦节点的权限控制。
Zookeeper安装⽅式有三种,单机模式和集群模式以及伪集群模式。
zookeeper安装以linux环境为例:
1、下载
⾸先我们下载稳定版本的zookeeper http://zookeeper.apache.org/releases.html
2、上传
下载完成后,将zookeeper压缩包zookeeper-3.4.14.tar.gz上传到linux系统
3、解压缩压缩包
tar -zxvf zookeeper-3.4.14.tar.gz
4、进⼊ zookeeper-3.4.14 ⽬录,创建 data ⽂件夹**
cd zookeeper-3.4.14
mkdir data
5、修改配置⽂件名称
cd conf
mv zoo_sample.cfg zoo.cfg
5、修改zoo.cfg中的data属性
dataDir=/root/zookeeper-3.4.14/data
6、zookeeper服务启动
进⼊bin⽬录,启动服务输⼊命令
./zkServer.sh start
关闭服务输⼊命令
./zkServer.sh stop
查看状态
./zkServer.sh status
Zookeeper不但可以在单机上运⾏单机模式Zookeeper,⽽且可以在单机模拟集群模式 Zookeeper的运⾏,也就是将不同实例运⾏在同⼀台机器,⽤端⼝进⾏区分,伪集群模式为我们体验Zookeeper和做⼀些尝试性的实验提供了很⼤的便利。⽐如,我们在测试的时候,可以先使⽤少量数据在伪集群模式下进⾏测试。当测试可⾏的时候,再将数据移植到集群模式进⾏真实的数据实验。这样不但保证了它的可⾏性,同时⼤⼤提⾼了实验的效率。这种搭建⽅式,⽐较简便,成本⽐较低,适合测试和学习。
注意事项:
⼀台机器上部署了3个server,也就是说单台机器及上运⾏多个Zookeeper实例。这种情况下,必须保证每个配置⽂档的各个端⼝号不能冲突,除clientPort不同之外,dataDir也不同。另外,还要在dataDir所对应的⽬录中创建myid⽂件来指定对应的Zookeeper服务器实例。
clientPort端⼝:
如果在1台机器上部署多个server,那么每台机器都要不同的 clientPort,⽐如 server1是2181,server2是2182,server3是2183。
dataDir和dataLogDir:
dataDir和dataLogDir也需要区分下,将数据⽂件和⽇志⽂件分开存放,同时每个server的这两变量所对应的路径都是不同的。
server.X和myid:
server.X 这个数字就是对应,data/myid中的数字。在3个server的myid⽂件中分别写⼊了1,2,3,那么每个server中的zoo.cfg都配 server.1 server.2,server.3就⾏了。因为在同⼀台机器上,后⾯连着的2个端⼝,3个server都不要⼀样,否则端⼝冲突。
下载:
⾸先我们下载最新稳定版本的zookeeper http://zookeeper.apache.org/releases.html
上传
下载完成后,将zookeeper压缩包 zookeeper-3.4.14.tar.gz上传到linux系统。
解压 压缩包
创建⽬录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=/zkcluster/zookeeper01/data
dataLogDir=/zkcluster/zookeeper01/data/logs
clientPort=2182
dataDir=/zkcluster/zookeeper02/data
dataLogDir=/zkcluster/zookeeper02/data/logs
clientPort=2183
dataDir=/zkcluster/zookeeper03/data
dataLogDir=/zkcluster/zookeeper03/data/logs
配置集群
(1)在每个zookeeper的 data ⽬录下创建⼀个 myid ⽂件,内容分别是1、2、3 。这个⽂件就是记录每个服务器的ID
touch myid
(2)在每⼀个zookeeper 的 zoo.cfg配置客户端访问端⼝(clientPort)和集群服务器IP列表。
server.1=10.211.55.4:2881:3881
server.2=10.211.55.4:2882:3882
server.3=10.211.55.4:2883:3883
#server.服务器ID=服务器IP地址:服务器之间通信端⼝:服务器之间投票选举端⼝
启动集群
依次启动三个zk实例
在ZooKeeper中,数据信息被保存在⼀个个数据节点上,这些节点被称为znode。ZNode 是Zookeeper 中最⼩数据单位,在 ZNode 下⾯⼜可以再挂 ZNode,这样⼀层层下去就形成了⼀个层次化命名空间 ZNode 树,我们称为 ZNode Tree,它采⽤了类似⽂件系统的层级树状结构进⾏管理。⻅下图示例:
ZNode的节点路径标识⽅式和Unix⽂件系统路径⾮常相似,都是由⼀系列使⽤斜杠(/)进⾏分割的路径表示,开发⼈员可以向这个节点写⼊数据,也可以在这个节点下⾯创建⼦节点。
ZNode 的类型
刚刚已经了解到,Zookeeper的znode tree是由⼀系列数据节点组成的,那接下来,我们就对数据节点做详细讲解。
Zookeeper 节点类型可以分为三⼤类:
在开发中在创建节点的时候通过组合可以⽣成以下四种节点类型:持久节点、持久顺序节点、临时节点、临时顺序节点。不同类型的节点则会有不同的⽣命周期。
**持久节点:**是Zookeeper中最常⻅的⼀种节点类型,所谓持久节点,就是指节点被创建后会⼀直存在服务器,直到删除操作主动清除。
**持久顺序节点:**就是有顺序的持久节点,节点特性和持久节点是⼀样的,只是额外特性表现在顺序上。顺序特性实质是在创建节点的时候,会在节点名后⾯加上⼀个数字后缀,来表示其顺序。
**临时节点:**就是会被⾃动清理掉的节点,它的⽣命周期和客户端会话绑在⼀起,客户端会话结束,节点会被删除掉。与持久性节点不同的是,临时节点不能创建⼦节点。
**临时顺序节点:**就是有顺序的临时节点,和持久顺序节点相同,在其创建的时候会在名字后⾯加上数字后缀。
事务ID:
⽽在ZooKeeper中,事务是指能够改变ZooKeeper服务器状态的操作,我们也称之为事务操作或更新操作,⼀般包括数据节点创建与删除、数据节点内容更新等操作。对于每⼀个事务请求,ZooKeeper都会为其分配⼀个全局唯⼀的事务ID,⽤ ZXID 来表示,通常是⼀个 64 位的数字。每⼀个 ZXID 对应⼀次更新操作,从这些ZXID中可以间接地识别出ZooKeeper处理这些更新操作请求的全局顺序。
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,如果是持久性节点那么值为 0
dataLength 表示数据⻓度。
numChildren 表示直系⼦节点数。
Zookeeper使⽤Watcher机制实现分布式数据的发布/订阅功能。
⼀个典型的发布/订阅模型系统定义了⼀种 ⼀对多的订阅关系,能够让多个订阅者同时监听某⼀个主题对象,当这个主题对象⾃身状态变化时,会通知所有订阅者,使它们能够做出相应的处理。在 ZooKeeper 中,引⼊了 Watcher 机制来实现这种分布式的通知功能。ZooKeeper 允许客户端向服务端注册⼀个 Watcher 监听,当服务端的⼀些指定事件触发了这个 Watcher,那么就会向指定客户端发送⼀个事件通知来实现分布式的通知功能。
整个Watcher注册与通知过程如图所示:
Zookeeper的Watcher机制主要包括客户端线程、客户端WatcherManager、Zookeeper服务器三部分。
具体⼯作流程为:客户端在向Zookeeper服务器注册的同时,会将Watcher对象存储在客户端的WatcherManager当中。当Zookeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatcherManager中取出对应的Watcher对象来执⾏回调逻辑。
Zookeeper作为⼀个分布式协调框架,其内部存储了分布式系统运⾏时状态的元数据,这些元数据会直接影响基于Zookeeper进⾏构造的分布式系统的运⾏状态,因此,如何保障系统中数据的安全,从⽽避免因误操作所带来的数据随意变更⽽导致的数据库异常⼗分重要,在Zookeeper中,提供了⼀套完善的ACL(Access Control List)权限控制机制来保障数据的安全。
我们可以从三个⽅⾯来理解ACL机制:权限模式(Scheme)、授权对象(ID)、权限(Permission),通常使⽤scheme: id : permission
来标识⼀个有效的ACL信息。
权限模式:Scheme
来确定权限验证过程中使⽤的检验策略,有如下四种模式:
username:password
形式的权限标识来进⾏权限配置,便于区分不同应⽤来进⾏权限控制。当我们通过“username:password”形式配置了权限标识后,Zookeeper会先后对其进⾏SHA-1加密和BASE64编码。授权对象:ID
授权对象指的是权限赋予的⽤户或⼀个指定实体,例如 IP 地址或是机器等。在不同的权限模式下,授权对象是不同的,表中列出了各个权限模式和授权对象之间的对应关系。
权限模式 | 授权对象 |
---|---|
IP | 通常是⼀个IP地址或IP段:例如:192.168.10.110 或192.168.10.1/24 |
Digest | ⾃定义,通常是username:BASE64(SHA-1(username:password))例如:zm:sdfndsllndlksfn7c= |
World | 只有⼀个ID :anyone |
Super | 超级⽤户 |
权限
权限就是指那些通过权限检查后可以被允许执⾏的操作。在ZooKeeper中,所有对数据的操作权限分为
以下五⼤类:
现在已经搭建起了⼀个能够正常运⾏的zookeeper服务了,所以接下来,就是来借助客户端来对zookeeper的数据节点进⾏操作
⾸先,进⼊到zookeeper的bin⽬录之后,通过zkClient进⼊zookeeper客户端命令⾏
./zkcli.sh 连接本地的zookeeper服务器
./zkCli.sh -server ip:port 连接指定的服务器
连接成功之后,系统会输出Zookeeper的相关环境及配置信息等信息。输⼊help之后,屏幕会输出可⽤的Zookeeper命令。
使⽤create命令,可以创建⼀个Zookeeper节点, 如
create [-s][-e] path data acl
其中,-s或-e分别指定节点特性,顺序或临时节点,若不指定,则创建持久节点;acl⽤来进⾏权限控制。
① 创建顺序节点
使⽤ create -s /zk-test 123
命令创建zk-test顺序节点
执⾏完后,就在根节点下创建了⼀个叫做/zk-test的节点,该节点内容就是123,同时可以看到创建的zk-test节点后⾯添加了⼀串数字以示区别。
② 创建临时节点
使⽤create -e /zk-temp 123
命令创建zk-temp临时节。
临时节点在客户端会话结束后,就会⾃动删除。
③ 创建永久节点
使⽤ create /zk-permanent 123
命令创建zk-permanent永久节点。永久节点不同于顺序节点,不会⾃动在后⾯添加⼀串数字。
与读取相关的命令有ls 命令和get 命令
ls命令可以列出Zookeeper指定节点下的所有⼦节点,但只能查看指定节点下的第⼀级的所有⼦节点:
ls path
其中,path表示的是指定数据节点的节点路径
get命令可以获取Zookeeper指定节点的数据内容和属性信息:
get path
若获取根节点下⾯的所有⼦节点,使⽤ls / 命令即可。
若想获取/zk-permanent的数据内容和属性,可使⽤如下命令:et /zk-permanent
从上⾯的输出信息中,我们可以看到,第⼀⾏是节点/zk-permanent 的数据内容,其他⼏⾏则是创建该节点的事务ID(cZxid)、最后⼀次更新该节点的事务ID(mZxid)和最后⼀次更新该节点的时间(mtime)等属性信息。
使⽤set命令,可以更新指定节点的数据内容,⽤法如下
set path data [version]
其中,data就是要更新的新内容,version表示数据版本,在zookeeper中,节点的数据是有版本概念的,这个参数⽤于指定本次更新操作是基于Znode的哪⼀个数据版本进⾏的,如将/zk-permanent节点的数据更新为456,可以使⽤如下命令:set /zk-permanent 456
现在dataVersion已经变为1了,表示进⾏了更新。
使⽤delete命令可以删除Zookeeper上的指定节点,⽤法如下
delete path [version]
其中version也是表示数据版本,使⽤delete /zk-permanent
命令即可删除/zk-permanent节点
若删除节点存在⼦节点,那么⽆法删除该节点,必须先删除⼦节点,再删除⽗节点
Zookeeper作为⼀个分布式框架,主要⽤来解决分布式⼀致性问题,它提供了简单的分布式原语,并且对多种编程语⾔提供了API,所以接下来重点来看下Zookeeper的java客户端API使⽤⽅式。
Zookeeper API共包含五个包,分别为:
其中org.apache.zookeeper,包含Zookeeper类,他是我们编程时最常⽤的类⽂件。这个类是Zookeeper客户端的主要类⽂件。如果要使⽤Zookeeper服务,应⽤程序⾸先必须创建⼀个Zookeeper实例,这时就需要使⽤此类。⼀旦客户端和Zookeeper服务端建⽴起了连接,Zookeeper系统将会给本次连接会话分配⼀个ID值,并且客户端将会周期性的向服务器端发送⼼跳来维持会话连接。只要连接有效,客户端就可以使⽤Zookeeper API来做相应处理了。
导⼊依赖
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
</dependency>
import org.apache.zookeeper.WatchedEvent; import org.apache.zookeeper.Watcher; import org.apache.zookeeper.ZooKeeper; import java.io.IOException; import java.util.concurrent.CountDownLatch; public class CreateSession implements Watcher { private static CountDownLatch countDownLatch = new CountDownLatch(1); /* 建立会话 */ public static void main(String[] args) throws IOException, InterruptedException { /* 客户端可以通过创建一个zk实例来连接zk服务器 new Zookeeper(connectString,sesssionTimeOut,Wather) connectString: 连接地址:IP:端口 sesssionTimeOut:会话超时时间:单位毫秒 Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端) */ ZooKeeper zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new CreateSession()); System.out.println(zooKeeper.getState()); // 计数工具类:CountDownLatch:不让main方法结束,让线程处于等待阻塞 countDownLatch.await(); System.out.println("客户端与服务端会话真正建立了"); } /* 回调方法:处理来自服务器端的watcher通知 */ public void process(WatchedEvent watchedEvent) { // SyncConnected if(watchedEvent.getState() == Event.KeeperState.SyncConnected){ //解除主程序在CountDownLatch上的等待阻塞 System.out.println("process方法执行了..."); countDownLatch.countDown(); } } }
注意:ZooKeeper 客户端和服务端会话的建⽴是⼀个异步的过程,也就是说在程序中,构造⽅法会在处理完客户端初始化⼯作后⽴即返回,在⼤多数情况下,此时并没有真正建⽴好⼀个可⽤的会话,在会话的⽣命周期中处于“CONNECTING”的状态。 当该会话真正创建完毕后ZooKeeper服务端会向会话对应的客户端发送⼀个事件通知,以告知客户端,客户端只有在获取这个通知之后,才算真正建⽴了会话。
所以代码中使用了process方法来监听建立链接后最一些操作;使用 countDownLatch来等待客户端连接。
import org.apache.zookeeper.*; import java.io.IOException; import java.util.concurrent.CountDownLatch; public class CreateNote implements Watcher { private static CountDownLatch countDownLatch = new CountDownLatch(1); private static ZooKeeper zooKeeper; /* 建立会话 */ public static void main(String[] args) throws IOException, InterruptedException, KeeperException { /* 客户端可以通过创建一个zk实例来连接zk服务器 new Zookeeper(connectString,sesssionTimeOut,Wather) connectString: 连接地址:IP:端口 sesssionTimeOut:会话超时时间:单位毫秒 Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端) */ zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new CreateNote()); System.out.println(zooKeeper.getState()); // 计数工具类:CountDownLatch:不让main方法结束,让线程处于等待阻塞 //countDownLatch.await();\ Thread.sleep(Integer.MAX_VALUE); } /* 回调方法:处理来自服务器端的watcher通知 */ public void process(WatchedEvent watchedEvent) { // SyncConnected if(watchedEvent.getState() == Event.KeeperState.SyncConnected){ //解除主程序在CountDownLatch上的等待阻塞 System.out.println("process方法执行了..."); // 创建节点 try { createNoteSync(); } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } /* 创建节点的方法 */ private static void createNoteSync() throws KeeperException, InterruptedException { /** * path :节点创建的路径 * data[] :节点创建要保存的数据,是个byte类型的 * acl :节点创建的权限信息(4种类型) * ANYONE_ID_UNSAFE : 表示任何人 * AUTH_IDS :此ID仅可用于设置ACL。它将被客户机验证的ID替换。 * OPEN_ACL_UNSAFE :这是一个完全开放的ACL(常用)--> world:anyone * CREATOR_ALL_ACL :此ACL授予创建者身份验证ID的所有权限 * createMode :创建节点的类型(4种类型) * PERSISTENT:持久节点 * PERSISTENT_SEQUENTIAL:持久顺序节点 * EPHEMERAL:临时节点 * EPHEMERAL_SEQUENTIAL:临时顺序节点 String node = zookeeper.create(path,data,acl,createMode); */ // 持久节点 String note_persistent = zooKeeper.create("/lg-persistent", "持久节点内容".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); // 临时节点 String note_ephemeral = zooKeeper.create("/lg-ephemeral", "临时节点内容".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); // 持久顺序节点 String note_persistent_sequential = zooKeeper.create("/lg-persistent_sequential", "持久顺序节点内容".getBytes(), ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT_SEQUENTIAL); System.out.println("创建的持久节点" + note_persistent); System.out.println("创建的临时节点" + note_ephemeral); System.out.println("创建的持久顺序节点" + note_persistent_sequential); } }
import org.apache.zookeeper.*; import java.io.IOException; import java.util.List; import java.util.concurrent.CountDownLatch; public class GetNoteData implements Watcher { private static CountDownLatch countDownLatch = new CountDownLatch(1); private static ZooKeeper zooKeeper; /* 建立会话 */ public static void main(String[] args) throws IOException, InterruptedException, KeeperException { /* 客户端可以通过创建一个zk实例来连接zk服务器 new Zookeeper(connectString,sesssionTimeOut,Wather) connectString: 连接地址:IP:端口 sesssionTimeOut:会话超时时间:单位毫秒 Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端) */ zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new GetNoteData()); System.out.println(zooKeeper.getState()); // 计数工具类:CountDownLatch:不让main方法结束,让线程处于等待阻塞 //countDownLatch.await();\ Thread.sleep(Integer.MAX_VALUE); } /* 回调方法:处理来自服务器端的watcher通知 */ public void process(WatchedEvent watchedEvent) { /* 子节点列表发生改变时,服务器端会发生noteChildrenChanged事件通知 要重新获取子节点列表,同时注意:通知是一次性的,需要反复注册监听 */ if(watchedEvent.getType() == Event.EventType.NodeChildrenChanged){ List<String> children = null; try { children = zooKeeper.getChildren("/lg-persistent", true); } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(children); } // SyncConnected if(watchedEvent.getState() == Event.KeeperState.SyncConnected){ //解除主程序在CountDownLatch上的等待阻塞 System.out.println("process方法执行了..."); // 获取节点数据的方法 try { getNoteData(); // 获取节点的子节点列表方法 getChildrens(); } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } /* 获取某个节点的内容 */ private void getNoteData() throws KeeperException, InterruptedException { /** * path : 获取数据的路径 * watch : 是否开启监听 * stat : 节点状态信息 * null: 表示获取最新版本的数据 * zk.getData(path, watch, stat); */ byte[] data = zooKeeper.getData("/lg-persistent", false, null); System.out.println(new String(data)); } /* 获取某个节点的子节点列表方法 */ public static void getChildrens() throws KeeperException, InterruptedException { /* path:路径 watch:是否要启动监听,当子节点列表发生变化,会触发监听 zooKeeper.getChildren(path, watch); */ List<String> children = zooKeeper.getChildren("/lg-persistent", true); System.out.println(children); } }
import org.apache.zookeeper.*; import org.apache.zookeeper.data.Stat; import java.io.IOException; import java.util.concurrent.CountDownLatch; public class UpdateNoteData implements Watcher { private static CountDownLatch countDownLatch = new CountDownLatch(1); private static ZooKeeper zooKeeper; /* 建立会话 */ public static void main(String[] args) throws IOException, InterruptedException, KeeperException { /* 客户端可以通过创建一个zk实例来连接zk服务器 new Zookeeper(connectString,sesssionTimeOut,Wather) connectString: 连接地址:IP:端口 sesssionTimeOut:会话超时时间:单位毫秒 Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端) */ zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new UpdateNoteData()); System.out.println(zooKeeper.getState()); // 计数工具类:CountDownLatch:不让main方法结束,让线程处于等待阻塞 //countDownLatch.await();\ Thread.sleep(Integer.MAX_VALUE); } /* 回调方法:处理来自服务器端的watcher通知 */ public void process(WatchedEvent watchedEvent) { // SyncConnected if(watchedEvent.getState() == Event.KeeperState.SyncConnected){ //解除主程序在CountDownLatch上的等待阻塞 System.out.println("process方法执行了..."); // 更新数据节点内容的方法 try { updateNoteSync(); } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } /* 更新数据节点内容的方法 */ private void updateNoteSync() throws KeeperException, InterruptedException { /* path:路径 data:要修改的内容 byte[] version:为-1,表示对最新版本的数据进行修改 zooKeeper.setData(path, data,version); */ byte[] data = zooKeeper.getData("/lg-persistent", false, null); System.out.println("修改前的值:" + new String(data)); //修改/lg-persistent 的数据 stat: 状态信息对象 Stat stat = zooKeeper.setData("/lg-persistent", "客户端修改了节点数据".getBytes(), -1); byte[] data2 = zooKeeper.getData("/lg-persistent", false, null); System.out.println("修改后的值:" + new String(data2)); } }
import org.apache.zookeeper.*; import org.apache.zookeeper.data.Stat; import java.io.IOException; import java.util.concurrent.CountDownLatch; public class DeleteNote implements Watcher { private static CountDownLatch countDownLatch = new CountDownLatch(1); private static ZooKeeper zooKeeper; /* 建立会话 */ public static void main(String[] args) throws IOException, InterruptedException, KeeperException { /* 客户端可以通过创建一个zk实例来连接zk服务器 new Zookeeper(connectString,sesssionTimeOut,Wather) connectString: 连接地址:IP:端口 sesssionTimeOut:会话超时时间:单位毫秒 Wather:监听器(当特定事件触发监听时,zk会通过watcher通知到客户端) */ zooKeeper = new ZooKeeper("127.0.0.1:2181", 5000, new DeleteNote()); System.out.println(zooKeeper.getState()); // 计数工具类:CountDownLatch:不让main方法结束,让线程处于等待阻塞 //countDownLatch.await();\ Thread.sleep(Integer.MAX_VALUE); } /* 回调方法:处理来自服务器端的watcher通知 */ public void process(WatchedEvent watchedEvent) { // SyncConnected if(watchedEvent.getState() == Event.KeeperState.SyncConnected){ //解除主程序在CountDownLatch上的等待阻塞 System.out.println("process方法执行了..."); // 删除节点 try { deleteNoteSync(); } catch (KeeperException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } } } /* 删除节点的方法 */ private void deleteNoteSync() throws KeeperException, InterruptedException { /* zooKeeper.exists(path,watch) :判断节点是否存在 zookeeper.delete(path,version) : 删除节点 */ Stat stat = zooKeeper.exists("/lg-persistent/c1", false); System.out.println(stat == null ? "该节点不存在":"该节点存在"); if(stat != null){ zooKeeper.delete("/lg-persistent/c1",-1); } Stat stat2 = zooKeeper.exists("/lg-persistent/c1", false); System.out.println(stat2 == null ? "该节点不存在":"该节点存在"); } }
ZkClient是Github上⼀个开源的zookeeper客户端,在Zookeeper原⽣API接⼝之上进⾏了包装,是⼀个更易⽤的Zookeeper客户端,同时,zkClient在内部还实现了诸如Session超时重连、Watcher反复注册等功能。
接下来,还是从创建会话、创建节点、读取数据、更新数据、删除节点等⽅⾯来介绍如何使⽤zkClient这个zookeeper客户端。
添加依赖:
在pom.xml⽂件中添加如下内容
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.2</version>
</dependency>
import org.I0Itec.zkclient.ZkClient; public class CreateSession { /* 借助zkclient完成会话的创建 */ public static void main(String[] args) { /* 创建一个zkclient实例就可以完成连接,完成会话的创建 serverString : 服务器连接地址 注意:zkClient通过对zookeeperAPI内部封装,将这个异步创建会话的过程同步化了.. */ ZkClient zkClient = new ZkClient("127.0.0.1:2181"); System.out.println("会话被创建了.."); } }
运⾏结果:ZooKeeper session established,结果表明已经成功创建会话。
ZkClient提供了递归创建节点的接⼝,即其帮助开发者先完成⽗节点的创建,再创建⼦节点。
import org.I0Itec.zkclient.ZkClient; public class Create_Note { /* 借助zkclient完成会话的创建 */ public static void main(String[] args) { /* 创建一个zkclient实例就可以完成连接,完成会话的创建 serverString : 服务器连接地址 注意:zkClient通过对zookeeperAPI内部封装,将这个异步创建会话的过程同步化了.. */ ZkClient zkClient = new ZkClient("127.0.0.1:2181"); System.out.println("会话被创建了.."); //创建节点 /* cereateParents : 是否要创建父节点,如果值为true,那么就会递归创建节点 */ zkClient.createPersistent("/lg-zkclient/c1",true); System.out.println("节点递归创建完成"); } }
运⾏结果:success create znode。
结果表明已经成功创建了节点,值得注意的是,在原⽣态接⼝中是⽆法创建成功的(⽗节点不存在),但是通过ZkClient通过设置createParents参数为true可以递归的先创建⽗节点,再创建⼦节点。
ZkClient提供了递归删除节点的接⼝,即其帮助开发者先删除所有⼦节点(存在),再删除⽗节点。
import org.I0Itec.zkclient.ZkClient; public class Delete_Note { /* 借助zkclient完成会话的创建 */ public static void main(String[] args) { /* 创建一个zkclient实例就可以完成连接,完成会话的创建 serverString : 服务器连接地址 注意:zkClient通过对zookeeperAPI内部封装,将这个异步创建会话的过程同步化了.. */ ZkClient zkClient = new ZkClient("127.0.0.1:2181"); System.out.println("会话被创建了.."); // 递归删除节点 String path = "/lg-zkclient/c1"; zkClient.createPersistent(path+"/c11"); zkClient.deleteRecursive(path); System.out.println("递归删除成功"); } }
运⾏结果: success delete znode。
结果表明ZkClient可直接删除带⼦节点的⽗节点,因为其底层先删除其所有⼦节点,然后再删除⽗节点。
package com.lagou.zkclient; import org.I0Itec.zkclient.IZkChildListener; import org.I0Itec.zkclient.ZkClient; import java.util.List; public class Get_NoteChildren { /* 借助zkclient完成会话的创建 */ public static void main(String[] args) throws InterruptedException { /* 创建一个zkclient实例就可以完成连接,完成会话的创建 serverString : 服务器连接地址 注意:zkClient通过对zookeeperAPI内部封装,将这个异步创建会话的过程同步化了.. */ ZkClient zkClient = new ZkClient("127.0.0.1:2181"); System.out.println("会话被创建了.."); // 获取子节点列表 List<String> children = zkClient.getChildren("/lg-zkclient"); System.out.println(children); // 注册监听事件 /* 客户端可以对一个不存在的节点进行子节点变更的监听 只要该节点的子节点列表发生变化,或者该节点本身被创建或者删除,都会触发监听 */ zkClient.subscribeChildChanges("/lg-zkclient-get", new IZkChildListener() { /* s : parentPath list : 变化后子节点列表 */ public void handleChildChange(String parentPath, List<String> list) throws Exception { System.out.println(parentPath + "'s child changed,currentChilds:"+ list); } }); //测试 zkClient.createPersistent("/lg-zkclient-get"); Thread.sleep(1000); zkClient.createPersistent("/lg-zkclient-get/c1"); Thread.sleep(1000); } }
客户端可以对⼀个不存在的节点进⾏⼦节点变更的监听。⼀旦客户端对⼀个节点注册了⼦节点列表变更监听之后,那么当该节点的⼦节点列表发⽣变更时,服务端都会通知客户端,并将最新的⼦节点列表发送给客户端该节点本身的创建或删除也会通知到客户端。
public class GetDataSample { public static void main(String[] args) throws InterruptedException { String path = "/lg-zkClient-Ep"; ZkClient zkClient = new ZkClient("127.0.0.1:2181"); //判断节点是否存在 boolean exists = zkClient.exists(path); if (!exists){ zkClient.createEphemeral(path, "123"); } //注册监听 zkClient.subscribeDataChanges(path, new IZkDataListener() { public void handleDataChange(String path, Object data) throws Exception { System.out.println(path+"该节点内容被更新,更新后的内容"+data); } public void handleDataDeleted(String s) throws Exception { System.out.println(s+" 该节点被删除"); } }); //获取节点内容 Object o = zkClient.readData(path); System.out.println(o); //更新 zkClient.writeData(path,"4567"); Thread.sleep(1000); //删除 zkClient.delete(path); Thread.sleep(1000); } }
运⾏结果:
123
/lg-zkClient-Ep该节点内容被更新,更新后的内容4567
/lg-zkClient-Ep 该节点被删除
注意的是:获取结果时可以成功监听节点数据变化或删除事件(节点是否存在、更新、删除)。
curator是Netflflix公司开源的⼀套Zookeeper客户端框架,和ZKClient⼀样,Curator解决了很多Zookeeper客户端⾮常底层的细节开发⼯作,包括连接重连,反复注册Watcher和NodeExistsException异常等,是最流⾏的Zookeeper客户端之⼀。从编码⻛格上来讲,它提供了基于Fluent的编程⻛格⽀持。
添加依赖:
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>2.12.0</version>
</dependency>
Curator的创建会话⽅式与原⽣的API和ZkClient的创建⽅式区别很⼤。Curator创建客户端是通CuratorFrameworkFactory⼯⼚类来实现的。具体如下:
import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.ExponentialBackoffRetry; public class CreateSession { // 创建会话 public static void main(String[] args) { //不使用fluent编程风格 RetryPolicy exponentialBackoffRetry = new ExponentialBackoffRetry(1000, 3); CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient("127.0.0.1:2181", exponentialBackoffRetry); curatorFramework.start(); System.out.println( "会话被建立了"); // 使用fluent编程风格 CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("127.0.0.1:2181") .sessionTimeoutMs(50000) .connectionTimeoutMs(30000) .retryPolicy(exponentialBackoffRetry) .namespace("base") // 独立的命名空间 /base .build(); client.start(); System.out.println("会话2创建了"); } }
参数:
connectString:zk的server地址,多个server之间使⽤英⽂逗号分隔开
connectionTimeoutMs:连接超时时间,如上是30s,默认是15s
sessionTimeoutMs:会话超时时间,如上是50s,默认是60s
retryPolicy:失败重试策略,可查看org.apache.curator.RetryPolicy接⼝的实现类
ExponentialBackoffffRetry:构造器含有三个参数 ExponentialBackoffffRetry(int baseSleepTimeMs, int maxRetries, int maxSleepMs)
baseSleepTimeMs:初始的sleep时间,⽤于计算之后的每次重试的sleep时间,计算公式:当前sleep时间=baseSleepTimeMs*Math.max(1, random.nextInt(1<<(retryCount+1)))
maxRetries:最⼤重试次数
maxSleepMs:最⼤sleep时间,如果上述的当前sleep计算出来⽐这个⼤,那么sleep⽤这个时间,默认的最⼤时间是Integer.MAX_VALUE毫秒。
需要注意的是session2会话含有隔离命名空间,即客户端对Zookeeper上数据节点的任何操作都是相对/base⽬录进⾏的,这有利于实现不同的Zookeeper的业务之间的隔离。
curator提供了⼀系列Fluent⻛格的接⼝,通过使⽤Fluent编程⻛格的接⼝,开发⼈员可以进⾏⾃由组合来完成各种类型节点的创建。
下⾯简单介绍⼀下常⽤的⼏个节点创建场景。
(1)创建⼀个初始内容为空的节点
client.create().forPath(path);
Curator默认创建的是持久节点,内容为空。
(2)创建⼀个包含内容的节点
client.create().forPath(path,"我是内容".getBytes());
Curator和ZkClient不同的是依旧采⽤Zookeeper原⽣API的⻛格,内容使⽤byte[]作为⽅法参数。
(3)递归创建⽗节点,并选择节点类型
client.create().creatingParentsIfNeeded().withMode(CreateMode.EPHEMERAL).forPath(path);
creatingParentsIfNeeded这个接⼝⾮常有⽤,在使⽤ZooKeeper 的过程中,开发⼈员经常会碰到NoNodeException 异常,其中⼀个可能的原因就是试图对⼀个不存在的⽗节点创建⼦节点。因此,开发⼈员不得不在每次创建节点之前,都判断⼀下该⽗节点是否存在——这个处理通常⽐较麻烦。在使⽤Curator 之后,通过调⽤creatingParentsIfNeeded 接⼝,Curator 就能够⾃动地递归创建所有需要的⽗节点。
下⾯通过⼀个实际例⼦来演示如何在代码中使⽤这些API:
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.CreateMode; public class CreateNote_curator { // 创建会话 public static void main(String[] args) throws Exception { CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("127.0.0.1:2181") .sessionTimeoutMs(50000) .connectionTimeoutMs(30000) .retryPolicy(exponentialBackoffRetry) .namespace("base") // 独立的命名空间 /base .build(); client.start(); System.out.println("会话创建了"); // 创建节点 String path = "/lg-curator/c1"; String s = client.create().creatingParentsIfNeeded() .withMode(CreateMode.PERSISTENT).forPath(path, "init".getBytes()); System.out.println("节点递归创建成功,该节点路径" + s); } }
删除节点的⽅法也是基于Fluent⽅式来进⾏操作,不同类型的操作调⽤ 新增不同的⽅法调⽤即可。
(1)删除⼀个⼦节点
client.delete().forPath(path);
(2)删除节点并递归删除其⼦节点
client.delete().deletingChildrenIfNeeded().forPath(path);
(3)指定版本进⾏删除
client.delete().withVersion(1).forPath(path);
如果此版本已经不存在,则删除异常,异常信息如下:
org.apache.zookeeper.KeeperException$BadVersionException: KeeperErrorCode =BadVersion for
(4)强制保证删除⼀个节点
client.delete().guaranteed().forPath(path);
只要客户端会话有效,那么Curator会在后台持续进⾏删除操作,直到节点删除成功。⽐如遇到⼀些⽹络异常的情况,此guaranteed的强制删除就会很有效果。
示例:
import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.ExponentialBackoffRetry; public class DeleteNote_curator { public static void main(String[] args) throws Exception { // 创建会话 // 使用fluent编程风格 CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("127.0.0.1:2181") .sessionTimeoutMs(50000) .connectionTimeoutMs(30000) .retryPolicy(exponentialBackoffRetry) .namespace("base") // 独立的命名空间 /base .build(); client.start(); System.out.println("会话创建了"); // 删除节点 String path = "/lg-curator"; client.delete().deletingChildrenIfNeeded().withVersion(-1).forPath(path); System.out.println("删除成功,删除的节点" + 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);
版本不⼀致异常信息。
ZooKeeper是⼀个典型的发布/订阅模式的分布式数据管理与协调框架,我们可以使⽤它来进⾏分布式数据的发布与订阅。另⼀⽅⾯,通过对ZooKeeper中丰富的数据节点类型进⾏交叉使⽤,配合Watcher事件通知机制,可以⾮常⽅便地构建⼀系列分布式应⽤中都会涉及的核⼼功能,如数据发布/订阅、命名服务、集群管理、Master选举、分布式锁和分布式队列等。那接下来就针对这些典型的分布式应⽤场景来做下介绍
数据发布/订阅(Publish/Subscribe)系统,即所谓的配置中⼼,顾名思义就是发布者将数据发布到ZooKeeper的⼀个或⼀系列节点上,供订阅者进⾏数据订阅,进⽽达到动态获取数据的⽬的,实现配置信息的集中式管理和数据的动态更新。
发布/订阅系统⼀般有两种模式:
ZooKeeper 采⽤的是推拉相结合的⽅式。客户端向服务端注册⾃⼰需要关注的节点,⼀旦该节点的数据发⽣变更,那么服务端就会向相应的客户端发送Watcher事件通知,客户端接收到这个消息通知之后,需要主动到服务端获取最新的数据。
如果将配置信息存放到ZooKeeper上进⾏集中管理,那么通常情况下,应⽤在启动的时候都会主动到ZooKeeper服务端上进⾏⼀次配置信息的获取,同时,在指定节点上注册⼀个Watcher监听,这样⼀来,但凡配置信息发⽣变更,服务端都会实时通知到所有订阅的客户端,从⽽达到实时获取最新配置信息的⽬的。
下⾯我们通过⼀个“配置管理”的实际案例来展示ZooKeeper在“数据发布/订阅”场景下的使⽤⽅式。
在我们平常的应⽤系统开发中,经常会碰到这样的需求:系统中需要使⽤⼀些通⽤的配置信息,例如机器列表信息、运⾏时的开关配置、数据库配置信息等。这些全局配置信息通常具备以下3个特性:
对于这类配置信息,⼀般的做法通常可以选择将其存储在本地配置⽂件或是内存变量中。⽆论采⽤哪种⽅式,其实都可以简单地实现配置管理,在集群机器规模不⼤、配置变更不是特别频繁的情况下,⽆论刚刚提到的哪种⽅式,都能够⾮常⽅便地解决配置管理的问题。但是,⼀旦机器规模变⼤,且配置信息变更越来越频繁后,我们发现依靠现有的这两种⽅式解决配置管理就变得越来越困难了。我们既希望能够快速地做到全局配置信息的变更,同时希望变更成本⾜够⼩,因此我们必须寻求⼀种更为分布式化的解决⽅案。
接下来我们就以⼀个“数据库切换”的应⽤场景展开,看看如何使⽤ZooKeeper来实现配置管理:
在进⾏配置管理之前,⾸先我们需要将初始化配置信息存储到Zookeeper上去,⼀般情况下,我们可以在Zookeeper上选取⼀个数据节点⽤于配置信息的存储,例如:/app1/database_config。
我们将需要管理的配置信息写⼊到该数据节点中去,例如:
#数据库配置信息
#DBCP
dbcp.driverClassName=com.mysql.jdbc.Driver
dbcp.dbJDBCUrl=jdbc:mysql://127.0.0.1:3306/lagou-test
dbcp.username=zm
dbcp.password=1234
dbcp.maxActive=30
dbcp.maxIdle=10
集群中每台机器在启动初始化阶段,⾸先会从上⾯提到的ZooKeeper配置节点上读取数据库信息,同时,客户端还需要在该配置节点上注册⼀个数据变更的 Watcher监听,⼀旦发⽣节点数据变更,所有订阅的客户端都能够获取到数据变更通知。
在系统运⾏过程中,可能会出现需要进⾏数据库切换的情况,这个时候就需要进⾏配置变更。借助ZooKeeper,我们只需要对ZooKeeper上配置节点的内容进⾏更新,ZooKeeper就能够帮我们将数据变更的通知发送到各个客户端,每个客户端在接收到这个变更通知后,就可以重新进⾏最新数据的获取。
**命名服务(Name Service)**也是分布式系统中⽐较常⻅的⼀类场景,是分布式系统最基本的公共服务之⼀。在分布式系统中,被命名的实体通常可以是集群中的机器、提供的服务地址或远程对象等——这些我们都可以统称它们为名字(Name),其中较为常⻅的就是⼀些分布式服务框架(如RPC、RMI)中的服务地址列表。
通过使⽤命名服务,客户端应⽤能够根据指定名字来获取资源的实体、服务地址和提供者的信息等。
ZooKeeper 提供的命名服务功能能够帮助应⽤系统通过⼀个资源引⽤的⽅式来实现对资源的定位与使⽤。另外,⼴义上命名服务的资源定位都不是真正意义的实体资源——在分布式环境中,上层应⽤仅仅需要⼀个全局唯⼀的名字,类似于数据库中的唯⼀主键。
⼀说起全局唯⼀ ID,相信⼤家都会联想到 UUID。它确实是⼀个⾮常不错的全局唯⼀ID⽣成⽅式,能够⾮常简便地保证分布式环境中的唯⼀性。但是也有很多缺陷:⻓度过⻓;含义不明。
所以接下来,我们结合⼀个分布式任务调度系统来看看如何使⽤ZooKeepe来实现这类全局唯⼀ID的⽣成。
之前我们已经提到,通过调⽤ZooKeeper节点创建的API接⼝可以创建⼀个顺序节点,并且在API返回值中会返回这个节点的完整名字。利⽤这个特性,我们就可以借助ZooKeeper来⽣成全局唯⼀的ID了,如下图:
对于⼀个任务列表的主键,使⽤ZooKeeper⽣成唯⼀ID的基本步骤:
在ZooKeeper中,每⼀个数据节点都能够维护⼀份⼦节点的顺序顺列,当客户端对其创建⼀个顺序⼦节点的时候 ZooKeeper 会⾃动以后缀的形式在其⼦节点上添加⼀个序号,在这个场景中就是利⽤了ZooKeeper的这个特性。
随着分布式系统规模的⽇益扩⼤,集群中的机器规模也随之变⼤,那如何更好地进⾏集群管理也显得越来越重要了。
所谓集群管理,包括集群监控与集群控制两⼤块,前者侧重对集群运⾏时状态的收集,后者则是对集群进⾏操作与控制。
在⽇常开发和运维过程中,我们经常会有类似于如下的需求:
Zookeeper有两⼤特性:
利⽤其两⼤特性,可以实现集群机器存活监控系统,若监控系统在/clusterServers节点上注册⼀个Watcher监听,那么但凡进⾏动态添加机器的操作,就会在/clusterServers节点下创建⼀个临时节点:/clusterServers/[Hostname],这样,监控系统就能够实时监测机器的变动情况。
分布式⽇志收集系统
下⾯通过分布式⽇志收集系统这个典型应⽤来学习Zookeeper如何实现集群管理。
分布式⽇志收集系统的核⼼⼯作就是收集分布在不同机器上的系统⽇志,在这⾥我们重点来看分布式⽇志系统(以下简称“⽇志系统”)的收集器模块。
在⼀个典型的⽇志系统的架构设计中,整个⽇志系统会把所有需要收集的⽇志机器(我们以“⽇志源机器”代表此类机器)分为多个组别,每个组别对应⼀个收集器,这个收集器其实就是⼀个后台机器(我们以“收集器机器”代表此类机器),⽤于收集⽇志。
对于⼤规模的分布式⽇志收集系统场景,通常需要解决两个问题:
⽆论是⽇志源机器还是收集器机器的变更,最终都可以归结为如何快速、合理、动态地为每个收集器分配对应的⽇志源机器。这也成为了整个⽇志系统正确稳定运转的前提,也是⽇志收集过程中最⼤的技术挑战之⼀,在这种情况下,我们就可以引⼊zookeeper了,下⾯我们就来看ZooKeeper在这个场景中的使⽤。
使⽤Zookeeper的场景步骤如下:
① 注册收集器机器
使⽤ZooKeeper来进⾏⽇志系统收集器的注册,典型做法是在ZooKeeper上创建⼀个节点作为收集器的根节点,例如/logs/collector(下⽂我们以“收集器节点”代表该数据节点),每个收集器机器在启动的时候,都会在收集器节点下创建⾃⼰的节点,例如/logs/collector/[Hostname],如下图:
② 任务分发
待所有收集器机器都创建好⾃⼰对应的节点后,系统根据收集器节点下⼦节点的个数,将所有⽇志源机器分成对应的若⼲组,然后将分组后的机器列表分别写到这些收集器机器创建的⼦节点(例如/logs/collector/host1/machine1)上去。这样⼀来,每个收集器机器都能够从⾃⼰对应的收集器节点上获取⽇志源机器列表,进⽽开始进⾏⽇志收集⼯作。
③ 状态汇报
完成收集器机器的注册以及任务分发后,我们还要考虑到这些机器随时都有挂掉的可能。因此,针对这个问题,我们需要有⼀个收集器的状态汇报机制:每个收集器机器在创建完⾃⼰的专属节点后,还需要在对应的⼦节点上创建⼀个状态⼦节点,例如/logs/collector/host1/status,每个收集器机器都需要定期向该节点写⼊⾃⼰的状态信息。我们可以把这种策略看作是⼀种⼼跳检测机制,通常收集器机器都会在这个节点中写⼊⽇志收集进度信息。⽇志系统根据该状态⼦节点的最后更新时间来判断对应的收集器机器是否存活。
④ 动态分配
如果收集器机器挂掉或是扩容了,就需要动态地进⾏收集任务的分配。在运⾏过程中,⽇志系统始终关注着/logs/collector这个节点下所有⼦节点的变更,⼀旦检测到有收集器机器停⽌汇报或是有新的收集器机器加⼊,就要开始进⾏任务的重新分配。⽆论是针对收集器机器停⽌汇报还是新机器加⼊的情况,⽇志系统都需要将之前分配给该收集器的所有任务进⾏转移。为了解决这个问题,通常有两种做法:
上述步骤已经完整的说明了整个⽇志收集系统的⼯作流程,其中有两点注意事项:
①节点类型
在/logs/collector节点下创建临时节点可以很好的判断机器是否存活,但是,若机器挂了,其节点会被删除,记录在节点上的⽇志源机器列表也被清除,所以需要选择持久节点来标识每⼀台机器,同时在节点下分别创建 /logs/collector/[Hostname]/status 节点来表征每⼀个收集器机器的状态,这样,既能实现对所有机器的监控,同时机器挂掉后,依然能够将分配任务还原。
② ⽇志系统节点监听
若采⽤Watcher机制,那么通知的消息量的⽹络开销⾮常⼤,需要采⽤⽇志系统主动轮询收集器节点的策略,这样可以节省⽹络流量,但是存在⼀定的延时。
Master选举是⼀个在分布式系统中⾮常常⻅的应⽤场景。分布式最核⼼的特性就是能够将具有ᇿ⽴计算能⼒的系统单元部署在不同的机器上,构成⼀个完整的分布式系统。⽽与此同时,实际场景中往往也需要在这些分布在不同机器上的ᇿ⽴系统单元中选出⼀个所谓的“⽼⼤”,在计算机中,我们称之为Master。
在分布式系统中,Master往往⽤来协调集群中其他系统单元,具有对分布式系统状态变更的决定权。例如,在⼀些读写分离的应⽤场景中,客户端的写请求往往是由 Master来处理的;⽽在另⼀些场景中,Master则常常负责处理⼀些复杂的逻辑,并将处理结果同步给集群中其他系统单元。Master选举可以说是ZooKeeper最典型的应⽤场景了,接下来,我们就结合“⼀种海量数据处理与共享模型”这个具体例⼦来看看 ZooKeeper在集群Master选举中的应⽤场景。
在分布式环境中,经常会碰到这样的应⽤场景:集群中的所有系统单元需要对前端业务提供数据,⽐如⼀个商品 ID,或者是⼀个⽹站轮播⼴告的⼴告 ID(通常出现在⼀些⼴告投放系统中)等,⽽这些商品ID或是⼴告ID往往需要从⼀系列的海量数据处理中计算得到——这通常是⼀个⾮常耗费 I/O 和 CPU资源的过程。鉴于该计算过程的复杂性,如果让集群中的所有机器都执⾏这个计算逻辑的话,那么将耗费⾮常多的资源。⼀种⽐较好的⽅法就是只让集群中的部分,甚⾄只让其中的⼀台机器去处理数据计算,⼀旦计算出数据结果,就可以共享给整个集群中的其他所有客户端机器,这样可以⼤⼤减少重复劳动,提升性能。 这⾥我们以⼀个简单的⼴告投放系统后台场景为例来讲解这个模型。
整个系统⼤体上可以分成客户端集群、分布式缓存系统、海量数据处理总线和 ZooKeeper四个部分。
⾸先我们来看整个系统的运⾏机制。图中的Client集群每天定时会通过ZooKeeper来实现Master选举。选举产⽣Master客户端之后,这个Master就会负责进⾏⼀系列的海量数据处理,最终计算得到⼀个数据结果,并将其放置在⼀个内存/数据库中。同时,Master还需要通知集群中其他所有的客户端从这个内存/数据库中共享计算结果。
接下去,我们将重点来看 Master 选举的过程,⾸先来明确下 Master 选举的需求:在集群的所有机器中选举出⼀台机器作为Master。针对这个需求,通常情况下,我们可以选择常⻅的关系型数据库中的主键特性来实现:集群中的所有机器都向数据库中插⼊⼀条相同主键 ID 的记录,数据库会帮助我们⾃动进⾏主键冲突检查,也就是说,所有进⾏插⼊操作的客户端机器中,只有⼀台机器能够成功——那么,我们就认为向数据库中成功插⼊数据的客户端机器成为Master。
借助数据库的这种⽅案确实可⾏,依靠关系型数据库的主键特性能够很好地保证在集群中选举出唯⼀的⼀个Master。但是我们需要考虑的另⼀个问题是,如果当前选举出的Master挂了,那么该如何处理?谁来告诉我Master挂了呢?显然,关系型数据库没法通知我们这个事件。那么,如果使⽤ZooKeeper是否可以做到这⼀点呢? 那在之前,我们介绍了ZooKeeper创建节点的API接⼝,其中⼀个重要特性便是:利⽤ZooKeeper的强⼀致性,能够很好保证在分布式⾼并发情况下节点的创建⼀定能够保证全局唯⼀性,即ZooKeeper将会保证客户端⽆法重复创建⼀个已经存在的数据节点。也就是说,如果同时有多个客户端请求创建同⼀个节点,那么最终⼀定只有⼀个客户端请求能够创建成功。利⽤这个特性,就能很容易地在分布式环境中进⾏Master选举了。
客户端集群每天都会定时往ZooKeeper 上创建⼀个临时节点,例如/master_election/2020-11-11/binding。在这个过程中,只有⼀个客户端能够成功创建这个节点,那么这个客户端所在的机器就成为了Master。同时,其他没有在ZooKeeper上成功创建节点的客户端,都会在节点/master_election/2020-11-11 上注册⼀个⼦节点变更的 Watcher,⽤于监控当前的 Master 机器是否存活,⼀旦发现当前的 Master 挂了,那么其余的客户端将会重新进⾏Master选举。
从上⾯的讲解中,我们可以看到,如果仅仅只是想实现Master选举的话,那么其实只需要有⼀个能够保证数据唯⼀性的组件即可,例如关系型数据库的主键模型就是⾮常不错的选择。但是,如果希望能够快速地进⾏集群 Master 动态选举,那么就可以基于 ZooKeeper来实现。
分布式锁是控制分布式系统之间同步访问共享资源的⼀种⽅式。如果不同的系统或是同⼀个系统的不同主机之间共享了⼀个或⼀组资源,那么访问这些资源的时候,往往需要通过⼀些互斥⼿段来防⽌彼此之间的⼲扰,以保证⼀致性,在这种情况下,就需要使⽤分布式锁了。
在平时的实际项⽬开发中,我们往往很少会去在意分布式锁,⽽是依赖于关系型数据库固有的排他性来实现不同进程之间的互斥。这确实是⼀种⾮常简便且被⼴泛使⽤的分布式锁实现⽅式。然⽽有⼀个不争的事实是,⽬前绝⼤多数⼤型分布式系统的性能瓶颈都集中在数据库操作上。因此,如果上层业务再给数据库添加⼀些额外的锁,例如⾏锁、表锁甚⾄是繁重的事务处理,那么就会让数据库更加不堪重负。
下⾯我们来看看使⽤ZooKeeper如何实现分布式锁,这⾥主要讲解排他锁和共享锁两类分布式锁。
排他锁(Exclusive Locks,简称 X 锁),⼜称为写锁或ᇿ占锁,是⼀种基本的锁类型。如果事务 T1对数据对象 O1加上了排他锁,那么在整个加锁期间,只允许事务 T1对 O1进⾏读取和更新操作,其他任何事务都不能再对这个数据对象进⾏任何类型的操作——直到T1释放了排他锁。
排他锁的核⼼是如何保证当前有且仅有⼀个事务获得锁,并且锁被释放后,所有正在等待获取锁的事务都能够被通知到。
下⾯我们就来看看如何借助ZooKeeper实现排他锁:
① 定义锁
在通常的Java开发编程中,有两种常⻅的⽅式可以⽤来定义锁,分别是synchronized机制和JDK5提供的ReentrantLock。然⽽,在ZooKeeper中,没有类似于这样的API可以直接使⽤,⽽是通过 ZooKeeper上的数据节点来表示⼀个锁,例如/exclusive_lock/lock节点就可以被定义为⼀个锁。
② 获取锁
在需要获取排他锁时,所有的客户端都会试图通过调⽤ create()接⼝,在/exclusive_lock节点下创建临时⼦节点/exclusive_lock/lock。在前⾯,我们也介绍了,ZooKeeper 会保证在所有的客户端中,最终只有⼀个客户端能够创建成功,那么就可以认为该客户端获取了锁。同时,所有没有获取到锁的客户端就需要到/exclusive_lock 节点上注册⼀个⼦节点变更的Watcher监听,以便实时监听到lock节点的变更情况。
③释放锁
在“定义锁”部分,我们已经提到,/exclusive_lock/lock 是⼀个临时节点,因此在以下两种情况下,都有可能释放锁。 1.当前获取锁的客户端机器发⽣宕机,那么ZooKeeper上的这个临时节点就会被移除; 2.正常执⾏完业务逻辑后,客户端就会主动将⾃⼰创建的临时节点删除。 ⽆论在什么情况下移除了lock节点,ZooKeeper都会通知所有在/exclusive_lock节点上注册了⼦节点变更Watcher监听的客户端。这些客户端在接收到通知后,再次重新发起分布式锁获取,即重复“获取锁”过程。
整个排他锁的获取和释放流程,如下图:
共享锁(Shared Locks,简称S锁),⼜称为读锁。
如果事务T1对数据对象O1加上了共享锁,那么当前事务只能对O1进⾏读取操作,其他事务也只能对这个数据对象加共享锁——直到该数据对象上的所有共享锁都被释放。
共享锁和排他锁最根本的区别在于,加上排他锁后,数据对象只对⼀个事务可⻅,⽽加上共享锁后,数据对所有事务都可⻅。
下⾯我们就来看看如何借助ZooKeeper来实现共享锁:
① 定义锁
和排他锁⼀样,同样是通过 ZooKeeper 上的数据节点来表示⼀个锁,是⼀个类似于“/shared_lock/[Hostname]-请求类型-序号”的临时顺序节点,例如/shared_lock/host1-R-0000000001,那么,这个节点就代表了⼀个共享锁,如图所示:
② 获取锁
在需要获取共享锁时,所有客户端都会到/shared_lock 这个节点下⾯创建⼀个临时顺序节点,如果当前是读请求,那么就创建例如/shared_lock/host1-R-0000000001的节点;如果是写请求,那么就创建例如/shared_lock/host2-W-0000000002的节点。
判断读写顺序
通过Zookeeper来确定分布式读写顺序,⼤致分为四步:
③ 释放锁
其释放锁的流程与ᇿ占锁⼀致。
⽺群效应
上⾯讲解的这个共享锁实现,⼤体上能够满⾜⼀般的分布式集群竞争锁的需求,并且性能都还可以——这⾥说的⼀般场景是指集群规模不是特别⼤,⼀般是在10台机器以内。但是如果机器规模扩⼤之后,会有什么问题呢?我们着重来看上⾯“判断读写顺序”过程的步骤3,结合下⾯的图,看看实际运⾏中的情况:
针对如上图所示的情况进⾏分析:
可以看到,host1客户端在移除⾃⼰的共享锁后,Zookeeper发送了⼦节点更变Watcher通知给所有机器,然⽽除了给host2产⽣影响外,对其他机器没有任何作⽤。⼤量的Watcher通知和⼦节点列表获取两个操作会重复运⾏,这样不仅会对zookeeper服务器造成巨⼤的性能影响影响和⽹络开销,更为严重的是,如果同⼀时间有多个节点对应的客户端完成事务或是事务中断引起节点消失,ZooKeeper服务器就会在短时间内向其余客户端发送⼤量的事件通知,这就是所谓的⽺群效应。
上⾯这个ZooKeeper分布式共享锁实现中出现⽺群效应的根源在于,没有找准客户端真正的关注点。我们再来回顾⼀下上⾯的分布式锁竞争过程,它的核⼼逻辑在于:判断⾃⼰是否是所有⼦节点中序号最⼩的。于是,很容易可以联想到,每个节点对应的客户端只需要关注⽐⾃⼰序号⼩的那个相关节点的变更情况就可以了——⽽不需要关注全局的⼦列表变更情况。
可以有如下改动来避免⽺群效应。
改进后的分布式锁实现:
主要的改动在于:每个锁竞争者,只需要关注/shared_lock节点下序号⽐⾃⼰⼩的那个节点是否存在即可,具体实现如下:
此⽅案改动主要在于:每个锁竞争者,只需要关注/shared_lock节点下序号⽐⾃⼰⼩的那个节点是否存在即可。
在具体的实际开发过程中,我们提倡根据具体的业务场景和集群规模来选择适合⾃⼰的分布式锁实现:在集群规模不⼤、⽹络资源丰富的情况下,第⼀种分布式锁实现⽅式是简单实⽤的选择;⽽如果集群规模达到⼀定程度,并且希望能够精细化地控制分布式锁机制,那么就可以试试改进版的分布式锁实现。
分布式队列可以简单分为两⼤类:⼀种是常规的FIFO先⼊先出队列模型,还有⼀种是 等待队列元素聚集后统⼀安排处理执⾏的Barrier模型。
① FIFO先⼊先出
FIFO(First Input First Output,先⼊先出), FIFO 队列是⼀种⾮常典型且应⽤⼴泛的按序执⾏的队列模型:先进⼊队列的请求操作先完成后,才会开始处理后⾯的请求。
使⽤ZooKeeper实现FIFO队列,和之前提到的共享锁的实现⾮常类似。FIFO队列就类似于⼀个全写的共享锁模型,⼤体的设计思路其实⾮常简单:所有客户端都会到/queue_fififo 这个节点下⾯创建⼀个临时顺序节点,例如/queue_fififo/host1-00000001。
创建完节点后,根据如下4个步骤来确定执⾏顺序。
② Barrier:分布式屏障
Barrier原意是指障碍物、屏障,⽽在分布式系统中,特指系统之间的⼀个协调条件,规定了⼀个队列的元素必须都集聚后才能统⼀进⾏安排,否则⼀直等待。这往往出现在那些⼤规模分布式并⾏计算的应⽤场景上:最终的合并计算需要基于很多并⾏计算的⼦结果来进⾏。
这些队列其实是在 FIFO 队列的基础上进⾏了增强,⼤致的设计思想如下:开始时,/queue_barrier 节点是⼀个已经存在的默认节点,并且将其节点的数据内容赋值为⼀个数字n来代表Barrier值,例如n=10表示只有当/queue_barrier节点下的⼦节点个数达到10后,才会打开Barrier。之后,所有的客户端都会到/queue_barrie节点下创建⼀个临时节点,例如/queue_barrier/host1,如图所示:
创建完节点后,按照如下步骤执⾏:
在深⼊了解zookeeper之前,很多同学可能会认为zookeeper就是paxos算法的⼀个实现,但事实上,zookeeper并没有完全采⽤paxos算法,⽽是使⽤了⼀种称为Zookeeper Atomic Broadcast(ZAB,Zookeeper原⼦消息⼴播协议)的协议作为其数据⼀致性的核⼼算法。
ZAB协议并不像Paxos算法那样 是⼀种通⽤的分布式⼀致性算法,它是⼀种特别为zookeeper专⻔设计的⼀种⽀持崩溃恢复的原⼦⼴播协议。
在zookeeper中,主要就是依赖ZAB协议来实现分布式数据的⼀致性,基于该协议,Zookeeper实现了⼀种主备模式的系统架构来保持集群中各副本之间的数据的⼀致性,表现形式就是 使⽤⼀个单⼀的主进程来接收并处理客户端的所有事务请求,并采⽤ZAB的原⼦⼴播协议,将服务器数据的状态变更以事务Proposal的形式⼴播到所有的副本进程中,ZAB协议的主备模型架构保证了同⼀时刻集群中只能够有⼀个主进程来⼴播服务器的状态变更,因此能够很好地处理客户端⼤量的并发请求。但是,也要考虑到主进程在任何时候都有可能出现崩溃退出或重启现象,因此,ZAB协议还需要做到当前主进程当出现上述异常情况的时候,依旧能正常⼯作。
ZAB协议的核⼼是定义了对于那些会改变Zookeeper服务器数据状态的事务请求的处理⽅式。
具体内容如下:
所有事务请求必须由⼀个全局唯⼀的服务器来协调处理,这样的服务器被称为Leader服务器,余下的服务器则称为Follower服务器,Leader服务器负责将⼀个客户端事务请求转化成⼀个事务Proposal(提议),并将该Proposal分发给集群中所有的Follower服务器,之后Leader服务器需要等待所有Follower服务器的反馈,⼀旦超过半数的Follower服务器进⾏了正确的反馈后,那么Leader就会再次向所有的Follower服务器分发Commit消息,要求其将前⼀个Proposal进⾏提交
ZAB协议包括两种基本的模式:崩溃恢复和消息⼴播
进⼊崩溃恢复模式:
当整个服务框架启动过程中,或者是Leader服务器出现⽹络中断、崩溃退出或重启等异常情况时,ZAB协议就会进⼊崩溃恢复模式,同时选举产⽣新的Leader服务器。当选举产⽣了新的Leader服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式,其中,所谓的状态同步就是指数据同步,⽤来保证集群中过半的机器能够和Leader服务器的数据状态保持⼀致。
进⼊消息⼴播模式:
当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进⼊消息⼴播模式。当⼀台同样遵守ZAB协议的服务器启动后加⼊到集群中,如果此时集群中已经存在⼀个Leader服务器在负责进⾏消息⼴播,那么加⼊的服务器就会⾃觉地进⼊数据恢复模式:找到Leader所在的服务器,并与其进⾏数据同步,然后⼀起参与到消息⼴播流程中去。Zookeeper只允许唯⼀的⼀个Leader服务器来进⾏事务请求的处理,Leader服务器在接收到客户端的事务请求后,会⽣成对应的事务提议并发起⼀轮⼴播协议,⽽如果集群中的其他机器收到客户端的事务请求后,那么这些⾮Leader服务器会⾸先将这个事务请求转发给Leader服务器。
接下来我们就重点讲解⼀下ZAB协议的消息⼴播过程和崩溃恢复过程
① 消息⼴播
ZAB协议的消息⼴播过程使⽤原⼦⼴播协议,类似于⼀个⼆阶段提交过程,针对客户端的事务请求,Leader服务器会为其⽣成对应的事务Proposal,并将其发送给集群中其余所有的机器,然后再分别收集各⾃的选票,最后进⾏事务提交。
在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消息后,也会完成对事务的提交。
② 崩溃恢复
ZAB协议的这个基于原⼦⼴播协议的消息⼴播过程,在正常情况下运⾏⾮常良好,但是⼀旦在Leader服务器出现崩溃,或者由于⽹络原因导致Leader服务器失去了与过半Follower的联系,那么就会进⼊崩溃恢复模式。在ZAB协议中,为了保证程序的正确运⾏,整个恢复过程结束后需要选举出⼀个新的Leader服务器,因此,ZAB协议需要⼀个⾼效且可靠的Leader选举算法,从⽽保证能够快速地选举出新的Leader,同时,Leader选举算法不仅仅需要让Leader⾃身知道已经被选举为Leader,同时还需要让集群中的所有其他机器也能够快速地感知到选举产⽣出来的新Leader服务器。
根据上⾯的内容,我们了解到,ZAB协议规定了如果⼀个事务Proposal在⼀台机器上被处理成功,那么应该在所有的机器上都被处理成功,哪怕机器出现故障崩溃。
接下来我们看看在崩溃恢复过程中,可能会出现的两个数据不⼀致性的隐患及针对这些情况ZAB协议所需要保证的特性。
ZAB协议需要确保那些已经在Leader服务器上提交的事务最终被所有服务器都提交
假设⼀个事务在 Leader 服务器上被提交了,并且已经得到过半 Folower 服务器的Ack反馈,但是在它将Commit消息发送给所有Follower机器之前,Leader服务器挂了。如图所示
图中的消息C2就是⼀个典型的例⼦:在集群正常运⾏过程中的某⼀个时刻,Server1 是 Leader 服务器,其先后⼴播了消息 P1、P2、C1、P3 和 C2,其中,当Leader服务器将消息C2(C2是Commit OfProposal2的缩写,即提交事务Proposal2)发出后就⽴即崩溃退出了。针对这种情况,ZAB协议就需要确保事务Proposal2最终能够在所有的服务器上都被提交成功,否则将出现不⼀致。
ZAB协议需要确保丢弃那些只在Leader服务器上被提出的事务
如果在崩溃恢复过程中出现⼀个需要被丢弃的提案,那么在崩溃恢复结束后需要跳过该事务Proposal,如图所示。
在图所示的集群中,假设初始的 Leader 服务器 Server1 在提出了⼀个事务Proposal3 之后就崩溃退出了,从⽽导致集群中的其他服务器都没有收到这个事务Proposal3。于是,当 Server1 恢复过来再次加⼊到集群中的时候,ZAB 协议需要确保丢弃Proposal3这个事务。
结合上⾯提到的这两个崩溃恢复过程中需要处理的特殊情况,就决定了 ZAB 协议必须设计这样⼀个Leader 选举算法:能够确保提交已经被 Leader 提交的事务 Proposal,同时丢弃已经被跳过的事务Proposal。针对这个要求,如果让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列表中,并开始之后的其他流程。
在ZAB协议的设计中,每个进程都有可能处于如下三种状态之⼀:
所有进程初始状态都是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选举。
联系:
区别:
总的来说,ZAB协议和Paxos算法的本质区别在于,两者的设计⽬标不太⼀样,ZAB协议主要⽤于构建⼀个⾼可⽤的分布式数据主备系统,⽽Paxos算法则⽤于构建⼀个分布式的⼀致性状态机系统。
Leader服务器是Zookeeper集群⼯作的核⼼,其主要⼯作有以下两个:
使⽤责任链来处理每个客户端的请求是Zookeeper的特⾊,Leader服务器的请求处理链如下:
可以看到,从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服务器是Zookeeper集群状态中的跟随者,其主要⼯作有以下三个:
和leader⼀样,Follower也采⽤了责任链模式组装的请求处理链来处理每⼀个客户端请求,由于不需要对事务请求的投票处理,因此Follower的请求处理链会相对简单,其处理链如下:
和 Leader 服务器的请求处理链最⼤的不同点在于,Follower 服务器的第⼀个处理器换成了FollowerRequestProcessor处理器,同时由于不需要处理事务请求的投票,因此也没有了ProposalRequestProcessor处理器。
(1)FollowerRequestProcessor
其⽤作识别当前请求是否是事务请求,若是,那么Follower就会将该请求转发给Leader服务器,Leader服务器在接收到这个事务请求后,就会将其提交到请求处理链,按照正常事务请求进⾏处理。
(2) SendAckRequestProcessor
其承担了事务⽇志记录反馈的⻆⾊,在完成事务⽇志记录后,会向Leader服务器发送ACK消息以表明⾃身完成了事务⽇志的记录⼯作。
Observer是ZooKeeper⾃3.3.0版本开始引⼊的⼀个全新的服务器⻆⾊。从字⾯意思看,该服务器充当了⼀个观察者的⻆⾊——其观察ZooKeeper集群的最新状态变化并将这些状态变更同步过来。
Observer服务器在⼯作原理上和Follower基本是⼀致的,对于⾮事务请求,都可以进⾏ᇿ⽴的处理,⽽对于事务请求,则会转发给Leader服务器进⾏处理。和Follower唯⼀的区别在于,Observer不参与任何形式的投票,包括事务请求Proposal的投票和Leader选举投票。简单地讲,Observer服务器只提供⾮事务服务,通常⽤于在不影响集群事务处理能⼒的前提下提升集群的⾮事务处理能⼒。
另外,Observer的请求处理链路和Follower服务器也⾮常相近,其处理链如下:
另外需要注意的⼀点是,虽然在图中可以看到,Observer 服务器在初始化阶段会将SyncRequestProcessor处理器也组装上去,但是在实际运⾏过程中,Leader服务器不会将事务请求的投票发送给Observer服务器。
Zookeeper服务器的启动,⼤致可以分为以下五个步骤:
单机版服务器的启动其流程图如下:
上图的过程可以分为预启动和初始化过程。
(1) 预启动
(2)初始化
⾄此,单机版的Zookeeper服务器启动完毕。
集群和单机服务器的启动在很多地⽅是⼀致的,其流程图如下:
上图的过程可以分为预启动、初始化、Leader选举、Leader与Follower启动期交互、Leader与Follower启动等过程。
(1)预启动
(2)初始化
(3) Leader选举
(4)Leader和Follower启动期交互过程
到这⾥为⽌,ZooKeeper已经完成了Leader选举,并且集群中每个服务器都已经确定了⾃⼰的⻆⾊——通常情况下就分为 Leader 和 Follower 两种⻆⾊。下⾯我们来对 Leader和Follower在启动期间的交互进⾏介绍,其⼤致交互流程如图所示:
(5)Leader和Follower启动
⾄此,集群版的Zookeeper服务器启动完毕。
Leader选举是zookeeper最重要的技术之⼀,也是保证分布式数据⼀致性的关键所在。当Zookeeper集群中的⼀台服务器出现以下两种情况之⼀时,需要进⼊Leader选举。
(1) 服务器初始化启动。
(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规则如下
现在我们来看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。
在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)改变服务器的状态。与启动时过程相同
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。