七、ZooKeeper使用方式和常用解决方案

ZK高级实现指南

此文包含使用ZK实现高级定制功能的指南,所有这些功能都是在客户端实现的,而且并不需要ZK的特殊修改。希望社区可以看到这些客户端方面的的约定,使ZK的使用更优雅,并促使这成为一种标准。

关于ZK,一个最有意思的事情就是ZK使用异步通知,但是却可以构建同步一致性操作应用,例如队列和锁。如你所见,这是可行的,因为ZK更新时从头到尾都是顺序执行的,并且有使用这些顺序的内部机制。

注意,下面的ZK使用方式尝试使用这些特性实现的最佳实践。特别的,这些示例避免使用poll、定时器和能够导致“羊群效应”、拥堵爆炸和限制伸缩性的任何事情。还有很多实用的功能没包含在这些示例中-可取消的读写优先级锁,还提到一些的别的构想-锁,在一些特定的点上,你甚至可以找到一些别的构想-例如事件处理或者队列,可以使用同一功能特性实现更好的实践。通常情况下,下面的示例只是起到抛砖引玉的作用。

异常处理的重要注意事项

功能实现的同时,你需要处理一些可恢复的异常,特别是,多个示例使用序列的临时节点。创建序列临时节点时,create()在服务器端成功,但是在返回节点名称给客户端的时候服务器挂掉了,这时,客户端重连会话依然是有效的,但是因此导致这个节点未被移除。这就意味着客户端不知道这个节点是否创建成功。一些会展示解决这些问题的方式。

拆箱可用:命名服务、配置服务和分组会员服务

命名服务和配置服务是Zk的最主要的两个应用,这两个功能是由ZK API直接提供的。分组会员服务表现为一个节点,组的成员在组节点下创建临时节点,当ZK捕捉到异常时,这些临时节点会被及时删除。

栅栏-Barriers

分布式系统使用栅栏阻塞一批进程,直到某一个条件达到的时候,所有进程才被允许执行。通过设计一个栅栏节点,ZK实现了栅栏功能,当栅栏节点存在时,就节点会被新节点替换。下面是伪代码实现:

  1. 客户端调用exists()查看栅栏节点,并设置监听

  2. 如果exists()返回false,栅栏消失,客户端可以继续执行

  3. 但是,如果exists() 返回TRUE,客户端会一直监听栅栏节点

  4. 监听事件触发,客户端重新调用exists(),栅栏节点的话就继续等待

双重栅栏-Double Barriers

双重栅栏使客户端能够同步每次计算的开始和结束,当足够的处理进程加入栅栏,处理进程就会开始计算,并在完成时离开栅栏。这个示例展示了如何使用ZK节点作为栅栏节点。伪代码使用节点b作为栅栏节点,每个客户端处理进程p进入时注册到栅栏节点,离开时取消注册。每个节点通过Enter程序注册到栅栏节点,直到客户端处理进程数量达到x个时开始计算。(x的个数取决于系统)

EnterLeave
Create a name n = b+“/”+pSet watch: exists(b + ‘‘/ready’’, true)Create child: create( n, EPHEMERAL)L = getChildren(b, false)if fewer children in L than x, wait for watch eventelse create(b + ‘‘/ready’’, REGULAR)L = getChildren(b, false)if no children, exitif p is only process node in L, delete(n) and exitif p is the lowest process node in L, wait on highest process node in Lelse delete(n) if still exists and wait on lowest process node in Lgoto 1

进入后,所有处理进程在栅栏节点下创建一个临时节点,并处于准备状态。除了最后一个进入的处理进程,其他所有处理进程会一直等待着第5行的准备好的节点出现。创建最后一个(第x个)节点的处理进程会看到子节点列表,创建最后一个节点并唤醒其他处理进程。注意,只有合适时间才会唤醒等待进程,所以等待是高效的。

退出时,由于发现处理进程节点删除了,所以不能设置标记。通过使用临时节点,失败的处理进程不会阻止正确的处理进程完成任务。当处理进程退出时,需要删除对应的节点并等待别的处理进程删除它们的节点。

栅栏节点b所有子节点都被删除时,进程就结束了。然而,你可以使用最低的进程节点作为准备节点。所有准备退出的处理进程监视最小的处理进程离开,并且最小的处理进程监视等待别的进程(最高的进程)离开。这意味着除了最后一个节点,每个节点删除时只有一个处理进程被唤醒,最后一个节点会在别的都离开后才被删除。

