Zookeeper概念和实现

转载:https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/

转载:http://www.cnblogs.com/ggjucheng/p/3370359.html

转载:理解zookeeper选举机制

转载: zab协议(zookeeper atomic broadcast)原子广播

一、Zookeeper的定义

ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务,是Google的Chubby一个开源的实现,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。最终,将简单易用的接口和性能高效、功能稳定的系统提供给用户。

Zookeeper 作为一个分布式的服务框架,主要用来解决分布式集群中应用系统的一致性问题,它能提供基于类似于文件系统的目录节点树方式的数据存储,但是 Zookeeper 并不是用来专门存储数据的,它的作用主要是用来维护和监控你存储的数据的状态变化。通过监控这些数据状态的变化,从而可以达到基于数据的集群管理。

二、Zookeeper的文件系统

Zookeeper的文件系统是Zookeeper的静态表现,文件系统中包含了数据模型、时间属性和用户角色三种

1. 数据模型znode

ZooKeeper的文件系统拥有一个层次的命名空间,这个和标准的文件系统非常相似。


(1) 引用方式
Zonde通过路径引用,如同Unix中的文件路径。路径必须是绝对的,因此他们必须由斜杠字符来开头。除此以外,他们必须是唯一的,也就是说每一个路径只有一个表示,因此这些路径不能改变。在ZooKeeper中,路径由Unicode字符串组成,并且有一些限制。字符串"/zookeeper"用以保存管理信息,比如关键配置信息。

(2) Znode结构
ZooKeeper命名空间中的Znode,兼具文件目录两种特点。既像文件一样维护着数据、元信息、ACL、时间戳等数据结构,又像目录一样可以作为路径标识的一部分。图中的每个节点称为一个Znode。 每个Znode由3部分组成:
stat:此为状态信息, 描述该Znode的版本, 权限等信息
data:与该Znode关联的数据
children:该Znode下的子节点

ZooKeeper虽然可以关联一些数据,但并没有被设计为常规的数据库或者大数据存储,相反的是,它用来管理调度数据,比如分布式应用中的配置文件信息、状态信息、汇集位置等等。这些数据的共同特性就是它们都是很小的数据,通常以KB为大小单位。ZooKeeper的服务器和客户端都被设计为严格检查并限制每个Znode的数据大小至多1M,但常规使用中应该远小于此值。


(3) 数据访问
ZooKeeper中的每个节点存储的数据要被原子性的操作。也就是说读操作将获取与节点相关的所有数据,写操作也将替换掉节点的所有数据。另外,每一个节点都拥有自己的ACL(访问控制列表),这个列表规定了用户的权限,即限定了特定用户对目标节点可以执行的操作。

(4) 节点类型
ZooKeeper中的节点有两种,分别为临时节点和永久节点。节点的类型在创建时即被确定,并且不能改变。
1、PERSISTENT-持久化目录节点 
客户端与zookeeper断开连接后,该节点依旧存在 
2、PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点 
客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号 
3、EPHEMERAL-临时目录节点 
客户端与zookeeper断开连接后,该节点被删除 
4、EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点 
客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号 

(5) 顺序节点
当创建Znode的时候,用户可以请求在ZooKeeper的路径结尾添加一个递增的计数。这个计数对于此节点的父节点来说是唯一的,它的格式为"%10d"(10位数字,没有数值的数位用0补充,例如"0000000001")。当计数值大于232-1时,计数器将溢出。

(6) 观察

客户端可以在节点上设置watch,我们称之为监视器。当节点状态发生改变时(Znode的增、删、改)将会触发watch所对应的操作。当watch被触发时,ZooKeeper将会向客户端发送且仅发送一条通知,因为watch只能被触发一次,这样可以减少网络流量。


2.  时间属性Zxid

