ZooKeeper食谱(六)

使用ZooKeeper构造高级别应用的指南

在这个文章中,你将会发现使用ZooKeeper来实现高级别功能的指南。全部的它们在客户端上被实现而不须要ZooKeeper特别的支持.但愿社区将注意到这些约定在客户端库里来方便他们的使用而且促进标准化。html

 

其中一个关于ZooKeeper最有趣的事是尽管ZooKeeper使用异步的通知,你可使用它构造同步的一致性原语,例如队列和锁。正如你将要看到的同样,这是可能的由于ZooKeeper对更新强加了一个总体的顺序,和暴躁这个顺序的机制。前端

注意下面的食谱试图实用最挂实践。特别地,他们避免投票,定时器或者任何其它致使产生“羊群效应”的形成突发的事故和限制扩展性。node

有不少能够被想到的有用的功能没有包含在这 - 可撤销的读-写优先锁,只有一个例子。而且这里提到的一些构件 - 特别是锁 - 阐明了特定的观点,尽管你可能找到其它构件,例如事件处理或队列,执行相同功能的一个更实用的方法。一般,这部分的例子被设计用来刺激思想。apache

关于错误处理的重要注意事项

当实现这些食谱你必须处理可恢复的异常(参考 FAQ)。特别地,一些食谱利用了顺序的短暂节点。当建立一个顺序的短暂节点时,有一个错误的案例,当create()在服务端成功了可是在返回给客户端这个节点的名字以前服务端挂掉了。当客户端从新链接它的会话仍然是有效的而且,所以,这个节点没有被删除。这个意思是对客户端来讲它很难知道这个节点有没有被建立。下面的食谱包含了处理这种状况的方法。数据结构

取出便可用的应用:命名服务,配置,集群管理

命名服务和配置管理是ZooKeeper的两个主要应用。这两上功能被ZooKeeper API直接提供。app

另外一个被ZooKeeper直接提供的是集群管理。组被表示为了一个节点。组中的成员在组节点下建立短暂的节点。不正常地失败的组中的节点将被自动地删除当ZooKeeper检测到失败。异步

屏障

分布式的系统使用屏障来一组节点的进程,直到遇到知足的条件的时候全部节点才被容许继续执行。在ZooKeeper中屏障经过指定一个屏障节点来实现。障碍在路上若是屏障节点存在。下面的它的伪代码:分布式

  1. 客户端在屏障节点上调用ZooKeeper API的exists()方法,而且设置 watch 为true.
  2. 若是exists()返回false,屏障消失而且客户端继续。
  3. 不然,若是exists()返回true,客户端等待屏障节点的事件。
  4. 当监听事件被触发,客户端再次调用exists()方法,再次等待直到屏障节点被删除。

两重屏障函数

两重屏障使客户端同步一个计算的开始和结束。当足够的进程进入到屏障,进程开始他们的计算,而且一旦完成就离开屏障。这个食谱展现了怎么做为屏障使用ZooKeeper节点。ui

这个食谱中的伪代码用b来表示一个屏障节点。每个客户端进程 p 在进入的时候注册到屏障节点上而且在离开的时候取消注册。一个节点经过下面的Enter过程来注册到屏障节点上,在继续计算以前它等待直到 x 客户端进程完成注册(这里的x取决于你的系统)。

Enter Leave

1. Create a name n = b+“/”+p

2. Set watch: exists(b + ‘‘/ready’’, true)

3. Create child: create( n, EPHEMERAL)

4. L = getChildren(b, false)

5. if fewer children in L than x, wait for watch event

6. else create(b + ‘‘/ready’’, REGULAR)

1. L = getChildren(b, false)

2. if no children, exit

3. if p is only process node in L, delete(n) and exit

4. if p is the lowest process node in L, wait on highest process node in L

5. else delete(n) if still exists and wait on lowest process node in L

6. goto 1

当进入的时候,全部进程监视一个准备好的节点,而且建立一个短暂节点做为屏障节点的子节点,每个进程可是最后进入的屏障,而且等待准备的节点出如今第5行。建立第x个节点的进程,也就是最后一个节点。将看到x个节点在孩子列表中。而且建立准备结点。而后唤醒其它进程。注意等待进程只有在退出的时候才唤醒。因此等待是高效的。

在退出的时候。你不能使用一个标示,例如ready,由于你正在等待处理的节点离开。经过使用短暂节点。在进入屏障以后失败的进程不能阻止正确的进程结束。当进程准备离开的时候。它们必须删除进程节点而且等待其它进程作一样的事情。

进程退出当没有进程节点做为b的孩子节点的时候。然而,为了效率,你可使用最底的进程节点做为准备标示。准备退出的全部其它进程监视最底的退出节点离开。而且最底节点的拥有者监视任一其它节点(为了简单选择最高的)的离开。这意为只一个节点唤醒当一个节点删除的时候除了最后一个节点删除。最后节点删除的时候全部节点被唤醒。

