ZooKeeper--Recipes和解决方案
使用ZooKeeper创建更高级别构造的指南
在本文中,您将找到使用ZooKeeper实现更高阶函数的指南。所有这些都是在客户端实现的约定,不需要ZooKeeper的特殊支持。社区将在客户端库中捕获这些约定,以便于使用并鼓励标准化。
ZooKeeper最有趣的一点是,即使ZooKeeper使用异步通知,您也可以使用它来构建同步一致性原语,例如队列和锁。正如您将看到的,这是可能的,因为ZooKeeper对更新施加了整体顺序,并且具有公开此排序的机制。
请注意,下面的配方尝试采用最佳实践。特别是,它们避免了轮询,计时器或其他任何会导致“群体效应”的行为,从而导致突发流量和限制可扩展性。
这里有许多可以想象的有用功能 - 可撤销的读写优先级锁,仅作为一个例子。这里提到的一些结构 - 特别是锁定 - 说明了某些点,即使你可能发现其他结构,例如事件句柄或队列,是执行相同功能的更实用的方法。一般而言,本节中的示例旨在激发思考。
关于错误处理的重要说明
实施配方时,您必须处理可恢复的异常(请参阅常见问题解答)。特别是,一些配方采用顺序短暂节点。创建顺序短暂节点时,存在一个错误情况,其中create()在服务器上成功但服务器在将节点名称返回给客户端之前崩溃。当客户端重新连接其会话时仍然有效,因此不会删除该节点。这意味着客户端很难知道其节点是否已创建。下面的食谱包括处理这个问题的措施。
开箱即用的应用程序:名称服务,配置,组成员身份
名称服务和配置是ZooKeeper的两个主要应用程序。这两个函数由ZooKeeper API直接提供。
ZooKeeper直接提供的另一个功能是组成员身份。该组由节点表示。该组的成员在组节点下创建临时节点。当ZooKeeper检测到故障时,将自动删除异常失败的成员节点。
障碍
分布式系统使用障碍 来阻止一组节点的处理,直到满足条件,此时允许所有节点继续进行。通过指定屏障节点在ZooKeeper中实现障碍。如果屏障节点存在,则屏障就位。这是伪代码:
- 客户端在屏障节点上调用ZooKeeper API的exists()函数,并将 watch设置为true。
- 如果exists()返回false,则屏障消失,客户端继续
- 否则,如果exists()返回true,则客户端等待来自ZooKeeper的监视节点的监视事件。
- 当触发监视事件时,客户端重新 发出exists()调用,再次等待,直到删除屏障节点。
双重障碍
双障碍使客户端能够同步计算的开始和结束。当足够的进程加入屏障时,进程开始计算并在完成后离开屏障。此配方显示如何使用ZooKeeper节点作为屏障。
此配方中的伪代码将屏障节点表示为 b。每个客户端进程p 在进入时向barrier节点注册,并在准备离开时取消注册。节点通过下面的Enter过程向barrier节点注册,它等待 x client客户端进程注册,然后再继续计算。(这里的x取决于您确定您的系统。)
| 输入 | 离开 |
|---|---|
| 1.创建名称n = b +“/”+ p | 1. L = getChildren(b,false) |
| 2.设置监视:存在(b +''/ ready'',true) | 2.如果没有孩子,退出 |
| 3.创建子项:create(n,EPHEMERAL) | 3.如果p只是L中的进程节点,则删除(n)并退出 |
| 4. L = getChildren(b,false) | 4.如果p是L中的最低进程节点,则等待L中的最高进程节点 |
| 5.如果L than_x_中的孩子较少,则等待观看事件 | 5.否则** delete(n)**如果仍然存在并等待L中的最低进程节点 |
| 6. else create(b +''/ ready'',REGULAR) | 6.转到1 |
进入时,所有进程都在就绪节点上进行监视,并创建一个短暂节点作为屏障节点的子节点。每个进程但最后进入屏障并等待就绪节点出现在第5行。创建第x个节点的进程(最后一个进程)将在子节点列表中看到x节点并创建就绪节点,唤醒其他过程。请注意,等待进程仅在退出时才会唤醒,因此等待是有效的。
退出时,您不能使用诸如ready之类的标志, 因为您正在监视进程节点是否消失。通过使用短暂节点,在输入屏障后失败的进程不会阻止正确的进程完成。当进程准备离开时,他们需要删除其进程节点并等待所有其他进程执行相同操作。
当没有剩余进程节点作为b的子节点时,进程退出 。但是,作为效率,您可以使用最低的进程节点作为就绪标志。准备退出的所有其他进程监视最低的现有进程节点将消失,并且最低进程的所有者监视任何其他进程节点(为简单起见选择最高)以消失。这意味着除了最后一个节点之外,每个节点上只有一个进程被唤醒,最后一个节点在被删除时唤醒每个节点。
队列
分布式队列是一种常见的数据结构。要在ZooKeeper中实现分布式队列,首先要指定一个znode来保存队列,即队列节点。分布式客户端通过调用带有以“queue-”结尾的路径名的create()将一些内容放入队列,并将create()调用中的 序列和临时标志设置为true。因为 设置了序列标志,所以新路径名将具有 路径到队列节点 /队列X的形式,其中X是单调递增的数字。想要从队列中删除的客户端使用watch调用ZooKeeper的getChildren()函数 在队列节点上设置为true,并开始处理编号最小的节点。客户端不需要发出另一个getChildren(),直到它耗尽从第一个getChildren()调用获得的列表。如果队列节点中没有子节点,则阅读器会等待观察通知再次检查队列。
注意
现在在ZooKeeper食谱目录中存在一个Queue实现。这与发布工件的release-zookeeper-recipes / zookeeper-recipes-queue目录一起分发。
优先级队列
要实现优先级队列,只需对通用队列配方进行两次简单更改。首先,要添加到队列,路径名以“queue-YY”结尾,其中YY是元素的优先级,较低的数字表示较高的优先级(就像UNIX一样)。其次,当从队列中删除时,客户端使用最新的子列表,这意味着如果针对队列节点触发监视通知,则客户端将使先前获得的子列表无效。
锁
完全分布式锁,全局同步,意味着在任何快照时,没有两个客户端认为它们拥有相同的锁。这些可以使用ZooKeeeper实现。与优先级队列一样,首先定义一个锁定节点。
注意
现在在ZooKeeper食谱目录中存在一个Lock实现。这与发布工件的release-zookeeper-recipes / zookeeper-recipes-lock目录一起分发。
希望获得锁定的客户执行以下操作:
- 使用路径名“ locknode / guid-lock-” 调用create(),并设置序列和 短暂标志。如果 错过了create()结果,则需要guid。请参阅下面的注释。
- 在锁定节点上调用getChildren()而不设置监视标志(这对于避免群体效应很重要)。
- 如果在步骤1中创建的路径名具有最低序列号后缀,则客户端具有锁,并且客户端退出协议。
- 客户端调用exists(),并在lock目录中的路径上设置watch标志,并使用下一个最低序列号。
- 如果exists()返回false,请转到步骤2。否则,在转到步骤2之前,请等待上一步中路径名的通知。
解锁协议非常简单:希望释放锁的客户端只删除他们在步骤1中创建的节点。
以下是一些需要注意的事项:
-
删除节点只会导致一个客户端被唤醒,因为每个节点都被一个客户端监视。通过这种方式,您可以避免群体效应。
-
没有民意调查或超时。
-
由于您实现锁定的方式,很容易看到锁争用,中断锁,调试锁定问题等的数量。
可恢复的错误和GUID
- 如果调用create()时发生可恢复的错误,则客户端应调用getChildren()并检查包含路径名中使用的guid的节点。这将处理服务器上成功的create()的情况(如上所述),但服务器在返回新节点的名称之前崩溃。
共享锁
您可以通过对锁协议进行一些更改来实现共享锁:
| 获取读锁定: | 获取写锁: |
|---|---|
| 1.调用create()以创建路径名为“ guid- / read- ” 的节点。这是稍后在协议中使用的锁节点。确保设置序列和短暂标志。 | 1.调用create()以创建路径名为“ guid- / write- ” 的节点。这是协议中稍后提到的锁定节点。确保设置序列和短暂标志。 |
| 2. 在锁定节点上调用getChildren()而不设置监视标志 - 这很重要,因为它避免了群体效应。 | 2. 在锁定节点上调用getChildren()而不设置监视标志 - 这很重要,因为它避免了群体效应。 |
| 3.如果没有路径名以“ write- ” 开头并且序列号低于步骤1中创建的节点的子节点,则客户端具有锁定并可以退出协议。 | 3.如果没有序列号低于步骤1中创建的节点的子节点,则客户端具有锁定,客户端退出协议。 |
| 4.否则,调用exists(),使用watch标志,在lock目录中的节点上设置路径名,其中“ write- ”具有下一个最低序列号。 | 4. 在具有下一个最低序列号的路径名的节点上调用exists(),并设置监视标志。 |
| 5.如果exists()返回false,请转到步骤2。 | 5.如果exists()返回false,请转到步骤2。否则,在转到步骤2之前,请等待上一步中路径名的通知。 |
| 6.否则,在转到步骤2之前,等待上一步中路径名的通知 |
笔记:
-
看起来这个配方会产生一个群体效应:当有一大群客户端等待读锁定时,当删除序列号最小的“ write- ”节点时,所有客户端都会或多或少地同时得到通知。事实上。这是有效的行为:所有那些等待读者客户端应该被释放,因为他们有锁。群体效应是指释放“群体”,实际上只有一个或少数机器可以继续。
-
有关如何在节点中使用guid的信息,请参阅锁定说明。
可撤销共享锁
通过对共享锁协议的微小修改,您可以通过修改共享锁协议来使共享锁可撤销:
在步骤1中,获取读取器和写入器锁定协议,在调用create()之后立即调用带有监视集的getData ()。如果客户端随后接收到它在步骤1中创建的节点的通知,则它在该节点上 执行另一个getData(),并 设置监视并查找字符串“unlock”,该字符串向客户端发出必须释放锁定的信号。这是因为,根据此共享锁协议,您可以通过在锁节点上调用setData()来向锁定放置锁的客户端请求,将“unlock”写入该节点。
请注意,此协议要求锁定持有者同意释放锁定。这种同意很重要,特别是如果锁具持有者在释放锁之前需要进行一些处理。当然,您可以通过在您的协议中规定,如果在一段时间后锁定器没有删除锁定,则允许撤销者删除锁定节点,您可以始终使用Freaking Laser Beams实现Revocable Shared Locks。
两阶段提交
两阶段提交协议是一种算法,它允许分布式系统中的所有客户端同意提交事务或中止。
在ZooKeeper中,您可以通过让协调器创建一个事务节点(例如“/ app / Tx”)和每个参与站点的一个子节点(例如“/ app / Tx / s_i”)来实现两阶段提交。当协调器创建子节点时,它会使内容未定义。一旦参与事务的每个站点从协调器接收到该事务,该站点就会读取每个子节点并设置一个监视。然后,每个站点处理查询并通过写入其相应节点来投票“提交”或“中止”。一旦写入完成,其他站点将得到通知,并且一旦所有站点都获得所有投票,他们就可以决定“中止”或“提交”。请注意,如果某个站点投票“中止”,节点可以更早地决定“中止”。
此实现的一个有趣方面是协调器的唯一作用是决定站点组,创建ZooKeeper节点,以及将事务传播到相应的站点。实际上,即使传播事务也可以通过ZooKeeper在事务节点中写入来完成。
上述方法存在两个重要缺点。一个是消息复杂性,即O(n²)。第二个是通过短暂节点检测站点故障的不可能性。要使用短暂节点检测站点的故障,站点必须创建节点。
要解决第一个问题,您只能通知协调器有关事务节点的更改,然后在协调器做出决策后通知站点。请注意,这种方法是可扩展的,但它也较慢,因为它需要所有通信都通过协调器。
要解决第二个问题,您可以让协调器将事务传播到站点,并让每个站点创建自己的临时节点。
领导人选举
使用ZooKeeper进行领导者选举的一种简单方法是在创建表示客户端“提议”的znode时使用 SEQUENCE | EPHEMERAL标志。这个想法是有一个znode,比如“/ election”,这样每个znode创建一个带有两个标志SEQUENCE | EPHEMERAL的子znode“/ election / guid-n_”。使用序列标志,ZooKeeper会自动附加一个序列号,该序列号大于先前附加到“/ election”子节点的序列号。创建具有最小附加序列号的znode的过程是领导者。
但这并不是全部。重要的是要注意领导者的失败,以便在当前领导者失败的情况下,新的领导者成为新的领导者。一个简单的解决方案是让所有应用程序进程监视当前最小的znode,并在最小的znode消失时检查它们是否是新的领导者(请注意,如果领导者因为节点是短暂的而失败,则最小的znode将消失)。但是这会导致群体效应:当前领导者失败时,所有其他进程都会收到通知,并在“/选举”中执行getChildren以获取“/选举”子项的当前列表。如果客户端数量很大,则会导致ZooKeeper服务器必须处理的操作数量激增。为了避免牧群效应,在znode序列上观察下一个znode就足够了。如果客户端收到它正在观看的znode消失的通知,那么在没有更小的znode的情况下它成为新的领导者。请注意,这可以通过不让所有客户端观看相同的znode来避免群体效应。
这是伪代码:
让ELECTION成为应用程序的选择路径。志愿成为领导者:
- 使用带有SEQUENCE和EPHEMERAL标志的路径“ELECTION / guid-n_”创建znode z;
- 设C为“选举”的子项,我是z的序号;
- 注意“ELECTION / guid-n_j”的变化,其中j是最大的序列号,使得j <i和n_j是C中的znode;
收到znode删除通知后:
- 让C成为ELECTION的新子集;
- 如果z是C中的最小节点,则执行leader过程;
- 否则,请注意“ELECTION / guid-n_j”的更改,其中j是最大的序列号,使得j <i和n_j是C中的znode;
笔记:
-
请注意,子节点列表中没有前面的znode的znode并不意味着此znode的创建者知道它是当前的领导者。应用程序可能会考虑创建一个单独的znode来确认领导者已经执行了领导程序。
-
有关如何在节点中使用guid的信息,请参阅锁定说明。

1330

被折叠的 条评论
为什么被折叠?