ZooKeeper有多种记录时间的形式,其中包含以下几个主要属性:
(1)Zxid
致使ZooKeeper节点状态改变的每一个操作都将使节点接收到一个zxid格式的时间戳,并且这个时间戳全局有序。也就是说,也就是说,每个对节点的改变都将产生一个唯一的zxid。如果zxid1的值小于zxid2的值,那么zxid1所对应的事件发生在zxid2所对应的事件之前。实际上,ZooKeeper的每个节点维护者三个zxid值,为别为:cZxid、mZxid、pZxid

  1. cZxid: 是节点的创建时间所对应的Zxid格式时间戳。
  2. mZxid:是节点的修改时间所对应的Zxid格式时间戳。
  3. pZxid:是节点的最近修改所对应的Zxid格式时间戳。
实现中zxid是一个64为的数字,它高32位是 epoch,用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个 新的epoch。低32位是个 递增计数
(2)版本号
对节点的每一个操作都将致使这个节点的版本号增加。每个节点维护着三个版本号,他们分别为:
  1. version   节点数据版本号
  2. cversion 子节点版本号
  3. aversion 节点所拥有的ACL版本号
(1)znode中的数据可以有多个版本,在查询该znode数据时就需要带上版本信息。如:set path version / delete path version
(2)znode可以是临时znode,由create -e 生成的节点,一旦创建这个znode的client与server断开连接,该znode将被自动删除。
client和server之间通过heartbeat来确认连接正常,这种状态称之为session,断开连接后session失效。
(3)临时znode不能有子znode。
(4)znode可以自动编号,由create -s 生成的节点,例如在 create -s /app/node 已存在时,将会生成 /app/node00***001节点。
(5)znode可以被监控,该目录下某些信息的修改,例如节点数据、子节点变化等,可以主动通知监控注册的client。事实上,通过这个特性,可以完成许多重要应用,例如配置管理、信息同步、分布式锁等等。

3. 用户角色

Zookeeper中的角色主要有以下三类,如下图所示:

zookeeper简介

ZooKeeper中包含LeaderFollowerObserver三个角色;
通过一次选举过程,被选举的机器节点被称为Leader,Leader机器为客户端提供读和写服务;FollowerObserver是集群中的其他机器节点,唯一的区别就是:Observer不参与Leader的选举过程,也不参与写操作的过半写成功策略。

系统模型如下图所示:

zookeeper简介

实际使用中Client可能会读写同一个znode(配置同步)或者不同znode(分布锁)。


4. 文件系统的操作

在ZooKeeper中文件系统有9个基本操作,如下图所示:

详细操作函数如下:

方法名 方法功能描述
String create(String path, byte[] data, List<ACL> acl, CreateMode createMode) 创建一个给定的目录节点 path, 并给它设置数据,CreateMode 标识有四种形式的目录节点,分别是 PERSISTENT;PERSISTENT_SEQUENTIAL;EPHEMERAL;EPHEMERAL_SEQUENTIAL
Stat exists(String path, boolean watch) 判断某个 path 是否存在,并设置是否监控这个目录节点,这里的 watcher 是在创建 ZooKeeper 实例时指定的 watcher,exists方法还有一个重载方法,可以指定特定的 watcher
Stat exists(String path, Watcher watcher) 重载方法,这里给某个目录节点设置特定的 watcherWatcher  ZooKeeper 是一个核心功能,Watcher 可以监控目录节点的数据变化以及子目录的变化,一旦这些状态发生变化,服务器就会通知所有设置在这个目录节点上的 Watcher,从而每个客户端都很快知道它所关注的目录节点的状态发生变化,而做出相应的反应
void delete(String path, int version) 删除 path 对应的目录节点,version  -1 可以匹配任何版本,也就删除了这个目录节点所有数据
List<StringgetChildren(String path, boolean watch) 获取指定 path 下的所有子目录节点,同样 getChildren方法也有一个重载方法可以设置特定的 watcher 监控子节点的状态
Stat setData(String path, byte[] data, int version) path 设置数据,可以指定这个数据的版本号,如果 version  -1 怎可以匹配任何版本
byte[] getData(String path, boolean watch, Stat stat) 获取这个 path 对应的目录节点存储的数据,数据的版本等信息可以通过 stat 来指定,同时还可以设置是否监控这个目录节点数据的状态
void addAuthInfo(String scheme, byte[] auth) 客户端将自己的授权信息提交给服务器,服务器将根据这个授权信息验证客户端的访问权限。
Stat setACL(String path, List<ACL> acl, int version) 给某个目录节点重新设置访问权限,需要注意的是 Zookeeper 中的目录节点权限不具有传递性,父目录节点的权限不能传递给子目录节点。目录节点 ACL 由两部分组成:perms  id。Perms 有 ALL、READ、WRITE、CREATE、DELETE、ADMIN 几种,而 id 标识了访问目录节点的身份列表,默认情况下有以下两种:ANYONE_ID_UNSAFE = new Id("world", "anyone") 和 AUTH_IDS = new Id("auth", "") 分别表示任何人都可以访问和创建者拥有访问权限。
List<ACLgetACL(String path, Stat stat) 获取某个目录节点的访问权限列表

