ZooKeeper常见问题和解决方案
构建Zookeeper高级设计指南
在本文中,你可以发现使用Zookeeper实现高级功能的解决方案. 这些都只需要在客户端支持实现,不需要Zookeeper做额外的支持. 希望社区开发者遵循这些客户端约定来提高易用性和标准化.
Zookeeper有趣事情之一是它使用异步消息通知,你可以使用这个功能区构建同步一致性元件,例如队列和锁. 正如你说看到的那样,它能够做到这一点,因为Zookeeper会使Updates操作保持整体的顺序性,并有机制来公开这个顺序.
注意下面的内容,这是尝试的最佳实战. 通常,我们要避免轮训和定时查询或者其他任何可能引起羊群效应的操作,这会导致流量爆发和限制可伸缩性.
有许多有用的功能不包含在这里,例如— 可撤销的读写优先锁,仅举这一个例子. 这里提到一些结构,在这里 - 锁,尤其是 -这里只是说明一些问题, 当日你也可能发现一些其他的接口,例如:事件处理或者队列, 执行相同操作但更优秀的设计. 总之,这个例子主要是为了给你开脑洞的.
开箱即用的应用:命名服务、配置服务、组成成员(Out of the Box Applications: Name Service, Configuration, Group Membership)
命名服务和配置是Zookeeper的两个重要应用. 这两个功能由Zookeeper API提供直接的支持.
同时Zookeeper也直接支持另一个功能group membership. 这个组由一个节点来表示. 组成员在组节点下创建临时节点. 当Zookeeper发生错误时,成员节点会自动删除.
阻塞
分布式系统使用阻塞去阻塞一系列节点的进程,当一个条件被触发,所有节点会同时进行数据处理. 阻塞的实现是基于Zookeeper中设置一个阻塞节点. 如果这个阻塞节点已经存在,说明可以实现阻塞. 这是伪代码:
-
客户单调用Zookeeper API的exists()方法来验证阻塞节点是否存在,并设置监听者为true.
-
如果exists() 返回false, 则阻塞消失,客户端继续.
-
否则,如果exists() 返回true, 客户端会等待关于该阻塞节点的Zookeeper监听者事件.
-
当客户单事件触发,客户端会重新调用 exists( ), 如果阻塞节点仍然存在会一直阻塞值该节点删除.
双重阻塞
双重阻塞可以同步计算的开始和结束. 当足够的进程进入到阻塞中, 进程开始他们的计算,当完成计算后会调用同时离开阻塞. 这个例子显示了如何使用Zookeeper节点做阻塞.
这段伪代码中,阻塞节点表示为b. 每个客户端p当进入阻塞时注册,离开时会注销该阻塞节点. 注册阻塞节点需要通过下面Enter , 它会在开始运算之前一直等待x个客户端进程注册. (这里的x由你的系统来决定)
Enter | Leave |
|
|
进入时, 所有进程都会设置ready节点的观察者,创建一个临时节点作为阻塞节点的孩子节点. 除了最后一个节点,其余的进程会进入阻塞等待ready节点(在第五行). 进程创建低x个节点,最后的进程会检查孩子节点(这时会大于x),并创建ready节点, 唤醒其他进程. 注意等待节点唤醒,仅当它被唤醒的那刻,所以线程等待是高效的.
在退出时,你不能使用使用flag例如ready,因为你正在检测进程节点离开. 使用临时节点,在阻塞过后失败的节点已经进入不阻塞的正确进程的完成. 当进程进程准备好离开,他们需要删除他们的进程节点,等待其他进程做同样的事情.
当b的进程节点不存在时,进程退出. 然而,为了高效,你可以使用最小进程节点最为ready flag. 所有其他准备离开的节点检查最低节点的消失,并且最低节点的拥有者检测其他进程节点(为了简单而选择最高). 这就意味着,除了最后一个节点,每个节点删除仅仅有一个单独的进程被唤醒. 当最后一个删除时唤醒每一个节点.
队列
分布式队列是一个常用的数据结构. 在Zookeeper中实现一个分布式队列,首先定义一个节点去表示队列,也就是队列节点. 分布式客户端通过调用create()以"queue-"+序列的形式放入队列中,并且在调用create时,设置序列和临时标记为true.因为序列标记为ture, 新的路径是这样_path-to-queue-node_/queue-X, X是单调递增序列. 一个客户端想要删除队列,需要调用Zookeeper的getChildren( ) 方法, 队列节点的监听者设置为true, 开始处理最小序列的节点. 这个节点不需要调用另外一个 getChildren( ) 直到它耗尽来自第一个getChildren( ) 的整个集合. 如果这个队列节点没有子节点,处理进程会等待一个监听者消息后再次会再次检查队列.
现在在Zookeeper目录中存在队列的实现. 这是发行版发布的 -- 在src/recipes/queue目录.
优先队列
实现一个优先队列, 你仅需要对 queue recipe做两个简单的改变. 首先,添加一个队列,"queue-YY"是路径名称的结尾,YY是指元素的优先级,数字越低表示优先级越高(仅指UNIX). 第二点,当从队列移除时,如果出发了额监听者关于队列节点的通知,客户端会放弃之前获取的孩子列表,而使用最新的孩子列表.锁
完整的分布式锁是全局同步的,意味着在同一时间两个客户端不会持有同一个锁. 这些可以使用Zookeeper实现. 就像优先队列,首先定义一个锁节点.
在Zookeeper目录中锁的实现. 在发行发布版中 -- src/recipes/lock 发布的目录.
客户端希望获得一个锁要做以下这些事情:
-
调用 create( ) 方法,参数是"_locknode_/lock-"的路径名和自增序列和临时节点标记.
-
在Lock节点上调用 getChildren( ) 而不设置监听者标记(这最终要的是避开羊群效应).
-
如果第一步创建的路径名有最小序列数后缀,客户端获得锁并离开协议.
-
客户端在锁目录的下一个最小序列值得路径上使用 exists( ) 设置监听者标记.
-
如果 exists( ) 返回false, 就进入第二部 2. 否则, 在进入第二部之前等待该路径的通知.
解锁协议十分简单: 客户端希望释放锁只需要简单的删除第一步创建的节点.
这里有些事情需要注意:
-
删除这个节点只会导致你唤醒精确的每个客户端的监听者. 通过这种方式,你避免了羊群效应.
-
没有轮序和超时.
-
因为你实现锁的方式,非常容易发生锁竞争的成员、打破锁、调试锁问题等.
共享锁
你在锁定协议上做一点改变就可以实现共享锁:
获得一个读取锁: | 获得一个写锁: |
|
|
这个操作可能造成羊群效应:当大量客户端在等待读锁时,当最小序列的"write-"节点被删除客户端都会或多或少的获得消息. 实际上,这个是有效行为: 所有等待读的客户端都应该释放,因为他们存在锁. 羊群效应实际上是指发布一个"herd",只有一个或少数客户端可以进行下去.
可恢复的共享锁
对共享锁进行小的修改,通过修改你可以使你的共享锁可以恢复:
在获取读和写锁协议的第一步,调用 getData( ) 来设置监听,之后立刻调用create( ). 如果客户端随后接收到第一步节点创建的消息通知,会调用在该节点另一个getData( ), 设置监听者并检查字符串"unlock", 发送给客户端信息,客户端必须释放锁. 这是因为,根据这个共享锁协议,可以在这个及节点通过客户端调用setData()要求其放弃锁, 只需要往节点中写入"unlock".
注意这个协议需要锁的持有者同意释放锁. 这个同意是十分重要的,特别是这个锁的持有者需要在释放锁之前做一些操作. 当然你可以一直实现可恢复的共享锁通过在协议里规定允许撤销者删除lock节点如果lock持有者在一定的时间之后还没有删除lock。
两阶段提交
两阶段提交协议是一个在分布式系统中让所有客户端系统可以提交和回滚事务的算法.
在Zookeeper中,你可以实现两阶段提交,通过一个协调者创建一个事务节点,例如 “/app/Tx”,每个参与者叫做"/app/Tx/s_i". 当协调者创建孩子节点时,它让内容定义. 一旦与事务有关的每个点从协调者哪里接收到事务,参与者读取每个孩子节点并设置监听者. 然后每个参与者处理查询和投票选择是"commit"或者"abort"然后写入各自对应的节点. 一旦写入完成,其他的节点接收到通知,并且不久后所有参与者都会拥有所有投票,他们可以决定是执行"aboort"或者"coomit". 注意如果有些节点投票了“abort”,那么一个节点可以提早决定"abort".
有趣的是,这个实现中,协调者的角色仅仅是决定参与者,然后创建Zookeeper节点,并将事务传播到相应的参与者. 事实上,可以可以通过吸入事务节点来传播事务.
上面的描述有两个重要的缺陷. 一个是消息的复杂性是O(n²). 第二个是通过临时节点检测参与者的错误是不可能的. 使用临时节点的参与者发现错误是必要,它需要能够创建这个节点.
解决第一个问题,你可以仅接受改变的事务节点的通知,然后通知参与者一旦参与者下决定. 注意这个方法是可扩展的,但是它很慢, 因为它要求所有通知通告协调者.
解决第二个问题, 你可以让协调者传播事务到参与者,并且每个参与者创建自己的临时节点.
领导者选举
使用Zookeeper做领导人选举的简单方法是创建代表客户端的"proposals"节点时设置SEQUENCE|EPHEMERAL 标记. 方法是有一个叫做"/election"的节点,然后每个每个节点创建一个叫"/election/n"的子节点,并加上标签SEQUENCE|EPHEMERAL. 关于sequence标记, ZooKeeper自动在"/election"的孩子节点后追加一个更大的数字. 创建的节点中最小序列的作为领导者.
不过,这不是全部. 重要的是检测领导者异常,然后当领导者异常时选举一个新的领导者. 一个简单的方案是让所有应用进程监控最小的节点,当最小节点离线时检测它们是否是最小的节点.(注意如果领导者发生异常最小节点将会消失,因为这个节点是临时节点). 但是可能会导致羊群效应:当领导者异常后,所有进程会收到一个通知,在"/election"节点调用getChildren获得"/election"节点当前的孩子节点列表,它会导致Zookeeper必须处理的操作数量激增. 为了避开羊群效应, 在znodes序列中监视下一个节点就足够了. 如果客户端接收到一个通知,它正在监听的节点已经消失,接着在此刻没有更小的节点时,它会称为新的领导者.注意这通过所有节点检测同一个znode节点而避免了羊群效应.
这里是它的伪代码:
让ELECTION称为应用车需的的选择路径. 自愿者成为领导者:
-
创建一个为z的节点,路径为"ELECTION/n_",设置SEQUENCE and EPHEMERAL 标记;
-
让C作为"ELECTION"的孩子, i作为z节点的序列号.
-
检测"ELECTION/n_j"的改变, 在这里j是最大的序列号,这里j小于i,并且n_j是C中的一个节点.
在接收到znode节点删除通知后:
-
让C成为ElECION的新的孩子.
-
如果Z是C红最小的节点,然后执行领导者过程;
-
否则,检测"ELECTION/n_j"的改变, 这里的j是最大序列号,这里j小于i并且n_j是C里的节点.
注意子节点列表里没有之前的节点并不意味着节点的创建者知道他是当前的领导者。应用可能考虑创建一个单独的节点去通知领导者已经执行了领导者选举过程。
请确认选中的文本是完整的单词或句子。
目前仅谷歌翻译支持汉译英。