队列-Queues

分布式队列是通用的数据结构,实现这样的功能,首先在zk中设计一个保存这个队列的节点。通过调用create()方法,分布的客户端往队列放东西,路径名称以“queue-”结尾,并设置序列标志和临时标志为true,新路径格式是这样的:_path-to-queue-node_/queue-X, X代表自增数字。客户端想要出队列时,调用ZK的getChildren()方法,并设置监视,最后开始处理具有最小编号的节点。如果队列节点下没有子节点,读取者会一直等待,直到收到监听事件,并再次检查队列节点。

注意

队列的实现代码在ZK recipes目录下,随着发布版本一起发布--src/recipes/queue.

优先级队列-Priority Queues

为了实现优先级队列,你只需在普通的队列实现方式上做两个简单的修改。首先,队列节点名称以“queue-YY”结束,YY代表着优先级,数字越小优先级越高(和UNIX一样)。第二,客户端从队列移除时,使用最新的子节点列表,意思是当收到监听通知时,获取并使用最新的子节点列表。

锁-Locks

完全分布式锁是全局同步的,意思就是在任何一个时间都不会有两个客户端持有同一个锁,使用ZK可以实现这个功能。对于优先级队列,首先创建一个锁节点。

Note:锁的实现代码在ZK recipes目录中,随着发布版本一起发布-- src/recipes/lock 。

客户端如下获取锁:

  1. 调用create()方法,路径名称"_locknode_/guid-lock-",并设置序号标记和临时节点标记,在创建结果丢失时需要guid

  2. 在锁节点下调用getChildren( )方法但不设置监听 (防止发生“羊群效应”).

  3. 如果步骤一创建的路径后缀数字是最小的,那这个客户端获取到锁

  4. 客户端调用exists( ) 并设置监听锁目录,为了下一个序列号最小的

  5. 如果exists( ) 返回false, 回到步骤2,否则一直等待直到通知最小序列号的客户端

释放锁的协议也很简单:客户端想要释放锁时只需删除在步骤1中创建的目录

这里有一些需要注意的点:

  • 一个节点的移除只会唤醒一个客户端,因为一个节点只会被一个客户端监视。这样,可以避免羊群效应

  • 没有poll或者超时

  • 由于锁的实现方式,可以很轻松查看锁连接的数量、坏锁、调试锁问题等等

可恢复错误和全局唯一标识符-Recoverable Errors and the GUID

  • 如果调用create()方法发生可恢复异常,客户端应该调用getChildren()并检查节点包含路径名称中用到的guid。这可以处理这样的情况,服务器create()执行成功,但是返回客户端前服务器的挂掉了。

共享锁-Shared Locks

通过对锁协议做少许修改就可以实现共享锁:

Obtaining a read lock:Obtaining a write lock:
Call create( ) to create a node with pathname "guid-/read-". This is the lock node use later in the protocol. Make sure to set both the sequence andephemeral flags.Call getChildren( ) on the lock node without setting the watch flag - this is important, as it avoids the herd effect.If there are no children with a pathname starting with "write-" and having a lower sequence number than the node created in step 1, the client has the lock and can exit the protocol.Otherwise, call exists( ), with watch flag, set on the node in lock directory with pathname staring with "write-" having the next lowest sequence number.If exists( ) returns false, goto step 2.Otherwise, wait for a notification for the pathname from the previous step before going to step 2Call create( ) to create a node with pathname "guid-/write-". This is the lock node spoken of later in the protocol. Make sure to set both sequenceand ephemeral flags.Call getChildren( ) on the lock node without setting the watch flag - this is important, as it avoids the herd effect.If there are no children with a lower sequence number than the node created in step 1, the client has the lock and the client exits the protocol.Call exists( ), with watch flag set, on the node with the pathname that has the next lowest sequence number.If exists( ) returns false, goto step 2. Otherwise, wait for a notification for the pathname from the previous step before going to step 2.

注意:

  • 这个实现方案可能导致羊群效应:当最小序号“write-”节点被删除时,有大量客户端等待读锁,并且这样客户端或多或少几乎同时被唤醒。事实上,这是正常的行为:因为所有的等待的读取者都有锁,所以它们应该被释放。羊群效应指的是当只有一个或小部分机器可以执行的时候释放领头羊。

可取消的共享锁-Revocable Shared Locks

对共享锁协议做最小的修改,就可以实现可恢复共享锁:

在步骤1中,获取读取者和写锁协议时,都在调用create()后立即调用getData()并设置监视。如果客户端随后收到它在步骤1创建的节点的通知,再调用getData()并设置监听,查找字符串“unlock”,这个字符串意思是客户端必须要释放锁。因为根据共享锁协议,可以通过调用setData()请求持有该锁的客户端放弃该锁,并给节点写入字符串“unlock”。

注意协议要求锁的持有者同意释放该锁,该同意许可很重要,特别是锁持有者在释放锁之前需要进行一些的处理的情况下。当然,在协议实现中,如果锁持有者一定时间内还未释放的话,也可以总是实现可以删除这个锁节点,但是这种方式很惊悚。

两阶段提交-Two-phased Commit

两阶段提交协议是一种算法,在分布式系统中,允许所有客户端一致同意提交事务或者放弃事务提交。

在ZooKeeper中,可以实现一个两阶段提交,通过协调器创建一个事务节点,称作“/app/Tx”,并创建一个参与站点的子节点,称作“/app/Tx/s_i”。当创建子节点时,内容设置为undefined,一旦加入事务的每个站点从协调器处获取到事务,那这个站点读取每个子节点并设置监听。接下来,每个处理进程执行查询,并通过写入各自节点的方式投票是“提交(commit)”或者“放弃(abort)”事务。一旦写入完成,别的站点会收到通知,并且只要所有站点都投票完成,就可以决定是放弃还是提交。注意,如果节点投票放弃时可以决定提前投票。

该实现方案中有意思的一方面就是,协调器的角色就是决定创建节点,传播事务给相应站点。事实上,传播事务也可以通过在事务节点写入数据的方式使用ZooKeeper完成。

上面描述中,有两点重要的不利点,一是消息复杂度-O(n²),另一个临时节点方式不能捕捉到站点失败。为了捕获临时节点异常,站点必须要创建节点。为了解决第一个问题,只能让协调器通知事务节点,并在协调器收到决定后通知所有站点。注意这种方式是伸缩性的,但是更慢,因为所有的通讯都需要通过协调器。为了解决第二个问题,可以让协调器传播事务给所有站点,并且,每个站点创建自己的临时节点。

领导者选举-Leader Election

使用ZooKeeper解决领导者选举的一种简单方式就是:给每个候选者客户端创建代表它们的节点,并且使用标记 SEQUENCE|EPHEMERAL,表名节点是有序号的,而且是临时节点。思路就是有个节点-“/election”,每个节点创建一个有序临时子节点-“/election/guid-n_”,使用有序标志,ZK自动给每个子节点添加一个序号,那么序列号最小的客户端就是领导者。

这并不是全部内容,监视领导者宕机很重要,这样,当前领导者宕机时,可以选择一个新的领导者。一个可行方案就是让所有应用监视当前最小序号节点,并在当前领导者宕机时查看自己是否是序号最小的节点(注意领导者宕机时最小序号节点会消逝,因为节点是临时节点)。但是这会导致羊群效应:当前领导者的失败,所有客户端都会收到通知,获取"/election"下子节点。如果客户端数量很大,ZK不得不处理一些失效的操作。为了避免羊群效应,也要监听下一个序号较小的节点。如果一个客户端收到它监控节点消逝的通知,那么它就会变成这个新的领导者,因为没有更小序号的节点了。注意:由于这没有让所有客户端监视这个节点,这就避免了羊群效应。

伪代码如下:

应用根节点设置为“ELECTION”,自愿成为领导者:

  1. Create znode z with path "ELECTION/guid-n_" with both SEQUENCE and EPHEMERAL flags;

  2. Let C be the children of "ELECTION", and i be the sequence number of z;

  3. Watch for changes on "ELECTION/guid-n_j", where j is the largest sequence number such that j < i and n_j is a znode in C;

Upon receiving a notification of znode deletion:

  1. Let C be the new set of children of ELECTION;

  2. If z is the smallest node in C, then execute leader procedure;

  3. Otherwise, watch for changes on "ELECTION/guid-n_j", where j is the largest sequence number such that j < i and n_j is a znode in C;

注意:

  • Note that the znode having no preceding znode on the list of children does not imply that the creator of this znode is aware that it is the current leader. Applications may consider creating a separate znode to acknowledge that the leader has executed the leader procedure.

  • See the note for Locks on how to use the guid in the node.

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值