更新ZooKeeper操作是有限制的。delete或setData必须明确要更新的Znode的版本号,我们可以调用exists找到。如果版本号不匹配,更新将会失败。
更新ZooKeeper操作是非阻塞式的。因此客户端如果失去了一个更新(由于另一个进程在同时更新这个Znode),他可以在不阻塞其他进程执行的情况下,选择重新尝试或进行其他操作。
尽管ZooKeeper可以被看做是一个文件系统,但是处于便利,摒弃了一些文件系统的操作原语。因为文件非常的小并且使整体读写的,所以不需要打开、关闭或是寻地的操作。

三、Zookeeper的通知机制

Zookeeper的通知机制是其动态实现,也是其功能主题,通知通过原语触发器实现。

1. Zab协议

Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和leader的状态同步以后,恢复模式就结束了。状态同步保证了leader和Server具有相同的系统状态


为了保证事务的顺序一致性,zookeeper采用了递增的事务id号(zxid)来标识事务。所有的提议(proposal)都在被提出的时候加上了zxid。实现中zxid是一个64位的数字,它高32位是epoch用来标识leader关系是否改变,每次一个leader被选出来,它都会有一个新的epoch,标识当前属于那个leader的统治时期。低32位用于递增计数。


每个Server在工作过程中有三种状态:

  • LOOKING:当前Server不知道leader是谁,正在搜寻
  • LEADING:当前Server即为选举出来的leader
  • FOLLOWING:leader已经选举出来,当前Server与之同步

2. Watch触发器

读操作existsgetChildrengetData都被设置了watch,并且这些watch都由写操作来触发:createdeletesetDataACL操作并不参与到watch中。当watch被触发时,watch事件被生成,他的类型由watch和触发他的操作共同决定。ZooKeeper所管理的watch可以分为两类:

  • 1.数据watch(data watches):getData和exists负责设置数据watch;
  • 2.孩子watch(child watches):getChildren负责设置孩子watch;
我们可以通过操作返回的数据来设置不同的watch:
  • 1.getData和exists:返回关于节点的数据信息
  • 2.getChildren:返回孩子列表
因此:

  1. 一个成功的setData操作将触发Znode的数据watch
  2. 一个成功的create操作将触发Znode的数据watch以及孩子watch
  3. 一个成功的delete操作将触发Znode的数据watch以及孩子watch
watch由客户端所连接的ZooKeeper服务器在本地维护,因此watch可以非常容易地设置、管理和分派。当客户端连接到一个新的服务器上时,任何的会话事件都将可能触发watch。另外,当从服务器断开连接的时候,watch将不会被接收。但是,当一个客户端重新建立连接的时候,任何先前注册过的watch都会被重新注册
  1. exists操作上的watch,在被监视的Znode创建、删除数据更新时被触发。
  2. getData操作上的watch,在被监视的Znode删除数据更新时被触发。在被创建时不能被触发,因为只有Znode一定存在,getData操作才会成功。
  3. getChildren操作上的watch,在被监视的Znode的子节点创建或删除,或是这个Znode自身被删除时被触发。可以通过查看watch事件类型来区分是Znode还是他的子节点被删除:NodeDelete表示Znode被删除,NodeDeletedChanged表示子节点被删除。
