ZooKeeper 食谱和解决方案

使用 ZooKeeper 创建更高级构造的指南

在本文中,您将找到使用 ZooKeeper 实施高阶函数的准则。所有这些都是在 Client 端实施的约定,不需要 ZooKeeper 的特殊支持。希望社区能够在 Client 端库中捕获这些约定,以简化它们的使用并鼓励标准化。

关于 ZooKeeper 的最有趣的事情之一是,即使 ZooKeeper 使用* asynchronous 通知,您也可以使用它来构建 synchronous *一致性 Primitives,例如队列和锁。正如您将看到的,这是可能的,因为 ZooKeeper 对更新强加了整体 Sequences,并具有公开此 Sequences 的机制。

请注意,以下食谱尝试采用最佳做法。特别是,它们避免了轮询,计时器或任何其他会导致“群效应”的事情,从而导致流量爆发并限制了可伸缩性。

可以想象这里没有包含许多有用的功能-可撤销的读写优先级锁,仅举一个例子。而且这里提到的某些结构(尤其是锁)说明了某些要点,即使您可能会发现其他结构(例如事件句柄或队列),也是执行同一功能的更实用方法。通常,本节中的示例旨在激发思想。

有关错误处理的重要说明

实施配方时,必须处理可恢复的异常(请参阅FAQ)。特别地,一些配方采用 Sequences 的临时节点。创建 Sequences 临时节点时,会出现错误情况,其中在服务器上 create()成功,但服务器崩溃后才将节点名称返回给 Client 端。当 Client 端重新连接时,其会话仍然有效,因此不会删除该节点。这意味着 Client 端很难知道其节点是否已创建。以下食谱包括解决此问题的措施。

开箱即用的应用程序:名称服务,配置,组成员身份

名称服务和配置是 ZooKeeper 的两个主要应用程序。这两个功能由 ZooKeeper API 直接提供。

ZooKeeper 直接提供的另一个功能是* group member *。该组由一个节点表示。组成员在组节点下创建临时节点。当 ZooKeeper 检测到故障时,将自动删除异常失败的成员节点。

Barriers

分布式系统使用* barriers *来阻止对一组节点的处理,直到满足条件时才允许所有节点 continue 进行。在 ZooKeeper 中通过指定屏障节点来实现屏障。如果障碍节点存在,则障碍就位。这是伪代码:

  • Client 端在屏障节点上调用 ZooKeeper API 的 exists() 函数,并将* watch *设置为 true。

  • 如果 exists() 返回 false,则障碍消失,Clientcontinue 前进

  • 否则,如果 exists() 返回 true,则 Client 端将 awaitZooKeeper 的 watch 事件以了解屏障节点。

  • 触发监视事件后,Client 端将重新发出 exists() 调用,再次 await 直到屏障节点被移除。

Double Barriers

双屏障使 Client 端可以同步计算的开始和结束。当足够多的进程加入障碍后,进程将开始计算并在完成后离开障碍。此食谱展示了如何使用 ZooKeeper 节点作为障碍。

此配方中的伪代码将障碍节点表示为* b 。每个 Client 端进程 p 在进入时向障碍节点注册,并在准备离开时注销。节点通过下面的“ Enter”过程向障碍节点注册,它 await 直到 x 个 Client 端进程注册后,才 continue 进行计算。 (这里的 x *由您决定是否适合您的系统.)

EnterLeave
1.创建一个名称 *n * = * b “ /” * p1. L = getChildren(b,假)
2.设置手表: 存在( b ``/ ready'',true)2.如果没有孩子,请退出
3.创建子项: create( n ,EPHEMERAL)3.如果* p *仅是 L 中的过程节点,则 delete(n)并退出
4. L = getChildren(b,假)4.如果* p *是 L 中最低的过程节点,请 awaitL 中最高的过程节点
5.如果 L 中的子项少于_x_,请 await 监视事件5.否则 delete( n ) 如果仍然存在并 awaitL 中的最低进程节点
6. else create(b''/ ready'',REGULAR)6.转到 1

