购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

6.1 ZooKeeper简介

ZooKeeper是一个分布式应用程序协调服务,主要用于解决分布式集群中应用系统的一致性问题。它能提供类似文件系统的目录节点树方式的数据存储功能,主要用途是维护和监控所存数据的状态变化,以实现对集群的管理。

6.1.1 应用场景

在分布式环境里,很多服务器都需要同样的配置来保证信息的一致性和集群的可靠性,而一个分布式集群往往动辄上百台服务器,一旦配置信息改变,就需要对每台服务器进行修改,这样会消耗大量时间。那么有没有一种简单的方法能统一对其修改呢?像这样的配置信息完全可以交给ZooKeeper来管理,即将配置信息保存在ZooKeeper的某个目录节点中,则所有应用服务器都监控配置信息的状态,一旦配置信息发生变化,每台应用服务器都会收到ZooKeeper的通知,然后从ZooKeeper获取新的配置信息应用到系统中即可。

1.统一命名服务

利用ZooKeeper中的树形分层结构,可以把系统中的各种服务的名称、地址以及目录信息存放在ZooKeeper中,需要的时候去ZooKeeper中读取就可以了。

此外,ZooKeeper中有一种节点类型是顺序节点,可以利用它的这个特性制作序列号。我们知道,数据库有主键ID可以自动生成,但是在分布式环境中就无法使用了,于是我们可以使用ZooKeeper的命名服务,它可以生成有顺序的编号,而且支持分布式,使用起来非常方便。

2.集群管理

ZooKeeper能够很容易地实现集群管理的功能,如有多台服务器组成一个服务集群,那么必须要有一个“总管”知道当前集群中每台机器的服务状态,一旦有服务器不能提供服务,集群中其他服务器必须知道,从而做出调整,重新分配服务策略。当增加一台或多台服务器时,同样也必须让“总管”知道。

ZooKeeper不仅能够帮助我们维护当前集群中服务器的服务状态,还能够选举出一个“总管”,让这个“总管”来管理集群,这种选举方式称为“Leader选举”。

3.分布式锁

在一个分布式环境中,为了提高可靠性,集群的每台服务器上都部署同样的服务。但是一个常见的问题是,如果集群中的每台服务器都进行同一件事情的话,它们之间就需要相互协调,编程起来将非常复杂。这个时候可以使用分布式锁,我们可以利用ZooKeeper来协调多个分布式进程之间的活动,在某个时刻只让一个服务器工作,当这个服务器出现问题的时候将锁释放,立即切换到另外的服务器。

6.1.2 架构原理

ZooKeeper集群由一组服务器(Server)节点组成,在这些服务器节点中有一个节点的角色为Leader,其他节点的角色为Follower。当客户端(Client)连接到ZooKeeper集群并执行写请求时,这些请求首先会被发送到Leader节点。Leader节点在接收到数据变更请求后,首先会将该变更写入到本地磁盘以作恢复使用,当所有的写请求持久化到磁盘后,会将数据变更应用到内存中,以加快数据读取速度,最后Leader节点上的数据变更会同步(广播)到集群的其他Follower节点上。

ZooKeeper集群的总体架构如图6-1所示。

图6-1 ZooKeeper集群的总体架构

当Leader节点发生故障而失效时,Follower节点会快速响应,由消息层重新选出一个Leader节点来处理客户端请求。

6.1.3 数据模型

ZooKeeper主要用于管理协调数据(服务器的配置、状态等信息),不能用于存储大型数据集。

ZooKeeper有一个树形层次的命名空间,该命名空间的组织方式类似于标准文件系统。ZooKeeper可以将该命名空间共享给分布式应用程序,使它们可以利用该命名空间进行相互协调。与典型文件系统为存储而设计不同,ZooKeeper数据保存在内存中,这样可以提高吞吐量且降低数据延迟。

在ZooKeeper的命名空间中,名称是由斜线“/”分隔的路径元素组成的。命名空间中的每个名称(也叫节点)都由路径标识,如图6-2所示。

图6-2 ZooKeeper数据模型

ZooKeeper命名空间中的每个节点都可以有与之关联的数据(也称元数据)以及子节点,就好比标准文件系统中的每个文件夹都可以存放文件并且每个文件夹都有子文件夹。