队列

分布式队列是一个广泛的数据结构。为了在ZooKeeper中实现一个分布式队列。首先指定一个znode持有队列。队列节点。分布式的客户端放一些东西进入队列经过用以queue-结尾的路径,序号和为短暂的标示true调用create()。由于序号的标示被设置。新的路径名字的形式为_path-to-queue-node_/queue-X,这里的X是自动增长的数字。一个想要从队列中被删除的节点调用ZooKeeper 的getChildren( ) 函数。在队列中的节点都设置了监视器。而且开始处理最小数值的节点。客户端不须要发起另外一个getChildren( )直到消耗到第一次调用getChildren( )返回的列表。若是没有孩子在队列节点中。读进程等待一个监视通知来再次检查队列。

注意

如今有一个队列实如今ZooKeeper食谱目录下。它在发行版本的src/recipes/queue 目录下。

优先队列

为了实现优先队列,你只须要在通用的队列上作两个小的修改。首先,为了增长一个队列,路径名字以"queue-YY"结尾,这里的YY是元素的优先级,并且最小的数有最高的优先级(就像UNIX)。第二,当从队列中删除的时候。客户端使用最新的孩子列表意为着客户端将使先前获取的孩子列表无效若是触发了这个队列节点的监听通知。

彻底分布式锁是全局同步的。意为着在任什么时候间的快照中没有两个客户认为它们持有相同的锁。这些能够实现使用ZooKeeper。就像优先队列,先定义一个锁节点。

注意如今有一个锁实如今ZooKeeper食谱目录下,它在发行版本的src/recipes/queue 目录下。

客户端想获取锁须要作下面的事情:

  1. 以参数"_locknode_/guid-lock-"同时设置sequence和ephemeral标识来调用create()。须要一个guid以防create()丢失。参考下面的注意事项。
  2. 在锁节点上调用getChildren()而不设置监视标识(这对避免羊群效应很重要)
  3. 若是在步骤1中建立的路径名有最小的顺序数字后缀,客户端拥有这个锁而且客户端退出协议。
  4. 客户端调用exists()并在锁目录中的下一个最小序列数字的路径设置监视标识。
  5. 若是exists()返回false,跳到第2步。不然等待从前一个步骤中获得路径名的通知在进入步骤2以前。

解锁协议是很是简单的:将要释放锁的客户端简单地删除他们在步骤1中建立的节点。

这里须要几件事须要注意:

  • 一个节点的删除将只引发一个客户端唤醒由于每个节点偏偏被一个客户端监视着。用这种方式你避免了羊群效应。
  • 这里没有投票或超时。
  • 由于你实现锁的方式,很容易看到锁竞争的数量,打断锁,调试锁问题,等等。

可从新覆盖的错误和GUID

  • 若是在调用create()的时候出现可从新覆盖的错误,客户端应该调用getChildren()而且检查包含用来在路径名的guid的节点。这处理在服务端create()成功可是在返回新节点的名字以前服务端崩溃的状况。

共享锁

你能够实现共享锁经过对锁协议作一些小的改变:

获取读锁 获取写锁

1. 调用create()来建立一个路径名 "guid-/read-"的为节点。这个在后来的协议中使用锁节点。确保设置了sequence 和 ephemeral标记。

2. 在节锁节点上调用getChildren(),而不设置监视标识 - 它很重要,它避免羊群效应。

3. 若是没有一个路径名以"write-"开头的孩子而且有比步骤1建立的节点小的序列号,那么客户端就拥有了锁而且可能退出协议。

4. 不然,调用监视标识的exists(),在有最小的序列号而且路径名是以"write-"开头的节点上设置。

5. 若是exists()返回 false,跳到步骤2。

6. 不然,在跳到步骤2以前等待来自上一个步骤中的路径名的通知。

1. 调用create()来建立一个路径名为"guid-/write-"的节点。这是锁节点。确保设置了sequence 和 ephemeral标记。

2. 在锁节点上调用getChildren( )方法而不设置监视标记 - 它很重要,它避免羊群效应。

3. 若是没有孩子节点的序列号比步骤1中建立的节点低,那么客户端就获取锁而且客户端退出这个协议。

4. 调用exists(),在下一个最小的序列号的节点上设置监视标识。

5. 若是exists() 返回false,跳到步骤2。不然,在跳到步骤2以前等待来自上一个步骤中的路径名的通知。

注意:

  • 这个食谱它可能出现羊群效应:当不少组的客户端正在等待读锁,在最小序列号的"write-"节点被删除的时候,这些客户端几乎同时获取到了通知。实际上,这是合法的行为:由于全部这些等待读的客户端应该被释放由于它们拥有锁。羊群效应是指释放一个"羊群"当实际上只有一个或一小部分机器可能处理的时候。
  • 参考note for Locks关于怎么使用guid