进入时,所有进程都会监视就绪节点,并创建一个临时节点作为屏障节点的子节点。除最后一个进程外,每个进程都进入障碍并 await 就绪节点出现在第 5 行。创建第 x 个节点的进程(最后一个进程)将在子级列表中看到 x 个节点并创建就绪节点,从而唤醒该进程。其他过程。请注意,await 进程仅在需要退出时才唤醒,因此 await 非常有效。

退出时,您不能使用* ready *之类的标志,因为您正在监视流程节点是否消失。通过使用临时节点,进入屏障后失败的过程不会阻止正确的过程完成。当流程准备好离开时,它们需要删除其流程节点并 await 所有其他流程执行相同的操作。

当没有进程节点作为* b *的子节点时,进程退出。但是,为了提高效率,可以将最低的进程节点用作就绪标志。准备好退出的所有其他进程都在监视最低的现有进程节点,而最低进程的所有者则在监视其他任何进程节点(为简单起见,选择最高的进程)。这意味着在删除每个节点时,只有一个进程会被唤醒,最后一个节点除外,最后一个节点会在删除后唤醒所有人。

Queues

分布式队列是一种常见的数据结构。要在 ZooKeeper 中实现分布式队列,请首先指定一个 znode 来保存该队列,即队列节点。分布式 Client 端通过调用路径名以“ queue-”结尾的 create()将某些东西放入队列中,并且 create()调用中的* sequence ephemeral 标志设置为 true。因为设置了 sequence 标志,所以新路径名的格式为 path-to-queue-node /queue-X,其中 X 是单调递增的数字。希望从队列中删除的 Client 端调用 ZooKeeper 的 getChildren() 函数,在队列节点上将 watch *设置为 true,然后开始处理编号最小的节点。在 Client 端用尽从第一个 getChildren() 调用获得的列表之前,Client 端不需要发出另一个 getChildren() 。如果队列节点中没有子节点,则读取器 await 监视通知以再次检查队列。

Note

Note

现在,ZooKeeper 配方目录中存在一个 Queue 实现。这与发布工件一起发布-Zookeeper-recipes/zookeeper-recipes-queue 目录。

Priority Queues

要实现优先级队列,只需对通用queue recipe进行两个简单更改。首先,要添加到队列中,路径名以“ queue-YY”结尾,其中 YY 是元素的优先级,数字越小表示优先级越高(就像 UNIX)。其次,从队列中删除时,Client 端使用最新的子列表,这意味着如果监视通知触发了队列节点,则 Client 端将使先前获取的子列表无效。

Locks

全局同步的完全分布式锁,这意味着在任何时间的快照中,没有两个 Client 端会认为它们拥有相同的锁。这些可以使用 ZooKeeeper 实现。与优先级队列一样,首先定义一个锁定节点。

Note

Note

现在,ZooKeeper 配方目录中存在一个 Lock 实现。它随发布工件一起发布-Zookeeper-recipes/zookeeper-recipes-lock 目录。

希望获得锁的 Client 请执行以下操作:

  • 用路径名“ * locknode /guid-lock-”调用 create() 并设置 sequence ephemeral 标志。如果缺少 create()结果,则需要 guid *。请参阅下面的 Comments。

  • 在不设置观察标志的情况下,在锁定节点上调用 getChildren() (这对于避免羊群效应很重要)。

  • 如果在步骤 1 中创建的路径名具有最低的序列号后缀,则 Client 端具有锁定,并且 Client 端退出协议。

  • Client 端使用在下一个最低序号的锁定目录中的路径上设置的监视标志调用 exists()

  • 如果 exists() 返回 null,请转到步骤 2 。否则,请 await 上一步的路径名通知,然后转到步骤 2

解锁协议非常简单:希望释放锁的 Client 端只需删除他们在步骤 1 中创建的节点即可。

