1. ZooKeeper自下向上的服务视图
Apache ZooKeeper是分布式应用程序的协调服务。 它旨在解决分布式应用程序中与组件协调相关的棘手问题。 它通过暴露一个简单而强大的接口来实现这一点。 应用程序可以设计在通过ZooKeeper API实现的这些接口上,以解决分布式同步,集群配置管理,组成员身份等问题。
ZooKeeper本身就是一个复制和分布式应用程序,其目的作为服务运行,类似于我们运行DNS或任何其他集中式服务的方式。 ZooKeeper服务的视图如下图所示:
从之前的图(该图从http://zookeeper.apache.org/doc/r3.4.6/zookeeperOver.html上能够找到),将看到运行ZooKeeper服务的复制服务器集,被称为ensemble。 客户可以通过连接到集合的任何成员来连接到ZooKeeper服务。 你可以发送和接收请求和响应,以及客户端和服务之间的事件通知,这些都是通过维护TCP连接并定期发送心跳来完成的。
Note
ensemble中的成员知道彼此的状态。 这意味着当前的内存中状态,事务日志以及服务状态的时间点副本通过构成整体的各个主机以持久的方式存储在本地数据存储中。 ZooKeeper是一个高度可用的服务,所以只要大部分服务器都可用,服务将始终可用。
ZooKeeper对事务进行了严格的排序,从而实现了简单可靠的高级分布式同步原语。 凭借其强大,可靠,高性能和快速的设计,该协调服务使其可用于大型复杂的分布式应用程序。
2. ZooKeeper数据模型
根据ZooKeeper wiki的定义,ZooKeeper允许分布式进程通过数据寄存器的共享分层命名空间相互协调。命名空间看起来非常类似于Unix文件系统。 数据寄存器在ZooKeeper命名中被称为znode。 可以在下面的图片中看到znodes的例子:
在这里,可以看到znode是一个标准的文件系统,层次结构很像一棵树。 需要注意的一些要点如下:
- 根节点有一个名为/zoo的子节点,它又有三个znode。
- ZooKeeper树中的每个znode都由一个路径标识,路径元素由『/』分隔。
- 这些节点被称为数据寄存器,因为它们可以存储数据。 因此,一个znode可以有子节点以及与之相关的数据。 这与文件系统可以把文件作为路径很类似。
znode中的数据通常以字节格式存储,每个znode中的最大数据大小不超过1 MB。 ZooKeeper是为协调而设计的,几乎所有形式的协调数据都比较小, 因此,对数据大小的限制是强制的。 建议实际的数据大小也要小于这个限制。
斜杠分隔的znode路径是规范的,必须是绝对路径。 相对路径和引用不被ZooKeeper识别。 znode名称可以由Unicode字符组成并且znode可以具有任何名称。 这个例外是ZooKeeper这个词是保留的。 最重要的是,使用“.” 作为一个路径组件是非法的。
与文件系统中的文件一样,znode维护一个stat结构,其中包含数据更改的版本号以及随更改相关的时间戳而更改的访问控制列表。 只要znode的数据发生变化,版本号就会增加。 ZooKeeper使用版本号以及相关的时间戳来验证它的核心内缓存。 znode版本号还允许客户端通过ZooKeeper API更新或删除特定的znode。 如果指定的版本号与znode的当前版本不匹配,则操作失败。 但是,执行znode更新或删除操作时,可以通过指定0作为版本号来覆盖。
(1) znode的类型
ZooKeeper有两种类型的znode:persistent和ephemeral。 可能已经听说过第三种类型,称为sequential的znode,这是另一种类型的限定符。 persistent和ephemeral的znode也可以是sequential的znode。 请注意,znode的类型是在创建时设置的。
(2) persistent类型的znode
顾名思义,persistent的znode在ZooKeeper的命名空间中有一个生命周期,直到它们被明确的删除。 一个znode可以通过调用delete API调用来删除。 没有必要只有创建持久化znode的客户端才能删除它。 请注意,任何ZooKeeper服务的授权客户端都可以删除一个znode。
现在使用ZooKeeper Java shell创建一个持久化的znode:
[zk: localhost(CONNECTED) 1] create /[PacktPub] "ApacheZooKeeper"
Created /[PacktPub]
[zk: localhost(CONNECTED) 2] get /[PacktPub]
"ApacheZooKeeper"
persistent的znodes对于存储需要高度可用且可由分布式应用程序的所有组件访问的数据时非常有用。 例如,应用程序可以将配置数据存储在持久的znode中。 即使创建者客户端死亡,数据以及znode也会存在。
(3) ephemeral类型的znode
相比之下,当创建客户端的会话结束时,ZooKeeper服务会删除一个ephemeral的znode。 客户端会话的结束可能由于客户端崩溃或连接的明确终止而发生断开连接。 尽管临时节点与客户端会话绑定,但它们对所有客户端均可见,具体取决于配置的访问控制列表(ACL)策略。
创建者客户端或任何其他授权客户端也可以通过使用删除API调用来显式删除ephemeral 的znode。 一旦其创建者客户端与ZooKeeper服务的会话结束,ephemeral 的znode就不复存在。 因此,在当前版本的ZooKeeper中,短暂的znodes不允许有子节点。
要使用ZooKeeper Java Shell创建一个ephemeral的znode,必须在create
命令中指定-e
标志,可以使用以下命令完成:
[zk: localhost(CONNECTED) 1] create -e /[PacktPub] "ApacheZooKeeper"
Created /[PacktPub]
现在,由于一个ephemeral的znode不允许有子节点,如果我们试图创建一个刚刚创建的子节点znode时,会被抛出一个错误,如下所示:
[zk: localhost(CONNECTED) 2] create -e /[PacktPub]/EphemeralChild "ChildOfEphemeralZnode"
Ephemerals cannot have children: /[PacktPub]/EphemeralChild
ephemeral的节点的用途可用于构建分布式应用程序,其中组件需要知道其他组件或资源的状态。 例如,分布式组成员资格服务可以通过使用ephemera 的znode来实现。 当创建者客户端会话结束时,ephemeral节点被删除的属性可用作加入或离开分布式集群的节点的模拟。 使用会员服务,任何节点都能够在任何特定的时间发现组的成员。
(4)sequential类型的znode
一个sequential的znode在ZooKeeper创建它的时候,分配一个序列号作为其名字的一部分。 一个单调递增的计数器(由父znode维护)的值被附加到znode的名称上。
计数器是一个有符号整数类型(4个字节),用来存储序号的。 它一共10位的格式,前面填充0。 例如,/path/to/znode-0000000001
。 这个命名约定对分配给它们的值对sequential的znode进行排序很有用。
Note
sequential的节点可以用于实现分布式全局队列,因为顺序号可以强制一个全局顺序。 它们也可以用来为分布式应用程序设计一个锁定服务。
由于persistent和ephemeral的znode都可以是sequential类型的znode,因此总共有四种znode模式:
- persistent
- ephemeral
- persistent_sequential
- ephemeral_sequential
使用ZooKeeper Java shell创建一个sequential的znode,我们必须使用create
命令的-s
标志:
[zk: localhost(CONNECTED) 1] create -s /[PacktPub] "PersistentSequentialZnode"
Created /[PacktPub]0000000001
[zk: localhost(CONNECTED) 3] create -s -e /[PacktPub] "EphemeralSequentialZnode"
Created /[PacktPub]0000000008
(5) 关注znode变化 —— ZooKeeper 监视
对于大型分布式应用程序,ZooKeeper的设计是一种可伸缩的、健壮的集中式服务。在客户端访问此类服务时,常见的设计反模式是通过轮询或拉式(pull)模型。当在大型和复杂的分布式系统中实现时,拉模型常常会受到可伸缩性问题的影响。为了解决这个问题,ZooKeeper设计了一种机制,客户端可以从ZooKeeper服务中获取通知,而不是轮询事件。这类似于一个推(push)模型,在这个模型中,通知被推送到ZooKeeper服务的注册客户端。
客户可以使用ZooKeeper服务注册与znode相关的任何更改。 这种注册被称为在ZooKeeper术语中的znode上设置监视(watch)。 监视允许客户以任何方式更改znode时收到通知。 监视是一次性操作,这意味着它只触发一个通知。 要继续接收通知,客户必须在收到每个事件通知后重新注册一个监视。
让我们通过一个集群组成员模型的例子来说明ZooKeeper监视和通知的概念:
- 在群集中,一个节点(例如Client1)在另一个节点加入群集时收到通知。 任何加入群集的节点都会在ZooKeeper路径
/Members
下创建一个ephemeral节点。 - 现在,另一个节点Client2加入群集,并在
/Members
下创建一个名为Host2的ephemeral节点。 - Client1在ZooKeeper路径
/Members
上发出一个getChildren
请求,并设置一个任何改动的监视。 当Client2创建一个znode作为/Members/Host2
时,监视被触发,Client1收到来自ZooKeeper服务的通知。 如果Client1现在在ZooKeeper路径/Members
上发出getChildren
请求,它将看到新的Host2的znode。 以下图片显示了监视设置的流程,以及监视的通知和后续重置:
ZooKeeper监视是一次性触发器。 这意味着如果一个客户端收到一个监视事件并想要得到未来变化的通知,那么它必须设置另一个监视。 每当监视被触发时,就会将一个通知分派给已经设置监视的客户端。 监视在维护与客户端连接的ZooKeeper服务器上,这使得它成为事件通知的快速和精益的方式。
监视触发以下三个变化到一个znode:
- 对znode数据的任何更改,例如使用
setData
操作将新数据写入znode的数据字段时。 - 对znode的子节点的任何更改。 例如,一个znode的子节点被删除。
- 正在创建或删除的znode,如果将新的znode添加到路径中或现有的znode被删除,则可能发生这种情况。
同样,ZooKeeper针对监视和通知声明以下保证:
- ZooKeeper确保监视始终以先进先出(FIFO)方式排序,并且通知总是按顺序发送
- 在对同一个znode进行任何其他更改之前,监视会将通知发送给客户端
- 监视事件的顺序是按照ZooKeeper服务的更新顺序排列的。
Note
由于ZooKeeper的监视是一次性触发的,由于在获取监视事件和重新设置监视之间存在延迟,所以客户端可能会在此期间丢失对znode的更改。在分布式应用程序中,一个znode在事件的调度和对事件的重新设置之间进行多次更改,开发人员必须小心处理应用程序逻辑中的这种情况。
当客户端与ZooKeeper服务器断开连接时,在连接重新建立之前,它不会收到任何监视。 如果客户重新连接,任何以前注册的监视也将被重新注册并触发。 如果客户端连接到一个新的服务器,监视将会触发任何会话事件。 从服务器断开连接并重新连接到新服务器对于客户端应用程序而言是透明的。
虽然ZooKeeper保证所有已注册的监视都被分派到客户端,但即使客户端从一台服务器断开连接并重新连接到ZooKeeper服务中的另一台服务器,也有一种可能的情况值得一提,即客户可能会错过一个监视。 这个特定的场景是,当客户端已经设置了一个尚未创建的znode的存在。 在这种情况下,在客户端处于断开状态创建和删除znode时,监视就会丢失。