通常使用znode来表示ZooKeeper命名空间中的名称节点,存储在每个znode上的数据会被客户端原子化地读取和写入。读取操作可以获取与znode关联的所有数据,而写入操作可以替换所有数据。

znode的主要特点如下:

·znode中仅存储协调数据,即与同步相关的数据,例如状态信息、配置内容、位置信息等,因此数据量很小,大概B到KB量级。

·一个znode维护一个状态结构,该结构包括版本号、ACL(访问控制列表)变更、时间戳。znode存储的数据每发生一次变化,版本号都会递增。当客户端检索数据时,会同时接收到数据的版本。客户端也可以基于版本号检索相关数据。

·每个znode都有一个ACL,用来限定该znode的客户端访问权限。

·客户端可以在znode上设置一个观察者(Watcher),如果该znode上的数据发生变更,ZooKeeper就会通知客户端,从而触发Watcher中实现的逻辑的执行。

6.1.4 节点类型

ZooKeeper中的znode节点主要有以下四种类型。

1.持久节点(PERSISTENT)

持久节点在创建后就一直存在,除非手动将其删除。

2.持久顺序节点(PERSISTENT _SEQUENTIAL)

持久顺序节点除了有持久节点的功能外,在创建时,ZooKeeper会在节点名称末尾自动追加一个自增长的数字后缀作为新的节点名称,以便记录每一个节点创建的先后顺序。数字后缀的长度是10位,且由0填充,例如0000000001。举个例子,当前有一个父节点/lock,我们需要在该节点下创建顺序子节点/lock/node-。ZooKeeper在生成该子节点时,会根据当前子节点数量自动增加数字后缀,如果是第一个创建的子节点,则节点名称为/lock/node-0000000000,下一个子节点则为/lock/node-0000000001,依次类推。

3.临时节点(EPHEMERAL)

只要创建节点的客户端与ZooKeeper服务器的连接会话是活动的,就存在临时节点。当客户端与服务器的连接会话断开时,临时节点将被删除。基于此,临时节点是不允许有子节点的。

4.临时顺序节点(EPHEMERAL _SEQUENTIAL)

临时顺序节点除了有临时节点的功能外,节点在创建时,会在节点末尾追加自增长的数字编号,这一点与持久顺序节点的顺序功能一致。

6.1.5 Watcher机制

ZooKeeper是一个基于Watcher(观察者)模式设计的分布式服务管理框架,其允许客户端向服务器的znode上注册一个Watcher,一旦znode的状态发生变化,ZooKeeper就会通知已经在它上面注册的Watcher做出相应的反应。当前,ZooKeeper有四种状态变化事件:节点创建、节点删除、节点数据修改和子节点变更。

ZooKeeper中有三种读取操作:getData()方法、getChildren()方法和exists()方法。这三种方法都可以向服务器设置一个Watcher。Watcher事件相当于一次性的触发器,当znode的数据发生改变时,会通知设置Watcher的客户端。例如,如果客户端执行getData(“/znode1”,true)方法,然后改变或删除/znode1的数据,客户端将获得/znode1的状态改变事件通知。如果/znode1再次更改,则不会发送任何通知给客户端,除非客户端提前再次向/znode1设置Watcher。

ZooKeeper的Watcher有两种类型:数据Watcher和子节点Watcher。数据Watcher只监听节点元数据的改变,子节点Watcher只监听节点的子节点的创建与删除。getData()方法和exists()方法可以设置数据Watcher,这两个方法返回znode节点的元数据信息。getChildren()方法可以设置子节点Watcher,该方法则返回一个子节点列表。因此,setData()方法会触发数据Watcher,一个成功的create()方法将触发正在创建的znode的数据Watcher以及父znode的子节点Watcher;一个成功的delete()方法将会为被删除的znode触发一个数据Watcher以及为被删除节点的父节点触发一个子节点Watcher。

1.Watcher机制执行流程

Watcher机制主要包括三个部分:客户端线程、客户端WatchManager和ZooKeeper服务器。具体流程为:客户端在向ZooKeeper服务器注册Watcher的同时,会将Watcher对象存储在客户端的WatchManager中。当ZooKeeper服务器端触发Watcher事件后,会向客户端发送通知,客户端线程从WatchManager中取出对应的Watcher对象来执行回调逻辑,如图6-3所示。