watch设置操作及相应的触发器如图下图所示:


watch事件包括了事件所涉及的Znode的路径,因此对于NodeCreated和NodeDeleted事件来说,根据路径就可以简单区分出是哪个Znode被创建或是被删除了。为了查询在NodeChildrenChanged事件后哪个子节点被改变了,需要再次调用getChildren来获得新的children列表。同样的,为了查询NodeDeletedChanged事件后产生的新数据,需要调用getData。在两种情况下,Znode可能在获取watch事件或执行读操作这两种状态下切换,在写应用程序时,必须记住这一点。

Zookeeper的watch实际上要处理两类事件:
1. 连接状态事件(type=None, path=null)
这类事件不需要注册,也不需要我们连续触发,我们只要处理就行了。
2. 节点事件
节点的建立,删除,数据的修改。它是one time trigger,我们需要不停的注册触发,还可能发生事件丢失的情况。
上面2类事件都在Watch中处理,也就是重载的process(Event event)
节点事件的触发,通过函数exists,getData或getChildren来处理
这类函数,有双重作用:
1. 注册触发事件
2. 函数本身的功能
函数的本身的功能又可以用异步的回调函数来实现,重载processResult()过程中处理函数本身的的功能。
函数还可以指定自己的watch,所以每个函数都有4个版本。根据自己的需要来选择不同的函数,不同的版本。

3 ZooKeeper访问控制列表ACL

ZooKeeper使用ACL来对Znode进行访问控制。ACL的实现和Unix文件访问许可非常相似:它使用许可位来对一个节点的不同操作进行允许或禁止的权限控制。但是,和标准的Unix许可不同的是,Zookeeper对于用户类别的区分,不止局限于所有者(owner)、组 (group)、所有人(world)三个级别。Zookeeper中,数据节点没有“所有者”的概念。访问者利用id标识自己的身份,并获得与之相应的 不同的访问权限。
注意:
传统的文件系统中,ACL分为两个维度,一个是属组,一个是权限,子目录/文件默认继承父目录的ACL。而在Zookeeper中一个ACL和一个ZooKeeper节点相对应。并且,父节点的ACL与子节点的ACL是相互独立的。也就是说,ACL不能被子节点所继承,父节点所拥有的权限与子节点所用的权限都没有任何关系。
Zookeeper支持可配置的认证机制。它利用一个三元组来定义客户端的访问权限:(scheme:expression, perms) 。其中:
1.scheme:定义了expression的含义。
如:(host:host1.corp.com,READ),标识了一个名为host1.corp.com的主机,有该数据节点的读权限。
2.Perms:标识了操作权限。
如:(ip:19.22.0.0/16, READ),表示IP地址以19.22开头的主机,有该数据节点的读权限。
Zookeeper的ACL也可以从三个维度来理解:一是,scheme; 二是,user; 三是,permission,通常表示为scheme:id:permissions,如下图所示。


scheme有如下几种分类:


Perms有如下几种类型:


当会话建立的时候,客户端将会进行自我验证。另外,ZooKeeper Java API支持三种标准的用户权限,它们分别为:
1.ZOO_PEN_ACL_UNSAFE:对于所有的ACL来说都是完全开放的,任何应用程序可以在节点上执行任何操作,比如创建、列出并删除子节点。
2.ZOO_READ_ACL_UNSAFE:对于任意的应用程序来说,仅仅具有读权限。
3.ZOO_CREATOR_ALL_ACL:授予节点创建者所有权限。需要注意的是,设置此权限之前,创建者必须已经通了服务器的认证。

四、Zookeeper的执行过程

