深入ZooKeeper——ZooKeeper原语和架构

ZooKeeper基础

设计一个用于协作需求的服务的方法往往是:
提供原语列表,暴露出每个原语的实例化调用方法,并直接控制这些实例。

这种设计存在一些重大的缺陷:首先,我们要么预先提出一份详尽的原语列表,要么提供API的扩展,以便引入新的原语;其次,以这种方式实现原语的服务使得应用丧失了灵活性。

因此,在ZooKeeper中我们另辟蹊径。ZooKeeper并不直接暴露原语,取而代之,它暴露了由一小部分调用方法组成的类似文件系统的API,以便允许应用实现自己的原语

我们通常使用菜谱(recipes)来表示这些原语的实现。菜谱包括ZooKeeper操作和维护一个小型的数据节点,这些节点被称为znode,采用类似于文件系统的层级树状结构进行管理。
在这里插入图片描述
图描述了一个znode树的结构,根节点包含4个子节点,其中三个子节点拥有下一级节点,叶子节点存储了数据信息
针对一个znode,没有数据常常表达了重要的信息。比如,在主-从模式的例子中,主节点的znode没有数据,表示当前还没有选举出主节点。

  • /workers节点作为父节点,其下每个znode子节点保存了系统中一个可用从节点信息。如图所示,有一个从节点(foot.com:2181)。
  • /tasks节点作为父节点,其下每个znode子节点保存了所有已经创建并等待从节点执行的任务的信息。主-从模式的应用的客户端在/tasks下添加一个znode子节点,用来表示一个新任务,并等待任务状态的znode节点。
  • /assign节点作为父节点,其下每个znode子节点保存了分配到某个
    从节点的一个任务信息
    ,当主节点为某个从节点分配了一个任务,就会
    在/assign下增加一个子节点。

API概述

znode节点可能含有数据,也可能没有。如果一个znode节点包含任何数据,那么数据存储为字节数组(byte array)。

字节数组的具体格式特定于每个应用的实现,ZooKeeper并不直接提供解析的支持。我们可以使用如Protocol Buffers、Thrift、Avro或MessagePack等序列化(Serialization)包来方便地处理保存于znode节点的数据格式,不过有些时候,以UTF-8或ASCII编码的字符串已经够用了。

ZooKeeper的API暴露了以下方法:

  • create/path data
    创建一个名为/path的znode节点,并包含数据data。
  • delete/path
    删除名为/path的znode。
  • exists/path
    检查是否存在名为/path的节点。
  • setData/path data
    设置名为/path的znode的数据为data。
  • getData/path
    返回名为/path节点的数据信息。
  • getChildren/path
    返回所有/path节点的所有子节点列表。

需要注意的是,ZooKeeper并不允许局部写入或读取znode节点的数据。当设置一个znode节点的数据或读取时,znode节点的内容会被整个替换或全部读取进来。

znode的不同类型
当新建znode时,还需要指定该节点的类型(mode),不同的类型决定了znode节点的行为方式。
1.持久和临时
znode节点可以是持久(persistent)节点,还可以是临时(ephemeral)节点。持久的znode,如/path,只能通过调用delete来进行删除。临时的znode,当创建该znode的客户端的会话因超时或主动关闭而中止时被删除,或则当某个客户端(不一定是创建者)主动删除该节点时被删除。

因为临时的znode在其创建者的会话过期时被删除,所以我们现在不允许临时节点拥有子节点。
2.有序节点
一个znode还可以设置为有序(sequential)节点。一个有序znode节点被分配唯一个单调递增的整数。
当创建有序节点时,一个序号会被追加到路径之后。例如,如果一个客户端创建了一个有序znode节点,其路径为/tasks/task-,那么ZooKeeper将会分配一个序号,如1,并将这个数字追加到路径之后,最后该znode节点为/tasks/task-1。

监视与通知
ZooKeeper通常以远程服务的方式被访问,如果每次访问znode时,客户端都需要获得节点中的内容,这样的代价就非常大。因为这样会导致更高的延迟,而且ZooKeeper需要做更多的操作。
在这里插入图片描述
考虑图2-2中的例子,第二次调用getChildren/tasks返回了相同的值,一个空的集合,其实是没有必要的。
这是一个常见的轮询问题。为了替换客户端的轮询,我们选择了基于通知(notification)的机制。
在这里插入图片描述
这样做还有一个问题:
因为通知机制是单次触发的操作,所以在客户端接收一个znode变更通知并设置新的监视点时,znode节点也许发生了新的变化。比如下面这个情况:
1.客户端c1 设置监视点来监控/tasks数据的变化。
2.客户端c1 连接后,向/tasks中添加了一个新的任务。
3.客户端c1 接收通知。
4.客户端c1 设置新的监视点,在设置完成前,第三个客户端c3 连接后,向/tasks中添加了一个新的任务。

客户端c1 最终设置了新的监视点,但由c3 添加数据的变更并没有触发一个通知。为了观察这个变更,在设置新的监视点前,c1 实际上需要读取节点/tasks的状态,通过在设置监视点前读取ZooKeeper的状态,最终,c1 就不会错过任何变更。

版本
每一个znode都有一个版本号,它随着每次数据变化而自增。两个API操作可以有条件地执行:setData和delete。这两个调用以版本号作为转入参数,只有当转入参数的版本号与服务器上的版本号一致时调用才会成功
在这里插入图片描述

ZooKeeper架构

ZooKeeper服务器端运行于两种模式下:独立模式(standalone)和仲裁模式(quorum)。
独立模式几乎与其术语所描述的一样:有一个单独的服务器,ZooKeeper状态无法复制。在仲裁模式下,具有一组ZooKeeper服务器,我们称为ZooKeeper集合(ZooKeeper ensemble),它们之前可以进行状态的复制,并同时为服务于客户端的请求。

ZooKeeper仲裁
在仲裁模式下,ZooKeeper复制集群中的所有服务器的数据树。但如果让一个客户端等待每个服务器完成数据保存后再继续,延迟问题将无法接受。
在公共管理领域,法定人数是指进行一项投票所需的立法者的最小数量。而在ZooKeeper中,则是指为了使ZooKeeper工作必须有效运行的服务器的最小数量。这个数字也是服务器告知客户端安全保存数据前,需要保存客户端数据的服务器的最小个数。

为了明白这到底是什么意思,让我们先来通过一个例子来看看:
假设有5个服务器并设置法定人数为2,现在服务器s1 和s2 确认它们需要对一个请求创建的znode/z进行复制,服务返回客户端,指出znode创建完成。
现在假设在复制新的znode到其他服务器之前,服务器s1 和s2 与其他服务器和客户端发生了长时间的分区隔离(更关键在于长时间)。这样会被认定为通信故障,将集群分割成两个分区,分别包含 2 台机器和 3 台机器,因为机器数量都不小于法定人数,所以能独立运行。这样出现了脑裂!

在集合中,服务器的个数并不是必须为奇数,只是使用偶数会使得系统更加脆弱。假设在集合中使用4个服务器,那么多数原则对应的数量为3个服务器。然而,这个系统仅能容许1个服务器崩溃,因为两个服务器崩溃就会导致系统失去多数原则的状态。

会话
在对ZooKeeper集合执行任何请求前,一个客户端必须先与服务建立会话。会话的概念非常重要,对ZooKeeper的运行也非常关键。客户端提交给ZooKeeper的所有操作均关联在一个会话上。当一个会话因某种原因而中止时,在这个会话期间创建的临时节点将会消失。

猜你喜欢

转载自blog.csdn.net/No_Game_No_Life_/article/details/84065496