这里有一些注意事项:

  • 删除一个节点只会导致一个 Client 端唤醒,因为每个节点正好由一个 Client 端监视。这样,您可以避免羊群效应。

  • 没有轮询或超时。

  • 由于实现了锁定的方式,很容易看到锁定争用,中断锁定,调试锁定问题等的数量。

可恢复的错误和 GUID

  • 如果在调用 create() 时发生可恢复的错误,则 Client 端应调用 getChildren() 并检查包含路径名中使用的* guid *的节点。这处理了在服务器上成功执行 create()的情况(记为above),但是服务器在返回新节点的名称之前崩溃了。

Shared Locks

您可以通过对锁协议进行一些更改来实现共享锁:

获得读锁:获得写锁:
1.调用 create() 以创建路径名为“ * guid-/read- ”的节点。这是协议后面部分中使用的锁定节点。确保同时设置 sequence ephemeral *标志。1.调用 create() 以创建路径名为“ * guid-/write- ”的节点。这是协议后面提到的锁定节点。确保同时设置 sequence ephemeral *标志。
2.在不设置* watch *标志的情况下,在锁定节点上调用 getChildren() -这很重要,因为它避免了羊群效应。2.在不设置* watch *标志的情况下,在锁定节点上调用 getChildren() -这很重要,因为它避免了羊群效应。
3.如果没有子代的路径名以“ * write- *”开头,并且序号比在步骤 1 中创建的节点低,则 Client 端具有锁定并且可以退出协议。3.如果没有子序号比在步骤 1 中创建的节点低的子代,则 Client 端拥有锁,并且 Client 端退出协议。
4.否则,使用* watch *标志调用 exists() ,该标志在锁定目录中的节点上设置,路径名称以“ * write- *”开头,具有最低的序列号。4.在路径名次低的节点上调用设置了* watch *标志的 exists(),
5.如果 exists() 返回* false *,则转到步骤 25.如果 exists() 返回* false *,则转到步骤 2 。否则,请 await 上一步的路径名通知,然后转到步骤 2
6.否则,请 await 上一步的路径名通知,然后再 continue 执行 2 步骤

Notes:

  • 看来,此配方会产生成群的效果:当有一大批 Client 机在 await 读锁时,当删除序号最低的“ * write- *”节点时,所有 Client 机或多或少会同时收到通知。事实上。这是正确的行为:由于所有 await 的读取器 Client 端都具有锁定,因此应释放它们。放牧效果是指实际上只有一台或少量机器可以运行时释放“放牧”。

  • 有关如何在节点中使用 GUID 的信息,请参见锁注意

可撤销的共享锁

通过对共享锁协议进行较小的修改,可以通过修改共享锁协议使共享锁可撤销:

在同时获得读取器和写入器锁定协议的步骤 1 中,在调用 create() 之后立即调用设置了* watch getData() 。如果 Client 端随后收到其在步骤 1 中创建的节点的通知,则它将在该节点上执行另一个设置了 watch *的 getData() ,并寻找字符串“ unlock”,该 signal 表示 Client 端必须释放锁。这是因为,根据此共享锁定协议,您可以通过在锁定节点上调用 setData() ,然后向该节点写入“解锁”,来请求具有该锁定的 Client 端放弃该锁定。

请注意,此协议要求锁持有人同意释放锁。这种同意很重要,尤其是在锁持有人需要在释放锁之前进行一些处理的情况下。当然,您始终可以通过在协议中规定,如果经过一定时间后锁持有人未删除该锁,则允许 revoker 删除锁节点,从而始终可以实现“带有奇异激光束的可撤销共享锁”。

Two-phased Commit

两阶段提交协议是一种算法,可让分布式系统中的所有 Client 端都同意提交事务或中止协议。