ZooKeeper的思想非常简单:他所需要做的就是保证对Znode树的每一次修改都复制到ensemble中的大部分机器上去。如果机器中的小部分出故障了,那么至少有一台机器将会恢复到最新状态,其他的则保存这副本,直到最终达到最新状态。Zookeeper采用Zab协议,它分为两个阶段,并且可能被无限的重复。

1. 领导者选举

在ensemble中的机器要参与一个选择特殊成员的进程,这个成员叫领导者,其他机器脚跟随者。在大部分的跟随者与他们的领导者同步了状态以后,这个阶段才算完成。

当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,恢复模式需要重新选举出一个新的leader,让所有的Server都恢复到一个正确的状态。Zk的选举算法有两种:一种是基于basic paxos实现的,另外一种是基于fast paxos算法实现的。系统默认的选举算法为fast paxos。先介绍basic paxos流程:

  • 1 .选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server;
  • 2 .选举线程首先向所有Server发起一次询问(包括自己);
  • 3 .选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(id,zxid),并将这些信息存储到当次选举的投票记录表中;
  • 4.  收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次要投票的Server
  • 5.  线程将当前zxid最大的Server设置为当前Server要推荐的Leader,如果此时获胜的Server获得n/2 + 1的Server票数, 设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置自己的状态,否则,继续这个过程,直到leader被选举出来。

通过流程分析我们可以得出:要使Leader获得多数Server的支持,则Server总数必须是奇数2n+1,且存活的Server的数目不得少于n+1。

fast paxos流程是在选举过程中,某Server首先向所有Server提议自己要成为leader,当其它Server收到提议以后,解决epoch和zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,最后一定能选举出Leader。


2:原子广播

所有的写操作请求被传送给领导者,并通过广播将更新信息告诉跟随者。当大部分跟随者执行了修改之后,领导者就提交更新操作,客户端将得到更新成功的回应。未获得一致性的协议被设计为原子的,因此无论修改失败与否,他都分两阶段提交。

状态切换图

如果领导者出故障了,剩下的机器将会再次进行领导者选举,并在新领导被选出前继续执行任务。如果在不久后老的领导者恢复了,那么它将以跟随者的身份继续运行。领导者选举非常快,由发布的结果所知,大约是200毫秒,因此在选举时性能不会明显减慢。

1. leader等待server连接;
2 .Follower连接leader,将最大的zxid发送给leader;
3 .Leader根据follower的zxid确定同步点;
4 .完成同步后通知follower 已经成为uptodate状态;
5 .Follower收到uptodate消息后,又可以重新接受client的请求进行服务了。

zookeeper简介

每一个Znode树的更新都会给定一个唯一的全局标识,叫zxid(表示ZooKeeper事务“ID”)。更新是被排序的,因此如果zxid的z1<z2,那么z1就比z2先执行。对于ZooKeeper来说,这是分布式系统中排序的唯一标准。

所有在ensemble中的机器在更新它们内存中的Znode树之前会先将更新信息写入磁盘。读操作请求可由任何机器服务,同时,由于他们只涉及内存查找,因此非常快。

 3. Leader工作流程

Leader主要有三个功能:

  • 1 .恢复数据
  • 2 .维持与Learner的心跳,接收Learner请求并判断Learner的请求消息类型;
  • 3 .Learner的消息类型主要有PING消息、REQUEST消息、ACK消息、REVALIDATE消息,根据不同的消息类型,进行不同的处理。

PING消息是指Learner的心跳信息;

REQUEST消息是Follower发送的提议信息,包括写请求及同步请求;

ACK消息是Follower的对提议的回复,超过半数的Follower通过,则commit该提议;

REVALIDATE消息是用来延长SESSION有效时间。

zookeeper简介

4. Follower工作流程

Follower主要有四个功能:

  • 1. 向Leader发送请求(PING消息、REQUEST消息、ACK消息、REVALIDATE消息);
  • 2 .接收Leader消息并进行处理;
  • 3 .接收Client的请求,如果为写请求,发送给Leader进行投票;
  • 4 .返回Client结果。