图6-3 ZooKeeper Watcher机制执行流程

WatchManager类的部分源码如下:

     /**
      * 管理Watcher
      */
     public class WatchManager {
       private final HashMap<String, HashSet<Watcher>> watchTable =
           new HashMap<String, HashSet<Watcher>>();
  
       /**
        * 添加Watcher
        */
       public synchronized void addWatch(String path, Watcher watcher) {
         //根据路径获取对应的所有watcher
         HashSet<Watcher> list = watchTable.get(path);
         if (list == null) {
           //新生成Set集合用于存放watcher
           list = new HashSet<Watcher>(4);
           watchTable.put(path, list);
         }
         list.add(watcher);
         HashSet<String> paths = watch2Paths.get(watcher);
         if (paths == null) {
           paths = new HashSet<String>();
           //将watcher和对应的paths添加至映射中
           watch2Paths.put(watcher, paths);
         }
         paths.add(path);
       }
     }

2.Watcher相关事件

我们可以调用exists()、getData()和getChildren()三个方法来设置Watcher,这些方法主要用于读取ZooKeeper的状态信息。常用的设置Watcher事件的方法如下:

·节点创建事件:通过调用exists()方法设置。

·节点删除事件:通过调用exists()、getData()和getChildren()方法设置。

·节点改变事件:通过调用exists()和getData()方法设置。

·子节点事件:通过调用getChildren()方法设置。

6.1.6 分布式锁

在分布式环境中,为了保证在同一时刻只能有一个客户端对指定的数据进行访问,需要使用分布式锁技术,只有获得锁的客户端才能对数据进行访问,其余客户端只能暂时等待。

利用ZooKeeper实现分布式锁,常用的实现方法是,所有希望获得锁的客户端都需要执行以下操作:

(1)客户端连接ZooKeeper,调用create()方法在指定的锁节点(如/lock)下创建一个临时顺序节点。例如节点名为“node-”,则第一个客户端创建的节点为“/lock/node-0000000000”,第二个客户端创建的节点为“/lock/node-0000000001”。

(2)客户端调用getChildren()方法查询锁节点/lock下的所有子节点列表,判断子节点列表中序号最小的子节点是否是自己创建的。如果是,则客户端获得锁,否则监听排在自己前一位的子节点的删除事件,若监听的子节点被删除,则重复执行此步骤,直至获得锁。

(3)客户端执行业务代码。

(4)客户端业务完成后,删除在ZooKeeper中对应的子节点以释放锁。

针对上述流程中的两个不容易理解的问题解析如下:

操作(1)中为什么要创建临时节点?

假如客户端A获得锁之后,客户端A所在的计算机宕机了,此时客户端A没有来得及主动删除子节点。如果创建的是永久节点,锁将永远不会被释放,从而导致死锁。临时节点的好处是,尽管客户端宕机了,但是ZooKeeper在一定时间内没有收到客户端的心跳则会认为会话失效,然后将临时节点删除以释放锁。

操作(2)中未获得锁的客户端为什么要监听排在自己前一位的子节点的删除事件?

按照争夺锁的规则,每一轮锁的争夺取的都是序号最小节点,当序号最小的节点删除后,正常情况排在最小节点后一位的节点将获得锁,以此类推。因此,若客户端没有获得锁,只需要监听自己前一位的节点即可,这样每当锁释放时,ZooKeeper只需要通知一个客户端,从而节省了网络带宽。若将监听事件设置在父节点/lock上,那么每次锁的释放将通知所有客户端。假如客户端数量庞大,会导致ZooKeeper服务器必须处理的操作数量激增,增加了ZooKeeper服务器的压力,同时很容易产生网络阻塞。

上述使用ZooKeeper实现分布式锁的流程如图6-4所示。

图6-4 ZooKeeper分布式锁实现流程 CgfPAX4miEPwieGa0I4s5VWv2eUx1ywYT7vFNucjOJOiKgA3c7f7hvF+v6qnJbFd

点击中间区域
呼出菜单
上一章
目录
下一章
×