可撤消的共享锁

对共享锁作一些小的修改,经过修改共享锁你可使用锁可撤销。

在步骤1中,同时获取了读和写锁,在调用create()后马上调用getData()并设置监视器,若是客户端随后收到在步骤1中建立的节点的通知,它作另外一个getData()在这个节点上,同时设置监视器而且搜寻字符串“unlock”,这表示客户端必须释放锁。这是由于根据共享锁的协议,你能够要求拥有锁的客户端放弃锁经过在被锁的节点上调用setData(),并写“unlock”到这个节点上。

注意这个协议要求锁持有者赞成释放锁。这样的赞成是很重要的,特别是若是锁持有者须要在释放锁以前作一些处理。固然你能够一直实现带着该死的激光束可撤消的锁经过在你的协议中规定撤消者被容许删除锁节点若是在一段时间事后锁没有被锁持有者删除。

两段式提交

一个两段式提交协议是一种逻辑,它让全部在分布式系统中的的客户端赞成要么提交事务要么取消事务。

在ZooKeeper中,你能够实现一个两段式提交经过用一个协调者建立一个事务节点,好比"/app/Tx",每个参与的站点的孩子节点,好比,"/app/Tx/s_i"。当协调者建立孩子节点,它没有设置内容。一旦每个事务中的一方从协调者收到事务,站点读每个孩子节点,而且设置一个监视器。每个站点而后处理查询 和投票“提交”和“取消”经过写它各自的节点。一旦写完成,其它站点被通知,而且一旦全部的站点者投票,他们能够决定是提交 仍是取消。注意一个节点可早点决定取消若是一些站点投票取消。

这个实现的一个有趣的地点是只有协调的角色来根据站点组来决作决定,为了建立ZooKeeper节点,为了传播事务到相应的站点。实际上,尽管传播事务事务能够被完成融通ZooKeeper经过写它在事务节点上。

上面论讨的方法有两个缺点。一个是消息的复杂度,它是O(n²)。第二个是经过临时节点检测站点失效的不可能性。为了使用临时节点来检测站点失效,它必须站点建立这个节点。

为了解决第一个问题,只能够只能让协调者被通知事务节点的改变,而且通知节点一旦协调者收到一个决定。注意这个方法是可扩展的,可是它也更慢,由于它要求全部的通讯经过协调者。

为了解决第二个问题,你可使协调者传播事务到站点,而且使每个站点建立它的临时节点。

领导者选举

ZooKeeper作领导者选择的一个简单的方法是当建立表明揭底客户端的节点时使用SEQUENCE|EPHEMERAL标记。这个思路是有一个znode,好比 “/election",每个znode建立一个带着SEQUENCE|EPHEMERAL孩子节点"/election/guid-n_",有了顺序标记,ZooKeeper自动地附加一个比选择增长的都大的数字到一个"/election"的孩子上面。建立的最小顺序号的节点就是领导者。

这仍是所有,监听领导者的失效是很重要的,因此在当前领导者失效的状况下一个新的客户端被选举出来。一个复杂的解决办法是使全部的应用进程监听当前最小的节点,而且检查他们是否是新的领导者当最小的znode消失的时候(注意最小的znode将消失,若是领导者失败,由于这个节点是临时节点)。可是这致使一个羊群效应:一旦当前领导者失败,全部其它里程收到 一个通知,而后执行"/election" 的getChildren的方法 来获取孩子列表。若是客户端的数字是大的,它致使ZooKeeper服务端不得不处理不少次数字操做。为了不羊群效应,只监听在znodes序列的下一个节点就足够了。若是一个客户端收到它监听的节点消失了,那么它变为新的领导,一旦没有更小的节点的状况下。注意到这避免了羊群效应经过不是全部的客户端监听同一个节点。

这里是它的伪代码:

假设ELECTION是应用的选择的路径。志愿成为一个领导者:

  1. 建立znode z,它的路径是"ELECTION/guid-n_",而且设置SEQUENCE 和 EPHEMERAL 标识;
  2. 让C成为"ELECTION"的孩子,而且i是z的序号;
  3. 监听"ELECTION/guid-n_j"的改变,这里的j是最小(官网上说是最大(largest)的应该是错的)的序序号,这样j<i而且 n_j是C中的节点;

一旦收到 节点删除的通知:

  1. 假如C是ELECTION的孩子节点
  2. 若是z是C中最小的节点,那么执行选举过程;
  3. 不然,监听"ELECTION/guid-n_j"的改变,这里的j是最小(官网上说是最大(largest)的应该是错的)的序序号,这样j<i而且 n_j是C中的节点;

注意:

  • 注意一个节点前端没有节点不意为着这个节点的建立者意识到它是当前的领导者。应用能够考虑建立一个单独的znode来通知领导者已经执行了领导选举。
  • 参考 note for Locks关于怎么使用节点中的guid
相关文章
相关标签/搜索