Follower的消息循环处理如下几种来自Leader的消息:

  • 1 .PING消息: 心跳消息;
  • 2 .PROPOSAL消息:Leader发起的提案,要求Follower投票;
  • 3 .COMMIT消息:服务器端最新一次提案的信息;
  • 4 .UPTODATE消息:表明同步完成;
  • 5 .REVALIDATE消息:根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息;
  • 6 .SYNC消息:返回SYNC结果到客户端,这个消息最初由客户端发起,用来强制得到最新的更新。
Follower的工作流程简图如下所示,在实际实现中,Follower是通过5个线程来实现功能的。

zookeeper简介

五、ZooKeeper会话

ZooKeeper客户端与ensemble中的服务器列表配置一致,在启动时,他尝试与表中的一个服务器相连接。如果连接失败了,他就尝试表中的其他服务器,以此类推,知道他最终连接到其中一个,或者ZooKeeper的所有服务器都无法获得时,连接失败。
一旦与ZooKeeper服务器连接成功,服务器会创建与客户端的一个新的对话。每个回话都有超时时段,这是应用程序在创建它时设定的。如果服务器没有在超时时段内得到请求,他可能会中断这个会话。一旦会话被中断了,他可能不再被打开,而且任何与会话相连接的临时节点都将丢失。
无论什么时候会话持续空闲长达一定时间,都会由客户端发送ping请求保持活跃(犹如心跳)。时间段要足够小以监测服务器故障(由读操作超时反应),并且能再回话超市时间段内重新连接到另一个服务器。
在ZooKeeper中有几个time参数。tick time是ZooKeeper中的基本时间长度,为ensemble里的服务器所使用,用来定义对于交互运行的调度。其他设置以tick time的名义定义,或者至少由它来约束。
创建更复杂的临时性状态的应用程序应该支持更长的会话超时,因为重新构建的代价会更昂贵。在一些情况下,我们可以让应用程序在一定会话时间内能够重启,并且避免会话过期。(这可能更适合执行维护或是升级)每个会话都由服务器给定一个唯一的身份和密码,而且如果是在建立连接时被传递给ZooKeeper的话,只要没有过期它能够恢复会话。
这些特性可以视为一种可以避免会话过期的优化,但它并不能代替用来处理会话过期。会话过期可能出现在机器突然故障时,或是由于任何原因导致的应用程序安全关闭了,但在会话中断前没有重启。

六、Zookeeper的实例状态

Zookeeper对象的转变是通过其生命周期中的不同状态来实现。可以使用getState()方法在任何时候去查询他的状态,Zookeeper状态事务如下所示:


getState()方法的返回类型是states,states是枚举类型代表Zookeeper对象可能所处的不同状态,一个Zookeeper实例可能一次只处于一个状态。一个新建的Zookeeper实例正在于Zookeeper服务器建立连接时,是处于CONNECTING状态的。一旦连接建立好以后,他就变成了Connected状态。
使用Zookeeper的客户端可以通过注册Watcher的方法来获取状态转变的消息。一旦进入了CONNNECTED状态,Watcher将获得一个KeepState值为SyncConnected的WatchedEvent。


七、Zookeeper的设计目的

1.最终一致性:client不论连接到哪个Server,展示给它都是同一个视图,这是zookeeper最重要的性能。
2 .可靠性:具有简单、健壮、良好的性能,如果消息m被到一台服务器接受,那么它将被所有的服务器接受。
3 .实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口。
4 .等待无关(wait-free):慢的或者失效的client不得干预快速的client的请求,使得每个client都能有效的等待。
5.原子性:更新只能成功或者失败,没有中间状态。
6 .顺序性:包括全局有序和偏序两种:全局有序是指如果在一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面。


八、Zookeeper的适用场景

1. 统一命名服务(Name Service)

