ZooKeeper简介
ZooKeeper是一个分布式应用协调服务,主要用于解决分布式集群中应用系统的一致性问题。它能提供类似文件系统的目录节点树方式的数据存储,主要用途是维护何监控所存数据的状态变化,以实现对集群的管理。
在分布式环境里,往往会有很多服务器都需要同样的配置来保证信息的一致性和集群的可靠性,而一个分布式集群往往会有上百台服务器,一旦配置信息改变,就需要对每台服务器进行修改,这样会消耗大量时间。像这样的配置信息完全可以交给ZooKeeper来管理,将配置信息保存在ZooKeeper的某个目录节点中,然后所有应用服务器都监控配置信息的状态,一旦配置信息发生变化,每台应用服务器就会收到ZooKeeper的通知,然后从ZooKeeper获取新的配置信息应用到系统中即可。
从设计模式角度来理解:是一个基于观察者模式设计的分布式服务管理框架,它负责存储和管理大家都关心的数据,然后接受观察者的注册,一旦这些数据的状态发生变化,Zookeeper就将负责通知已经在ZooKeeper上注册的那些观察者做出相应的反应。
ZooKeeper的应用场景
统一命名服务
利用ZooKeeper中的树形分层结构,可以把系统中的各种服务的名称、地址以及目录信息存放在ZooKeeper中,需要的时候去ZooKeeper中读取就可以了。ZooKeeper中有一种节点类型是顺序结点,可以利用它的特性制作序列号。数据库具有主键ID可以自动生成,但是在分布式环境中就无法使用了,于是可以使用ZooKeeper的命名服务,它可以生成有顺序的编号,而且支持分布式,非常方便。
集群管理
ZooKeeper能够很容易的实现集群管理的功能,如果有堕胎服务器组成一个服务集群,那么必须要有一个“总管”知道当前集群中每台机器的服务状态,一旦有服务器不能提供服务,集群中其他服务器必须知道,从而做出调整,重新分配服务策略。当增加一台或多台服务器时,同样也必须让“总管”知道。ZooKeeper不仅能够帮助维护当前集群中服务器的服务状态,而且能够选举出一个“总管”,让这个“总管”来管理集群,这种选举方式称为“Leader选举”。
分布式锁
在一个分布式环境中,为了提高可靠性,集群的每台服务器上都部署这同样的服务。但是一个常见的问题就是,如果集群中的每台服务器都进行同一件事情的话,他们相互之间就要协调,编程起来将非常复杂。这个时候可以使用分布式锁,利用ZooKeeper来协调多个分布式进程之间的活动,让某个时刻只让一个服务去工作,当这个服务出现问题的时候将锁释放,立刻切换到另外的服务。
ZooKeeper特点
1.一个领导者(Leader),多个跟随者(Follower)组成的集群。
2.集群中只要有半数以上节点存活,Zookeeper集群就能正常服务。所以Zookeeper适合安装奇数台服务器。
3.全局数据一致:每个Server保存一份相同的数据副本,Client无论连接到哪个Server,数据都是一致的。
4.更新请求顺序执行,来自同一个Client的更新请求按其发送顺序依次执行。
5.数据更新原子性,一次数据更新要么成功,要么失败。
6.实时性,在一定时间范围内,Client能读到最新数据。
ZooKeeper架构原理
ZooKeeper集群由一组服务器节点组成,有一个节点角色为Leader,其他节点的角色为Follower。当客户端连接到ZooKeeper集群并执行写请求时,这些请求首先会发送到Leader节点,Leader节点接收到数据变更请求后,首先将该变更写入本地磁盘供恢复使用,写完后将数据变更到内存中以加快读取数据。最后,Leader节点上的数据变更会同步(广播)到集群的其他Follower节点上。当Leader失效时,Follower快速响应,由消息层重新选出一个Leader节点来处理客户端的请求。
ZooKeeper数据模型
ZooKeeper有一个树形层次的命名空间,该命名空间的组织方式类似于标准文件系统。ZooKeeper可以将该命名空间共享给分布式应用程序,使它们可以利用该命名空间进行相互协调。与为存储而设计的典型文件系统不同,ZooKeeper数据保存在内存中,这样可以提高吞吐量和降低数据延迟。
在ZooKeeper的命名空间当中,名称由斜线(/)分隔的路径元素组成,每个元素(也叫节点)都有路径标识。
ZooKeeper的命名空间中的每个结点都可以有与之关联的数据(也称元数据)以及子节点,就好比标准文件系统中的每个文件夹都可以存放文件并且每个文件夹都有子文件夹。
通常使用znode来表示ZooKeeper的命名空间中的名称结点,存储在每个znode上的数据会被客户端原子化地读取和写入。读取操作可以获取与znode关联的所有数据,而写入操作可以替换所有数据。
znode特点
1.znode中仅存储协调数据,即与同步相关的数据,例如状态信息、配置内容、位置信息等,因此数据量很小,大概B到KB量级。
2.一个znode维护一个状态结构,该结构包括版本号、ACL(访问控制列表)变更、时间戳。znode存储的数据每次发生变化,版本号都会递增,每当客户端检索数据时,客户端也会同时接受到数据的版本。客户端也可以基于版本号检索相关数据。
3.每个znode都有一个ACL,用来限定该znode的客户端访问权限。
4.客户端可以在znode上设置一个观察者(Watcher),如果该znode上的数据发生变更,ZooKeeper就会通知客户端,从而触发Watcher中实现的逻辑的执行。
zode节点类型
持久节点
持久节点在创建后就一直存在,除非手动将其删除。
持久顺序节点
持久顺序节点除了有持久节点的功能外,在创建时,ZooKeeper会在节点名称末尾自动追加一个自增长的数字后缀作为新的节点名称,以便记录每一个节点创建的先后顺序。数字后缀的长度是10位,且由0填充,例如0000000000,下一个节点追加的后缀就是0000000001。
临时节点
只要创建节点的客户端与ZooKeeper服务器的连接会话是活动的,这些节点就存在。当客户端与服务端的连接会话断开时,节点将被删除。基于此,临时节点是不允许有子节点的。
临时顺序节点
临时顺序节点除了有临时节点的功能外,节点在创建时,会在节点末尾追加自增长的数字编号,这一点与持久顺序节点的顺序功能一致。
ZooKeeper选举机制
相关概念
SID:服务器ID。用来唯一标识一台ZooKeeper集群中的机器,每台机器不能重复,和myid一致。
ZXID:事务ID。用来标识一次服务器状态的变更。在某一时刻,集群中的每台机器的ZXID值不一定完全一致,这和ZooKeeper服务器对于客户端“更新请求”的处理逻辑有关。
Epoch:每个Leader任期的代号。没有Leader时同一轮投票过程中的逻辑时钟值是相同的。每投完一次票这个数据就会增加。
第一次启动
以5个节点为例
1.服务器1启动,发起一次选举。服务器1投自己一票。此时服务器1票数一票,不够半数以上(3票),选举无法完成,服务器1状态保持为LOOKING。
2.服务器2启动,再发起一次选举。服务器1和2分别投自己一票并交换选票信息:此时服务器1发现服务器2的myid比自己目前投票推举的(服务器1)大,更改选票为推举服务器2。此时服务器1票数0票,服务器2票数2票,没有半数以上结果,选举无法完成,服务器1,2状态保持LOOKING。
3.服务器3启动,发起一次选举。此时服务器1和2都会更改选票为服务器3。此次投票结果:服务器1为0票,服务器2为0票,服务器3为3票。此时服务器3的票数已经超过半数,服务器3当选Leader。服务器1,2更改状态为FOLLOWING,服务器3更改状态为LEADING。
4/服务器4启动,发起一次选举。此时服务器1,2,3已经不是LOOKING状态,不会更改选票信息。交换选票信息结果:服务器3为3票,服务器4为1票。此时服务器4服从多数,更改选票信息为服务器3,并更改状态为FOLLOWING。
5.服务器5启动,同4一样当小弟。
非第一次启动
当ZooKeeper集群中的一台服务器出现服务器初始化启动或者服务器运行期间无法和Leader保持连接的情况时,就会开始进入Leader选举。
而当一台机器进入Leader选举流程时,当前集群也可能会处于以下两种状态:集群中本来就已经存在一个Leader和集群中确实不存在Leader。
当集群中本来就已经存在一个Leader时,机器试图去选举Leader时,会被告知当前服务器的Leader信息,对于该机器来说,仅仅需要和Leader机器建立连接,并进行状态同步即可。
当集群中确实不存在Leader,则进行重新选举,Epoch大的直接胜出,成为新的Leader, Epoch相同,事务id大的成为新的Leader,事务id相同,服务器id大的成为新的Leader。
Watcher机制
ZooKeeper是一个基于Watcher(观察者)模式设计的分布式服务管理框架。允许客户端向服务器的znode上注册一个Watcher,一旦znode的状态发生变化,ZooKeeper就会通知已经在它上面注册的Watcher做出相应反应。当前,ZooKeeper有四种变化事件:节点创建、节点删除、节点数据修改、子节点变更。所有的读取操作,都可以向服务器设置一个Watcher。Watcher事件相当于一次性的触发器。如果znode再次变更,则不会发送任何通知。
Watcher有两种类型:数据Watcher和子节点Watcher。数据Watcher只监听节点元数据的改变。子节点Watcher只监听子节点的创建与删除。
执行流程
Watcher机制主要包括客户端线程、客户端WatchManager和ZooKeeper服务器三部分。具体流程为:客户端向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当ZooKeeper服务器触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑。
相关事件
getData()方法和exists()方法可以设置数据Watcher。这两个方法返回znode节点的元数据信息。getChildren()方法可以设置子节点Watcher,该方法则返回一个子节点列表。setData()方法会触发数据Watcher,一个成功的create()方法会触发正在创建的znode的数据Watcher以及父节点的子节点Watcher,一个成功的delete()方法会为被删除的znode触发一个数据Watcher以及父节点的子节点Watcher。
总体来说,节点创建事件通过调用exists()方法设置,节点删除事件通过调用exists()方法、getData()方法和getChildren()方法设置,节点改变事件通过exists()方法和getChildren()方法设置,子节点事件通过调用getChildren()方法设置。
分布式锁
在分布式环境中,为了保证在同一时刻只有一个客户端对指定的数据进行访问,需要使用分布式锁技术。只有获得锁的客户端才能对数据进行访问,其余客户端只能暂时等待。
利用ZooKeeper实现分布式锁,所有希望获得锁的客户端都需要执行以下操作:
1.客户端连接ZooKeeper,调用create方法在指定锁节点(如/lock)下创建一个临时顺序节点。
2.客户端调用getChildren()方法查询指定锁节点下的所有子节点列表,判断子节点列表中序号最小的子节点是否是自己创建。如果是,则客户端获得锁,否则监听排在自己前一位的子节点的删除事件,若监听的子节点被删除,则重复执行此步骤,直至获得锁。
3.客户端执行业务代码。
4.客户端完成业务代码后,删除在ZooKeeper中对应的子节点以释放锁。