在 ZooKeeper 中,可以通过让协调器创建一个事务节点(例如“/app/Tx”)和每个参与站点的一个子节点(例如“/app/Tx/s_i”)来实现两阶段提交。当协调器创建子节点时,其内容未定义。一旦 Transaction 中涉及的每个站点都从协调员那里收到 Transaction,站点就会读取每个子节点并设置监视。然后,每个站点都会处理查询,并通过写入其各自的节点来投票“提交”或“中止”。一旦写入完成,便会通知其他站点,一旦所有站点都获得了所有投票,他们就可以决定“中止”还是“提交”。注意,如果某些站点投票赞成“中止”,则节点可以更早地决定“中止”。

此实现的一个有趣的方面是,协调器的唯一作用是决定站点组,创建 ZooKeeper 节点并将事务传播到相应的站点。实际上,甚至可以通过将 Zookeeper 写入事务节点中来通过 ZooKeeper 进行事务传输。

上述方法有两个重要的缺点。一种是消息复杂度,即 O(n²)。第二个是不可能通过临时节点检测站点的故障。要使用临时节点检测站点的故障,必须由站点创建该节点。

要解决第一个问题,您可以仅将协调器的事务节点更改通知协调器,然后在协调器做出决定后通知站点。请注意,这种方法是可扩展的,但它也较慢,因为它要求所有通信都通过协调器。

为了解决第二个问题,您可以让协调器将事务传播到站点,并让每个站点创建自己的临时节点。

Leader Election

使用 ZooKeeper 进行领导者选举的一种简单方法是,在创建表示 Client 端“建议”的 znode 时使用 SEQUENCE | EPHEMERAL 标志。这个想法是要有一个 znode,例如“/election”,以便每个 znode 创建一个带有两个标志 SEQUENCE | EPHEMERAL 的子 znode“/election/guid-n_”。有了序列标志,ZooKeeper 会自动添加一个序列号,该序列号大于以前添加到“/election”子代的任何序列号。创建带有最小附加序列号的 znode 的进程为 leader。

但这还不是全部。重要的是要注意领导者的失败,以便在当前领导者失败的情况下,新 Client 就可以成为新的领导者。一个简单的解决方案是让所有应用程序进程监视当前最小的 znode,并在最小的 znode 消失时检查它们是否是新的领导者(请注意,如果领导者失败,则最小的 znode 将消失,因为领导者是短暂的)。但是,这会导致成群的影响:在当前领导者失败后,所有其他进程都会收到通知,并在“/election”上执行 getChildren 以获得“/election”的当前子代列表。如果 Client 端数量很大,则会导致 ZooKeeper 服务器必须处理的操作数量激增。为了避免成群效应,足够注意 znode 序列上的下一个 znode。如果 Client 端收到其正在监视的 znode 消失的通知,则在没有较小的 znode 的情况下它将成为新的领导者。请注意,通过避免让所有 Client 端都观看同一 znode,这避免了羊群效应。

这是伪代码:

让 ELECTION 成为应用程序的选择路径。自愿成为领导者:

  • 用 SEQUENCE 和 EPHEMERAL 标志创建路径为“ ELECTION/guid-n_”的 znode z;

  • 令 C 为“选举”的子代,而 i 为 z 的序列号;

  • 注意“ ELECTION/guid-n_j”上的更改,其中 j 是最大的序列号,因此 j <i 且 n_j 是 C 中的 znode;

收到 znode 删除通知后:

  • 令 C 为选举的新子集;

  • 如果 z 是 C 中最小的节点,则执行领导程序;

  • 否则,请注意“ ELECTION/guid-n_j”上的更改,其中 j 是最大的序列号,使得 j <i 且 n_j 是 C 中的 znode;

Notes:

  • 请注意,子节点列表中没有前面的 znode 的 znode 并不表示此 znode 的创建者知道它是当前的领导者。应用程序可以考虑创建一个单独的 znode 来确认领导者已经执行了领导者过程。

  • 有关如何在节点中使用 GUID 的信息,请参见锁注意