分布式应用中,通常需要有一套完整的命名规则,既能够产生唯一的名称又便于人识别和记住,通常情况下用树形的名称结构是一个理想的选择,树形的名称结构是一个有层次的目录结构,既对人友好又不会重复。说到这里你可能想到了 JNDI(Tomcat里曾经使用JNDI引入资源),没错 Zookeeper 的 Name Service 与 JNDI 能够完成的功能是差不多的,它们都是将有层次的目录结构关联到一定资源上,但是 Zookeeper 的 Name Service 更加是广泛意义上的关联,也许你并不需要将名称关联到特定资源上,你可能只需要一个不会重复名称,就像数据库中产生一个唯一的数字主键一样。
Name Service 已经是 Zookeeper 内置的功能,你只要调用 Zookeeper 的 API 就能实现。如调用 create 接口就可以很容易创建一个目录节点。

2. 配置管理(Configuration Management)

配置的管理在分布式应用环境中很常见,例如同一个应用系统需要多台 PC Server 运行,但是它们运行的应用系统的某些配置项是相同的,如果要修改这些相同的配置项,那么就必须同时修改每台运行这个应用系统的 PC Server,这样非常麻烦而且容易出错。
像这样的配置信息完全可以交给 Zookeeper 来管理,将配置信息保存在 Zookeeper 的某个目录节点中,然后将所有需要修改的应用机器监控配置信息的状态,一旦配置信息发生变化,每台应用机器就会收到 Zookeeper 的通知,然后从 Zookeeper 获取新的配置信息应用到系统中。

图 2. 配置管理结构图

3. 集群管理(Group Membership)

Zookeeper 能够很容易的实现集群管理的功能,如有多台 Server 组成一个服务集群,那么必须要一个“总管”知道当前集群中每台机器的服务状态,一旦有机器不能提供服务,集群中其它集群必须知道,从而做出调整重新分配服务策略。同样当增加集群的服务能力时,就会增加一台或多台 Server,同样也必须让“总管”知道。
Zookeeper 不仅能够帮你维护当前的集群中机器的服务状态,而且能够帮你选出一个“总管”,让这个总管来管理集群,这就是 Zookeeper 的另一个功能 Leader Election。
它们的实现方式都是在 Zookeeper 上创建一个 EPHEMERAL 类型的目录节点,然后每个 Server 在它们创建目录节点的父目录节点上调用getChildren(String path, boolean watch) 方法并设置 watch 为 true,由于是 EPHEMERAL 目录节点,当创建它的 Server 死去,这个目录节点也随之被删除,所以 Children 将会变化,这时 getChildren上的 Watch 将会被调用,所以其它 Server 就知道已经有某台 Server 死去了。新增 Server 也是同样的原理。
Zookeeper 如何实现 Leader Election,也就是选出一个 Master Server。和前面的一样每台 Server 创建一个 EPHEMERAL 目录节点,不同的是它还是一个SEQUENTIAL 目录节点,所以它是个EPHEMERAL_SEQUENTIAL 目录节点。之所以它是 EPHEMERAL_SEQUENTIAL 目录节点,是因为我们可以给每台 Server 编号,我们可以选择当前是最小编号的 Server 为 Master,假如这个最小编号的 Server 死去,由于是 EPHEMERAL 节点,死去的 Server 对应的节点也被删除,所以当前的节点列表中又出现一个最小编号的节点,我们就选择这个节点为当前 Master。这样就实现了动态选择 Master,避免了传统意义上单 Master 容易出现单点故障的问题。

图 3. 集群管理结构图

Leader Election 关键代码

void findLeader() throws InterruptedException { 
       byte[] leader = null; 
       try { 
           leader = zk.getData(root + "/leader", true, null); 
       } catch (Exception e) { 
           logger.error(e); 
       } 
       if (leader != null) { 
           following(); 
       } else { 
           String newLeader = null; 
           try { 
               byte[] localhost = InetAddress.getLocalHost().getAddress(); 
               newLeader = zk.create(root + "/leader", localhost, 
               ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); 
           } catch (Exception e) { 
               logger.error(e); 
           } 
           if (newLeader != null) { 
               leading(); 
           } else { 
               mutex.wait(); 
           } 
       } 
   }

4. 共享锁(Locks)

共享锁在同一个进程中很容易实现,但是在跨进程或者在不同 Server 之间就不好实现了。Zookeeper 却很容易实现这个功能,实现方式也是需要获得锁的 Server 创建一个EPHEMERAL_SEQUENTIAL目录节点,然后调用getChildren方法获取当前的目录节点列表中最小的目录节点是不是就是自己创建的目录节点,如果正是自己创建的,那么它就获得了这个锁,如果不是那么它就调用exists(String path, boolean watch) 方法并监控 Zookeeper 上目录节点列表的变化,一直到自己创建的节点是列表中最小编号的目录节点,从而获得锁,释放锁很简单,只要删除前面它自己所创建的目录节点就行了。

图 4. Zookeeper 实现 Locks 的流程图


同步锁的关键代码

void getLock() throws KeeperException, InterruptedException{ 
       List<String> list = zk.getChildren(root, false); 
       String[] nodes = list.toArray(new String[list.size()]); 
       Arrays.sort(nodes); 
       if(myZnode.equals(root+"/"+nodes[0])){ 
           doAction(); 
       } 
       else{ 
           waitForLock(nodes[0]); 
       } 
   } 
   void waitForLock(String lower) throws InterruptedException, KeeperException {
       Stat stat = zk.exists(root + "/" + lower,true); 
       if(stat != null){ 
           mutex.wait(); 
       } 
       else{ 
           getLock(); 
       } 
   }

5. 队列管理

Zookeeper 可以处理两种类型的队列:
  1. 当一个队列的成员都聚齐时,这个队列才可用,否则一直等待所有成员到达,这种是同步队列。
  2. 队列按照 FIFO 方式进行入队和出队操作,例如实现生产者和消费者模型。
同步队列用 Zookeeper 实现的实现思路如下:
创建一个父目录 /synchronizing,每个成员都监控标志(Set Watch)位目录 /synchronizing/ start 是否存在,然后每个成员都加入这个队列,加入队列的方式就是创建 /synchronizing/ member_i 的临时目录节点,然后每个成员获取 / synchronizing 目录的所有目录节点,也就是 member_i。判断 i 的值是否已经是成员的个数,如果小于成员个数等待 /synchronizing/start 的出现,如果已经相等就创建 /synchronizing/start。

图 5. 同步队列流程图

同步队列
void addQueue() throws KeeperException, InterruptedException{ 
       zk.exists(root + "/start",true); 
       zk.create(root + "/" + name, new byte[0], Ids.OPEN_ACL_UNSAFE, 
       CreateMode.EPHEMERAL_SEQUENTIAL); 
       synchronized (mutex) { 
           List<String> list = zk.getChildren(root, false); 
           if (list.size() < size) { 
               mutex.wait(); 
           } else { 
               zk.create(root + "/start", new byte[0], Ids.OPEN_ACL_UNSAFE,
                CreateMode.PERSISTENT); 
           } 
       } 
}
当队列没满是进入 wait(),然后会一直等待 Watch 的通知,Watch 的代码如下:
public void process(WatchedEvent event) { 
       if(event.getPath().equals(root + "/start") &&
        event.getType() == Event.EventType.NodeCreated){ 
           System.out.println("得到通知"); 
           super.process(event); 
           doAction(); 
       } 
   }
FIFO 队列用 Zookeeper 实现思路如下:
实现的思路也非常简单,就是在特定的目录下创建 SEQUENTIAL 类型的子目录 /queue_i,这样就能保证所有成员加入队列时都是有编号的,出队列时通过 getChildren( ) 方法可以返回当前所有的队列中的元素,然后消费其中最小的一个,这样就能保证 FIFO。





  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值