ZooKeeper 程序员指南
开发使用 ZooKeeper 的分布式应用程序
- 介绍
- ZooKeeper 数据模型
- ZooKeeper 会话
- ZooKeeper 手表
- ZooKeeper 使用 ACL 进行访问控制
- 可插拔 ZooKeeper 身份验证
- 一致性保证
- 绑定
- 构建块:ZooKeeper 操作指南
- 陷阱:常见问题和故障排除
介绍
本文档是为希望创建利用 ZooKeeper 协调服务的分布式应用程序的开发人员提供的指南。它包含概念和实践信息。
本指南的前四个部分对各种 ZooKeeper 概念进行了更高层次的讨论。这些对于理解 ZooKeeper 如何工作以及如何使用它都是必要的。它不包含源代码,但假定您熟悉与分布式计算相关的问题。第一组中的部分是:
接下来的四个部分提供了实用的编程信息。这些都是:
本书最后附有一个附录,其中包含指向其他有用的 ZooKeeper 相关信息的链接。
本文档中的大部分信息都可以作为独立的参考资料访问。但是,在开始您的第一个 ZooKeeper 应用程序之前,您可能至少应该阅读有关ZooKeeper 数据模型和ZooKeeper 基本操作的章节。
ZooKeeper 数据模型
ZooKeeper 有一个分层命名空间,很像分布式文件系统。唯一的区别是命名空间中的每个节点都可以有与其关联的数据以及子节点。这就像拥有一个允许文件也成为目录的文件系统。节点的路径始终表示为规范的、绝对的、斜线分隔的路径;没有相对参考。任何 unicode 字符都可以在受以下约束的路径中使用:
- 空字符 (\u0000) 不能是路径名的一部分。(这会导致 C 绑定出现问题。)
- 不能使用以下字符,因为它们显示不好,或呈现方式混乱:\u0001 - \u001F 和 \u007F
- \u009F。
- 不允许使用以下字符:\ud800 - uF8FF、\uFFF0 - uFFFF。
- 这 ”。” 字符可以用作另一个名称的一部分,但“.” 并且“..”不能单独用于指示路径上的节点,因为 ZooKeeper 不使用相对路径。以下内容无效:“/a/b/./c”或“/a/b/../c”。
- 令牌“动物园管理员”是保留的。
Z节点
ZooKeeper 树中的每个节点都称为znode。Znode 维护一个 stat 结构,其中包括数据更改的版本号、acl 更改。stat 结构也有时间戳。版本号与时间戳一起允许 ZooKeeper 验证缓存并协调更新。每次 znode 的数据更改时,版本号都会增加。例如,每当客户端检索数据时,它也会收到数据的版本。当客户端执行更新或删除时,它必须提供它正在更改的 znode 数据的版本。如果它提供的版本与数据的实际版本不匹配,则更新将失败。(此行为可以被覆盖。
笔记
在分布式应用工程中,节点一词可以指通用主机、服务器、集成成员、客户端进程等。在 ZooKeeper 文档中,znodes指的是数据节点。服务器是指组成 ZooKeeper 服务的机器;quorum peers指的是组成一个整体的服务器;客户端是指使用 ZooKeeper 服务的任何主机或进程。
Znodes 是程序员访问的主要实体。他们有几个特点在这里值得一提。
手表
客户端可以在 znode 上设置监视。对该 znode 的更改会触发监视,然后清除监视。当 watch 触发时,ZooKeeper 会向客户端发送通知。有关手表的更多信息,请参阅ZooKeeper Watches部分。
数据访问
存储在命名空间中每个 znode 的数据是原子读取和写入的。读取获取与 znode 关联的所有数据字节,写入替换所有数据。每个节点都有一个访问控制列表 (ACL),它限制谁可以做什么。
ZooKeeper 不是为通用数据库或大型对象存储而设计的。相反,它管理协调数据。这些数据可以以配置、状态信息、集合点等形式出现。各种形式的协调数据的一个共同特性是它们相对较小:以千字节为单位。ZooKeeper 客户端和服务器实现有健全性检查,以确保 znode 的数据少于 1M,但数据应该比平均数据少得多。在相对较大的数据大小上进行操作将导致某些操作比其他操作花费更多的时间,并且会影响某些操作的延迟,因为通过网络将更多数据移动到存储介质上需要额外的时间。如果需要大数据存储,处理此类数据的通常模式是将其存储在大容量存储系统上,
临时节点
ZooKeeper 也有临时节点的概念。只要创建 znode 的会话处于活动状态,这些 znode 就存在。当会话结束时,znode 被删除。由于这种行为,临时 znode 不允许有子节点。可以使用getEphemerals() api 检索会话的临时列表。
getEphemerals()
检索会话为给定路径创建的临时节点列表。如果路径为空,它将列出会话的所有临时节点。用例- 一个示例用例可能是,如果需要收集会话的临时节点列表以进行重复数据输入检查,并且节点是按顺序创建的,因此您不知道重复检查的名称。在这种情况下,getEphemerals() api 可用于获取会话的节点列表。这可能是服务发现的典型用例。
序列节点——唯一命名
创建 znode 时,您还可以请求 ZooKeeper 在路径末尾附加一个单调递增的计数器。这个计数器对于父 znode 是唯一的。计数器的格式为 %010d - 即 10 位数字和 0(零)填充(计数器以这种方式格式化以简化排序),即“
容器节点
在 3.6.0 中添加
ZooKeeper 有容器 znode 的概念。容器 znode 是特殊用途的 znode,可用于诸如领导者、锁等配方。当容器的最后一个子节点被删除时,该容器将成为服务器在未来某个时间点删除的候选者。
鉴于此属性,您应该准备好在容器 znode 内创建子节点时获取 KeeperException.NoNodeException。即在容器znodes 内创建子znodes 时,总是检查KeeperException.NoNodeException 并在它发生时重新创建容器znode。
TTL 节点
在 3.6.0 中添加
创建 PERSISTENT 或 PERSISTENT_SEQUENTIAL znode 时,您可以选择为 znode 设置一个 TTL(以毫秒为单位)。如果 znode 在 TTL 内没有被修改并且没有子节点,它将成为将来某个时候被服务器删除的候选节点。
注意:TTL 节点必须通过系统属性启用,因为默认情况下它们是禁用的。有关详细信息,请参阅《管理员指南》。如果您尝试在未设置正确系统属性的情况下创建 TTL 节点,服务器将抛出 KeeperException.UnimplementedException。
ZooKeeper 中的时间
ZooKeeper 以多种方式跟踪时间:
- Zxid ZooKeeper 状态的每次更改都会收到zxid(ZooKeeper 事务 ID)形式的标记。这向 ZooKeeper 公开了所有更改的总顺序。每个更改都会有一个唯一的 zxid,如果 zxid1 小于 zxid2,则 zxid1 发生在 zxid2 之前。
- 版本号对节点的每次更改都会导致该节点的版本号增加。三个版本号分别是version(znode的数据变化次数)、cversion(znode子节点的变化次数)和aversion(znode的ACL变化次数)。
- Ticks在使用多服务器 ZooKeeper 时,服务器使用 ticks 来定义事件的计时,例如状态上传、会话超时、对等方之间的连接超时等。tick 时间只是通过最小会话超时(2 倍于 tick 时间)间接暴露; 如果客户端请求的会话超时小于最小会话超时,服务器将告诉客户端会话超时实际上是最小会话超时。
- 实时ZooKeeper 根本不使用实时时间或时钟时间,除了在 znode 创建和 znode 修改时将时间戳放入 stat 结构中。
ZooKeeper 统计结构
ZooKeeper 中每个 znode 的 Stat 结构由以下字段组成:
- czxid导致创建此 znode 的更改的 zxid。
- mzxid最后修改此 znode 的更改的 zxid。
- pzxid最后修改此 znode 子节点的更改的 zxid。
- ctime从创建此 znode 的纪元开始的时间(以毫秒为单位)。
- mtime上次修改此 znode 时从纪元开始的时间(以毫秒为单位)。
- version此 znode 的数据更改次数。
- cversion此 znode 的子节点的更改数。
- aversion此 znode 的 ACL 的更改次数。
- ephemeralOwner如果 znode 是临时节点,则该 znode 所有者的会话 id。如果它不是临时节点,它将为零。
- dataLength此 znode 的数据字段的长度。
- numChildren此 znode 的子节点数。
ZooKeeper 会话
ZooKeeper 客户端通过使用语言绑定创建服务句柄来与 ZooKeeper 服务建立会话。创建后,句柄以 CONNECTING 状态开始,客户端库尝试连接到构成 ZooKeeper 服务的服务器之一,此时它切换到 CONNECTED 状态。在正常操作期间,客户端句柄将处于这两种状态之一。如果发生不可恢复的错误,例如会话过期或身份验证失败,或者应用程序显式关闭句柄,则句柄将移至 CLOSED 状态。下图显示了 ZooKeeper 客户端可能的状态转换:
要创建客户端会话,应用程序代码必须提供一个连接字符串,其中包含以逗号分隔的主机:端口对列表,每个对应于 ZooKeeper 服务器(例如“127.0.0.1:4545”或“127.0.0.1:3000,127.0.0.1 :3001,127.0.0.1:3002”)。ZooKeeper 客户端库将选择一个任意服务器并尝试连接到它。如果此连接失败,或者客户端因任何原因与服务器断开连接,客户端将自动尝试列表中的下一个服务器,直到(重新)建立连接。
在 3.2.0 中添加: 一个可选的“chroot”后缀也可以附加到连接字符串中。这将在解释与此根目录相关的所有路径时运行客户端命令(类似于 unix chroot 命令)。如果使用该示例,该示例将如下所示:“127.0.0.1:4545/app/a”或“127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002/app/a”,其中客户端将植根于"/app/a" 并且所有路径都相对于这个根目录 - 即获取/设置/etc... "/foo/bar" 将导致在 "/app/a/foo/bar" 上运行操作(来自服务器视角)。此功能在多租户环境中特别有用,在这种环境中,特定 ZooKeeper 服务的每个用户都可能具有不同的 root 权限。这使得重用变得更加简单,因为每个用户都可以对他/她的应用程序进行编码,就好像它植根于“/”一样,
当客户端获得 ZooKeeper 服务的句柄时,ZooKeeper 会创建一个 ZooKeeper 会话,以 64 位数字表示,并分配给客户端。如果客户端连接到不同的 ZooKeeper 服务器,它将发送会话 ID 作为连接握手的一部分。作为一项安全措施,服务器会为任何 ZooKeeper 服务器都可以验证的会话 ID 创建一个密码。当客户端建立会话时,该密码会与会话 ID 一起发送给客户端。每当与新服务器重新建立会话时,客户端都会发送此密码和会话 ID。
创建 ZooKeeper 会话的 ZooKeeper 客户端库调用的参数之一是会话超时(以毫秒为单位)。客户端发送请求的超时,服务器以它可以给客户端的超时响应。当前的实现要求超时时间至少为 tickTime 的 2 倍(在服务器配置中设置),最大为 tickTime 的 20 倍。ZooKeeper 客户端 API 允许访问协商的超时。
当客户端(会话)从 ZK 服务集群中分区时,它将开始搜索在会话创建期间指定的服务器列表。最终,当客户端和至少一个服务器之间的连接重新建立时,会话将再次转换到“已连接”状态(如果在会话超时值内重新连接),或者将转换到“过期”状态(如果在会话超时后重新连接)。不建议创建新的会话对象(c 绑定中的新 ZooKeeper.class 或 zookeeper 句柄)来断开连接。ZK 客户端库将为您处理重新连接。特别是我们在客户端库中内置了启发式算法来处理诸如“羊群效应”之类的事情......
会话过期由 ZooKeeper 集群本身管理,而不是由客户端管理。当 ZK 客户端与集群建立会话时,它会提供上面详述的“超时”值。集群使用此值来确定客户端会话何时到期。当集群在指定的会话超时期限(即没有心跳)内没有收到客户端的消息时,就会发生过期。在会话到期时,集群将删除该会话拥有的任何/所有临时节点,并立即将更改通知任何/所有连接的客户端(任何观看这些 znode 的人)。此时过期会话的客户端仍然与集群断开连接,直到/除非它能够重新建立与集群的连接,它才会被通知会话过期。
过期会话的观察者看到的过期会话的示例状态转换:
- 'connected' :会话已建立并且客户端正在与集群通信(客户端/服务器通信正常运行)
- ....客户端从集群中分区
- 'disconnected' : 客户端已失去与集群的连接
- ....时间过去了,在“超时”期限之后,集群使会话过期,客户端没有看到任何内容,因为它与集群断开连接
- .... 时间过去了,客户端重新获得与集群的网络级连接
- 'expired' :最终客户端重新连接到集群,然后通知它过期
ZooKeeper 会话建立调用的另一个参数是默认观察者。当客户端发生任何状态更改时,会通知观察者。例如,如果客户端失去与服务器的连接,客户端将被通知,或者客户端的会话过期等......这个观察者应该认为初始状态是断开的(即在任何状态更改事件被发送给观察者之前客户端库)。在新连接的情况下,发送给观察者的第一个事件通常是会话连接事件。
会话通过客户端发送的请求保持活动状态。如果会话空闲一段时间会超时,客户端将发送 PING 请求以保持会话处于活动状态。这个 PING 请求不仅允许 ZooKeeper 服务器知道客户端仍然处于活动状态,而且还允许客户端验证其与 ZooKeeper 服务器的连接仍然处于活动状态。PING 的时间足够保守,以确保有合理的时间检测到死连接并重新连接到新服务器。
一旦成功建立(连接)到服务器的连接,基本上有两种情况,客户端库在同步或执行异步操作,并满足以下条件之一:
- 应用程序在不再活动/有效的会话上调用操作
- 当服务器有挂起的操作时,ZooKeeper 客户端与服务器断开连接,即有一个挂起的异步调用。
在 3.2.0 中添加 -- SessionMovedException. 有一个称为 SessionMovedException 的客户端通常看不到的内部异常。发生此异常的原因是在连接上接收到已在不同服务器上重新建立的会话的请求。此错误的正常原因是客户端向服务器发送请求,但网络数据包延迟,因此客户端超时并连接到新服务器。当延迟的数据包到达第一台服务器时,旧服务器检测到会话已经移动,并关闭客户端连接。客户端通常不会看到此错误,因为它们不会从那些旧连接中读取。(旧连接通常被关闭。)可以看到这种情况的一种情况是当两个客户端尝试使用保存的会话 ID 和密码重新建立相同的连接时。
更新服务器列表。我们允许客户端通过提供一个新的逗号分隔的主机:端口对列表来更新连接字符串,每个对对应一个 ZooKeeper 服务器。该函数调用概率负载平衡算法,该算法可能导致客户端与其当前主机断开连接,目标是在新列表中实现每个服务器的预期统一连接数。如果客户端连接的当前主机不在新列表中,则此调用将始终导致连接断开。否则,将根据服务器数量是增加还是减少以及增加多少来决定。
例如,如果之前的连接字符串包含 3 台主机,而现在列表包含这 3 台主机和另外 2 台主机,则连接到 3 台主机中的每台主机的 40% 的客户端将移动到其中一台新主机以平衡负载。该算法将导致客户端以 0.4 的概率断开与当前主机的连接,在这种情况下,客户端将连接到随机选择的 2 个新主机之一。
另一个例子——假设我们有 5 台主机,现在更新列表以删除 2 台主机,连接到剩余 3 台主机的客户端将保持连接,而连接到 2 台已删除主机的所有客户端都需要移动到其中一个3 台主机,随机选择。如果连接断开,客户端将进入特殊模式,在该模式下,他使用概率算法选择新服务器进行连接,而不仅仅是轮询。
在第一个示例中,每个客户端决定以 0.4 的概率断开连接,但一旦做出决定,它将尝试连接到随机的新服务器,并且只有当它无法连接到任何新服务器时才会尝试连接到旧服务器那些。在找到服务器或尝试新列表中的所有服务器但连接失败后,客户端返回正常操作模式,从 connectString 中选择任意服务器并尝试连接到它。如果失败,它将继续循环尝试不同的随机服务器。(见上文用于最初选择服务器的算法)
本地会话。在 3.5.0 中添加,主要由ZOOKEEPER-1147实现。
- 背景:ZooKeeper 中会话的创建和关闭成本很高,因为它们需要仲裁确认,当需要处理数千个客户端连接时,它们成为 ZooKeeper 集成的瓶颈。所以在 3.5.0 之后,我们引入了一种新的会话类型:本地会话,它没有普通(全局)会话的全部功能,这个功能将通过打开localSessionsEnabled来使用。
当localSessionsUpgradingEnabled禁用时:
-
本地会话无法创建临时节点
-
一旦本地会话丢失,用户就无法使用会话 ID/密码重新建立它,会话及其监视将永远消失。注意:丢失 tcp 连接并不一定意味着会话丢失。如果可以在会话超时之前与同一个 zk 服务器重新建立连接,那么客户端可以继续(它根本无法移动到另一个服务器)。
-
当本地会话连接时,会话信息仅在其连接的 zookeeper 服务器上维护。领导者不知道此类会话的创建,并且没有写入磁盘的状态。
-
ping、过期和其他会话状态维护由当前会话连接到的服务器处理。
当localSessionsUpgradingEnabled启用时:
-
本地会话可以自动升级为全局会话。
-
创建新会话时,它会在本地保存在包装的LocalSessionTracker中。随后可以根据需要将其升级为全局会话(例如创建临时节点)。如果请求升级,则从本地集合中删除会话,同时保持相同的会话 ID。
-
目前,只有操作:创建临时节点需要将会话从本地升级到全局。原因是临时节点的创建很大程度上依赖于全局会话。如果本地会话可以在不升级到全局会话的情况下创建临时节点,则会导致不同节点之间的数据不一致。领导者还需要了解会话的生命周期,以便在关闭/到期时清理临时节点。这需要一个全局会话,因为本地会话绑定到其特定的服务器。
-
升级过程中一个会话既可以是本地会话也可以是全局会话,但是升级操作不能被两个线程同时调用。
-
ZooKeeperServer(独立)使用SessionTrackerImpl;LeaderZookeeper使用持有SessionTrackerImpl(全局)和LocalSessionTracker(如果启用)的LeaderSessionTracker ;FollowerZooKeeperServer和ObserverZooKeeperServer使用持有LocalSessionTracker的LearnerSessionTracker。关于会话的 UML 类图:
+----------------+ +--------------------+ +---------------------+ | | --> | | ----> | LocalSessionTracker | | SessionTracker | | SessionTrackerImpl | +---------------------+ | | | | +-----------------------+ | | | | +-------------------------> | LeaderSessionTracker | +----------------+ +--------------------+ | +-----------------------+ | | | | | | | +---------------------------+ +---------> | | | UpgradeableSessionTracker | | | | | ------------------------+ +---------------------------+ | | | v +-----------------------+ | LearnerSessionTracker | +-----------------------+
-
问答
- 配置选项禁用本地会话升级的原因是什么?
- 在想要处理大量客户端的大型部署中,我们知道客户端通过观察者进行连接,而观察者应该只是本地会话。所以这更像是防止有人意外创建大量临时节点和全局会话。
-
会话何时创建?
- 在当前实现中,它会在处理ConnectRequest和createSession请求到达FinalRequestProcessor时尝试创建本地会话。
-
如果在服务器 A 上发送创建会话,并且客户端断开与其他服务器 B 的连接,该服务器 B 最终再次发送它,然后断开连接并连接回服务器 A,会发生什么情况?
- 当客户端重新连接到 B 时,其 sessionId 将不会存在于 B 的本地会话跟踪器中。所以B会发送验证包。如果 A 发出的 CreateSession 在验证数据包到达之前提交,则客户端将能够连接。否则,客户端将获得会话过期,因为仲裁尚未知道此会话。如果客户端还尝试再次连接回 A,则会话已从本地会话跟踪器中删除。所以 A 需要向领导者发送一个验证包。结果应与 B 相同,具体取决于请求的时间。
ZooKeeper 手表
ZooKeeper 中的所有读取操作 - getData()、getChildren()和exists() - 都可以选择将监视设置为副作用。下面是 ZooKeeper 对 watch 的定义:watch 事件是一次性触发,发送到设置 watch 的客户端,当设置 watch 的数据发生变化时发生。在这个手表的定义中需要考虑三个关键点:
- 一次性触发当数据发生变化时,会向客户端发送一个 watch 事件。例如,如果客户端执行 getData("/znode1", true) 之后 /znode1 的数据被更改或删除,客户端将获得 /znode1 的监视事件。如果 /znode1 再次更改,则不会发送任何监视事件,除非客户端已进行另一次读取以设置新监视。
- 发送到客户端这意味着事件正在发送到客户端,但在更改操作的成功返回代码到达发起更改的客户端之前,可能无法到达客户端。手表异步发送给观察者。ZooKeeper 提供了排序保证:客户端在第一次看到 watch 事件之前,永远不会看到它设置了 watch 的更改。网络延迟或其他因素可能会导致不同的客户端在不同的时间查看手表并从更新中返回代码。关键是不同客户端看到的所有东西都会有一致的顺序。
- 设置手表的数据这是指节点可以更改的不同方式。将 ZooKeeper 视为维护两个监视列表会有所帮助:数据监视和子监视。getData() 和 exists() 设置数据手表。getChildren() 设置子手表。或者,考虑根据返回的数据类型设置手表可能会有所帮助。getData() 和 exists() 返回有关节点数据的信息,而 getChildren() 返回子节点列表。因此, setData() 将触发正在设置的 znode 的数据监视(假设设置成功)。成功的 create() 将触发正在创建的 znode 的数据监视和父 znode 的子监视。一个成功的 delete() 将触发一个数据监视和一个子监视(因为不能有更多的子节点)被删除的 znode 以及父 znode 的子监视。
监视在客户端连接到的 ZooKeeper 服务器上本地维护。这允许手表在设置、维护和调度方面是轻量级的。当客户端连接到新服务器时,将针对任何会话事件触发监视。与服务器断开连接时将不会收到手表。当客户端重新连接时,任何以前注册的手表都将重新注册并在需要时触发。一般来说,这一切都是透明地发生的。有一种情况可能会丢失监视:如果在断开连接时创建和删除 znode,则会丢失尚未创建的 znode 存在的监视。
3.6.0 中的新功能:客户端还可以在 znode 上设置永久的递归监视,这些监视在触发时不会被删除,并且会以递归方式触发已注册 znode 以及任何子 znode 上的更改。
手表的语义
我们可以使用读取 ZooKeeper 状态的三个调用来设置监视:exists、getData 和 getChildren。以下列表详细说明了手表可以触发的事件以及启用它们的调用:
- 创建事件:通过调用存在启用。
- 已删除事件:通过调用 exists、getData 和 getChildren 启用。
- 更改事件:通过调用 exists 和 getData 启用。
- 子事件:通过调用 getChildren 启用。
持久的、递归的手表
3.6.0 中的新功能:上述标准手表现在有一个变体,您可以设置一个在触发时不会被移除的手表。此外,这些监视触发事件类型NodeCreated、NodeDeleted和NodeDataChanged ,并且可选地,对于从监视注册的 znode 开始的所有 znode 递归。请注意,持久递归监视不会触发NodeChildrenChanged事件,因为它是多余的。
使用方法addWatch()设置持久手表。触发语义和保证(一次性触发除外)与标准手表相同。关于事件的唯一例外是递归持久观察者永远不会触发子更改事件,因为它们是多余的。使用具有观察者类型WatcherType.Any的removeWatches()删除持久观察。
删除手表
我们可以通过调用 removeWatches 来删除在 znode 上注册的手表。此外,即使没有服务器连接,ZooKeeper 客户端也可以通过将本地标志设置为 true 来在本地删除手表。以下列表详细说明了成功移除手表后将触发的事件。
- Child Remove 事件:通过调用 getChildren 添加的 Watcher。
- 数据删除事件:通过调用存在或 getData 添加的观察程序。
- Persistent Remove 事件:通过调用添加持久观察添加的观察者。
ZooKeeper 对手表的保证
关于手表,ZooKeeper 维护以下保证:
-
观察是相对于其他事件、其他观察和异步回复进行排序的。ZooKeeper 客户端库确保按顺序分派所有内容。
-
客户端将在看到对应于该 znode 的新数据之前看到它正在观察的 znode 的观察事件。
-
ZooKeeper 的监视事件顺序对应于 ZooKeeper 服务看到的更新顺序。
关于手表要记住的事情
-
标准手表是一次性触发器;如果您收到手表事件并希望收到有关未来更改的通知,则必须设置另一个手表。
-
因为标准 watch 是一次性触发器,并且在获取事件和发送新请求以获取 watch 之间存在延迟,所以您无法可靠地看到 ZooKeeper 中节点发生的每一次更改。准备好处理 znode 在获取事件和再次设置手表之间多次更改的情况。(你可能不在乎,但至少意识到它可能会发生。)
-
对于给定的通知,监视对象或函数/上下文对只会被触发一次。例如,如果为同一个文件注册了相同的监视对象和 getData 调用,然后删除了该文件,则监视对象只会被调用一次,并带有文件的删除通知。
-
当您与服务器断开连接时(例如,当服务器发生故障时),在重新建立连接之前,您将无法获得任何监视。出于这个原因,会话事件被发送到所有未完成的监视处理程序。使用会话事件进入安全模式:在断开连接时您将不会收到事件,因此您的进程应该在该模式下谨慎行事。
ZooKeeper 使用 ACL 进行访问控制
ZooKeeper 使用 ACL 来控制对其 znode(ZooKeeper 数据树的数据节点)的访问。ACL 实现与 UNIX 文件访问权限非常相似:它使用权限位来允许/禁止对节点的各种操作以及这些位适用的范围。与标准 UNIX 权限不同,ZooKeeper 节点不受用户(文件所有者)、组和世界(其他)三个标准范围的限制。ZooKeeper 没有 znode 所有者的概念。相反,ACL 指定一组 id 和与这些 id 关联的权限。
另请注意,ACL 仅适用于特定的 znode。特别是它不适用于儿童。例如,如果/app只能被 ip:172.16.16.1 读取并且/app/status是世界可读的,那么任何人都可以读取/app/status;ACL 不是递归的。
ZooKeeper 支持可插入的身份验证方案。id 使用表单scheme:expression指定,其中scheme是 id 对应的身份验证方案。有效表达式的集合由方案定义。例如,ip:172.16.16.1是使用ip方案的地址为 172.16.16.1 的主机的 id,而digest : bob:password是使用摘要方案的名称为bob的用户的 id。
当客户端连接到 ZooKeeper 并对其自身进行身份验证时,ZooKeeper 会将与客户端对应的所有 id 与客户端连接相关联。当客户端尝试访问节点时,这些 id 会根据 znode 的 ACL 进行检查。ACL 由成对的(scheme:expression, perms)组成。表达式的格式特定于方案。例如,对(ip:19.22.0.0/16, READ)将READ权限授予 IP 地址以 19.22 开头的任何客户端。
ACL 权限
ZooKeeper 支持以下权限:
- CREATE:您可以创建一个子节点
- READ:您可以从节点获取数据并列出其子节点。
- WRITE:您可以为节点设置数据
- DELETE:您可以删除一个子节点
- ADMIN:您可以设置权限
CREATE和DELETE权限已从WRITE权限中分离出来,以实现更精细的访问控制。CREATE和DELETE的情况如下:
您希望 A 能够在 ZooKeeper 节点上进行设置,但不能创建或删除子节点。
CREATE without DELETE:客户端通过在父目录中创建 ZooKeeper 节点来创建请求。您希望所有客户端都可以添加,但只有请求处理器可以删除。(这有点像文件的 APPEND 权限。)
此外,由于 ZooKeeper 没有文件所有者的概念,因此存在ADMIN权限。在某种意义上,ADMIN权限将实体指定为所有者。ZooKeeper 不支持 LOOKUP 权限(对目录执行权限位以允许您查找,即使您无法列出目录)。每个人都隐含地拥有 LOOKUP 权限。这允许您统计一个节点,但仅此而已。(问题是,如果你想在一个不存在的节点上调用 zoo_exists(),没有权限检查。)
ADMIN权限在 ACL 方面也有特殊作用:为了检索 znode 用户的 ACL,必须具有READ或ADMIN权限,但如果没有ADMIN权限,摘要哈希值将被屏蔽掉。
内置 ACL 方案
ZooKeeper 具有以下内置方案:
- world有一个 id,anyone,它代表任何人。
- auth是一个特殊的方案,它忽略任何提供的表达式,而是使用当前用户、凭据和方案。在持久化 ACL 时,ZooKeeper 服务器会忽略提供的任何表达式(无论是使用 SASL 身份验证的用户还是使用DIGEST 身份验证的user:password )。但是,仍必须在 ACL 中提供表达式,因为 ACL 必须匹配格式scheme:expression:perms。提供此方案是为了方便,因为它是用户创建 znode 然后将对该 znode 的访问限制为仅该用户的常见用例。如果没有经过身份验证的用户,使用 auth 方案设置 ACL 将失败。
- 摘要使用用户名:密码字符串生成 MD5 哈希,然后将其用作 ACL ID 身份。通过以明文形式发送用户名:密码来完成身份验证。在 ACL 中使用时,表达式将是username:base64编码的SHA1密码摘要。
- ip使用客户端主机 IP 作为 ACL ID 身份。ACL 表达式的格式为addr/bits,其中addr的最高有效位与客户端主机 IP的最高有效位相匹配。
- x509使用客户端 X500 Principal 作为 ACL ID 身份。ACL 表达式是客户端的确切 X500 主体名称。使用安全端口时,客户端会自动进行身份验证,并设置 x509 方案的身份验证信息。
ZooKeeper C 客户端 API
ZooKeeper C 库提供以下常量:
- 常量 int ZOO_PERM_READ; //可以读取节点的值并列出其子节点
- const int ZOO_PERM_WRITE;// 可以设置节点的值
- 常量 int ZOO_PERM_CREATE; //可以创建孩子
- const int ZOO_PERM_DELETE;// 可以删除孩子
- 常量 int ZOO_PERM_ADMIN; //可以执行set_acl()
- const int ZOO_PERM_ALL;// 上述所有标志 OR'd 在一起
以下是标准 ACL ID:
- 结构ID ZOO_ANYONE_ID_UNSAFE;//('世界','任何人')
- struct Id ZOO_AUTH_IDS;// ('auth','')
ZOO_AUTH_IDS 空身份字符串应解释为“创建者的身份”。
ZooKeeper 客户端附带三个标准 ACL:
- 结构ACL_vector ZOO_OPEN_ACL_UNSAFE;//(ZOO_PERM_ALL,ZOO_ANYONE_ID_UNSAFE)
- struct ACL_vector ZOO_READ_ACL_UNSAFE;// (ZOO_PERM_READ, ZOO_ANYONE_ID_UNSAFE)
- 结构ACL_vector ZOO_CREATOR_ALL_ACL; //(ZOO_PERM_ALL,ZOO_AUTH_IDS)
ZOO_OPEN_ACL_UNSAFE 对所有 ACL 完全免费开放:任何应用程序都可以在节点上执行任何操作,并且可以创建、列出和删除其子节点。ZOO_READ_ACL_UNSAFE 是任何应用程序的只读访问权限。CREATE_ALL_ACL 将所有权限授予节点的创建者。创建者必须通过服务器的身份验证(例如,使用“ digest ”方案)才能使用此 ACL 创建节点。
以下 ZooKeeper 操作处理 ACL:
- int zoo_add_auth (zhandle_t *zh, const char * scheme, const char * cert, int certLen, void_completion_t completion, const void *data);
应用程序使用 zoo_add_auth 函数向服务器验证自身。如果应用程序想要使用不同的方案和/或身份进行身份验证,则可以多次调用该函数。
- int zoo_create (zhandle_t *zh, const char *path, const char *value, int valuelen, const struct ACL_vector *acl, int flags, char *realpath, int max_realpath_len);
zoo_create(...) 操作创建一个新节点。acl 参数是与节点关联的 ACL 列表。父节点必须设置 CREATE 权限位。
- int zoo_get_acl (zhandle_t *zh, const char *path, struct ACL_vector *acl, struct Stat *stat);
此操作返回节点的 ACL 信息。该节点必须具有 READ 或 ADMIN 权限集。如果没有 ADMIN 权限,摘要哈希值将被屏蔽掉。
- int zoo_set_acl (zhandle_t *zh, const char *path, int version, const struct ACL_vector *acl);
此函数将节点的 ACL 列表替换为新列表。该节点必须具有 ADMIN 权限集。
这是一个示例代码,它利用上述 API 使用“ foo ”方案进行身份验证,并创建一个具有仅创建权限的临时节点“/xyz”。
笔记
这是一个非常简单的示例,旨在展示如何专门与 ZooKeeper ACL 交互。有关C 客户端实现的示例,请参见.../trunk/zookeeper-client/zookeeper-client-c/src/cli.c
#include <string.h>
#include <errno.h>
#include "zookeeper.h"
static zhandle_t *zh;
/**
* In this example this method gets the cert for your
* environment -- you must provide
*/
char *foo_get_cert_once(char* id) { return 0; }
/** Watcher function -- empty for this example, not something you should
* do in real code */
void watcher(zhandle_t *zzh, int type, int state, const char *path,
void *watcherCtx) {}
int main(int argc, char argv) {
char buffer[512];
char p[2048];
char *cert=0;
char appId[64];
strcpy(appId, "example.foo_test");
cert = foo_get_cert_once(appId);
if(cert!=0) {
fprintf(stderr,
"Certificate for appid [%s] is [%s]\n",appId,cert);
strncpy(p,cert, sizeof(p)-1);
free(cert);
} else {
fprintf(stderr, "Certificate for appid [%s] not found\n",appId);
strcpy(p, "dummy");
}
zoo_set_debug_level(ZOO_LOG_LEVEL_DEBUG);
zh = zookeeper_init("localhost:3181", watcher, 10000, 0, 0, 0);
if (!zh) {
return errno;
}
if(zoo_add_auth(zh,"foo",p,strlen(p),0,0)!=ZOK)
return 2;
struct ACL CREATE_ONLY_ACL[] = {{ZOO_PERM_CREATE, ZOO_AUTH_IDS}};
struct ACL_vector CREATE_ONLY = {1, CREATE_ONLY_ACL};
int rc = zoo_create(zh,"/xyz","value", 5, &CREATE_ONLY, ZOO_EPHEMERAL,
buffer, sizeof(buffer)-1);
/** this operation will fail with a ZNOAUTH error */
int buflen= sizeof(buffer);
struct Stat stat;
rc = zoo_get(zh, "/xyz", 0, buffer, &buflen, &stat);
if (rc) {
fprintf(stderr, "Error %d for %s\n", rc, __LINE__);
}
zookeeper_close(zh);
return 0;
}
可插拔 ZooKeeper 身份验证
ZooKeeper 运行在各种不同的环境中,具有各种不同的身份验证方案,因此它具有完全可插拔的身份验证框架。即使是内置的身份验证方案也使用可插入的身份验证框架。
要了解身份验证框架的工作原理,首先必须了解两个主要的身份验证操作。框架首先必须对客户端进行身份验证。这通常在客户端连接到服务器后立即完成,包括验证从客户端发送或收集的关于客户端的信息并将其与连接相关联。框架处理的第二个操作是在 ACL 中查找与客户端对应的条目。ACL 条目是 < idspec, permissions > 对。标识规范可以是与连接关联的身份验证信息匹配的简单字符串,也可以是针对该信息评估的表达式。由身份验证插件的实现来进行匹配。这是身份验证插件必须实现的接口:
public interface AuthenticationProvider {
String getScheme();
KeeperException.Code handleAuthentication(ServerCnxn cnxn, byte authData[]);
boolean isValid(String id);
boolean matches(String id, String aclExpr);
boolean isAuthenticated();
}
第一个方法getScheme返回标识插件的字符串。因为我们支持多种身份验证方法,身份验证凭证或idspec将始终以scheme:为前缀。ZooKeeper 服务器使用身份验证插件返回的方案来确定方案适用于哪些 id。
当客户端发送与连接关联的身份验证信息时,将调用handleAuthentication 。客户端指定信息对应的方案。ZooKeeper 服务器将信息传递给其getScheme与客户端传递的方案匹配的身份验证插件。handleAuthentication的实现者通常会在确定信息错误时返回错误,或者使用cnxn.getAuthInfo().add(new Id(getScheme(), data))将信息与连接关联起来。
身份验证插件涉及设置和使用 ACL。当为 znode 设置 ACL 时,ZooKeeper 服务器会将条目的 id 部分传递给isValid(String id)方法。由插件来验证 id 是否具有正确的形式。例如,ip:172.16.0.0/16是一个有效的 id,但ip:host.com不是。如果新 ACL 包含“auth”条目,则使用isAuthenticated来查看是否应将与连接关联的此方案的身份验证信息添加到 ACL。某些方案不应包含在 auth 中。例如,如果指定了 auth,则客户端的 IP 地址不被视为应添加到 ACL 的 id。
ZooKeeper在检查 ACL 时调用match(String id, String aclExpr) 。它需要将客户端的认证信息与相关的ACL条目进行匹配。为了找到适用于客户端的条目,ZooKeeper 服务器将找到每个条目的方案,如果该方案有来自该客户端的身份验证信息,将调用match(String id, String aclExpr)并将id设置为身份验证之前通过handleAuthentication添加到连接的信息,并将aclExpr设置为 ACL 条目的 id。身份验证插件使用自己的逻辑和匹配方案来确定id是否包含在aclExpr中。
有两个内置的身份验证插件:ip和digest。可以使用系统属性添加其他插件。在启动时,ZooKeeper 服务器将查找以“zookeeper.authProvider”开头的系统属性。并将这些属性的值解释为身份验证插件的类名。可以使用-Dzookeeeper.authProvider.X=com.f.MyAuth或在服务器配置文件中添加如下条目来设置这些属性:
authProvider.1=com.f.MyAuth
authProvider.2=com.f.MyAuth2
应注意确保属性上的后缀是唯一的。如果有-Dzookeeeper.authProvider.X=com.f.MyAuth -Dzookeeper.authProvider.X=com.f.MyAuth2等重复项,则只会使用一个。此外,所有服务器都必须定义相同的插件,否则使用插件提供的身份验证方案的客户端将无法连接到某些服务器。
在 3.6.0 中添加:另一种抽象可用于可插入身份验证。它提供了额外的参数。
public abstract class ServerAuthenticationProvider implements AuthenticationProvider {
public abstract KeeperException.Code handleAuthentication(ServerObjs serverObjs, byte authData[]);
public abstract boolean matches(ServerObjs serverObjs, MatchValues matchValues);
}
您扩展了 ServerAuthenticationProvider,而不是实现 AuthenticationProvider。然后,您的 handleAuthentication() 和 matches() 方法将接收附加参数(通过 ServerObjs 和 MatchValues)。
- ZooKeeperServer ZooKeeperServer 实例
- ServerCnxn当前连接
- path正在操作的 ZNode 路径(如果不使用,则为 null)
- perm操作值或0
- setAcls操作 setAcl() 方法时,正在设置的 ACL 列表
一致性保证
ZooKeeper 是一种高性能、可扩展的服务。读取和写入操作都被设计为快速,尽管读取比写入快。这样做的原因是,在读取的情况下,ZooKeeper 可以服务较旧的数据,这又是由于 ZooKeeper 的一致性保证:
-
顺序一致性:来自客户端的更新将按照它们发送的顺序应用。
-
原子性:更新成功或失败——没有部分结果。
-
单一系统映像:客户端将看到相同的服务视图,而不管它连接到哪个服务器。即,即使客户端故障转移到具有相同会话的不同服务器,客户端也永远不会看到系统的旧视图。
-
可靠性:一旦应用了更新,它将从那时起持续存在,直到客户端覆盖更新。这种保证有两个推论:
- 如果客户端获得成功的返回码,则更新将被应用。在某些故障(通信错误、超时等)上,客户端将不知道更新是否已应用。我们采取措施尽量减少失败,但只有成功的返回代码才会提供保证。(这在 Paxos中称为单调性条件。)
- 客户端通过读取请求或成功更新看到的任何更新,在从服务器故障中恢复时将永远不会回滚。
-
及时性:保证系统的客户端视图在一定的时间范围内(大约几十秒)是最新的。客户端将在此范围内看到系统更改,或者客户端将检测到服务中断。
使用这些一致性保证可以很容易地在 ZooKeeper 客户端构建更高级别的功能,例如领导者选举、屏障、队列和读/写可撤销锁(无需添加到 ZooKeeper)。有关更多详细信息,请参阅食谱和解决方案。
笔记
有时开发人员会错误地假设 ZooKeeper 实际上没有做出的另一种保证。这是:*同时一致的跨客户端视图*:ZooKeeper 不保证在每个实例中,两个不同的客户端将具有相同的 ZooKeeper 数据视图。由于网络延迟等因素,一个客户端可能会在另一个客户端收到更改通知之前执行更新。考虑两个客户端 A 和 B 的场景。如果客户端 A 将 znode /a 的值从 0 设置为 1,然后告诉客户端 B 读取 /a,客户端 B 可能会读取旧值 0,具体取决于哪个服务器它连接到。如果客户端 A 和客户端 B 读取相同的值很重要,客户端 B 应该调用sync()ZooKeeper API 方法中的方法,然后再执行读取。因此,ZooKeeper 本身并不能保证更改在所有服务器之间同步发生,但 ZooKeeper 原语可用于构建提供有用的客户端同步的更高级别的功能。(有关更多信息,请参阅ZooKeeper 食谱。
绑定
ZooKeeper 客户端库有两种语言:Java 和 C。以下部分描述了这些。
Java 绑定
ZooKeeper Java 绑定有两个包:org.apache.zookeeper和org.apache.zookeeper.data。组成 ZooKeeper 的其余包在内部使用或者是服务器实现的一部分。org.apache.zookeeper.data包由生成的类组成,这些类仅用作容器。
ZooKeeper Java 客户端使用的主要类是ZooKeeper类。它的两个构造函数的区别仅在于可选的会话 ID 和密码。ZooKeeper 支持跨进程实例的会话恢复。Java 程序可以将其会话 ID 和密码保存到稳定存储、重新启动和恢复该程序的早期实例使用的会话。
创建 ZooKeeper 对象时,也会创建两个线程:一个 IO 线程和一个事件线程。所有 IO 都发生在 IO 线程上(使用 Java NIO)。所有事件回调都发生在事件线程上。会话维护,例如重新连接到 ZooKeeper 服务器和维护心跳是在 IO 线程上完成的。同步方法的响应也在 IO 线程中处理。对异步方法和监视事件的所有响应都在事件线程上处理。这种设计有几点需要注意:
- 异步调用和观察者回调的所有完成都将按顺序完成,一次完成。调用者可以进行任何他们希望的处理,但在此期间不会处理其他回调。
- 回调不会阻塞 IO 线程的处理或同步调用的处理。
- 同步调用可能不会以正确的顺序返回。例如,假设客户端执行以下处理:在watch设置为 true 的情况下发出节点/a的异步读取,然后在读取的完成回调中执行/a的同步读取。(也许不是好的做法,但也不违法,这是一个简单的例子。)请注意,如果在异步读取和同步读取之间对/a进行了更改,客户端库将收到说/a的监视事件在同步读取的响应之前发生了变化,但是由于完成回调阻塞了事件队列,同步读取将返回/a的新值在处理监视事件之前。
最后,与关闭相关的规则很简单:一旦 ZooKeeper 对象关闭或收到致命事件(SESSION_EXPIRED 和 AUTH_FAILED),ZooKeeper 对象就会失效。关闭时,两个线程关闭,并且对 zookeeper 句柄的任何进一步访问都是未定义的行为,应该避免。
客户端配置参数
以下列表包含 Java 客户端的配置属性。您可以使用 Java 系统属性设置任何这些属性。有关服务器属性,请查看管理员指南的服务器配置部分。ZooKeeper Wiki 也有关于ZooKeeper SSL 支持和ZooKeeper 的 SASL 身份验证的有用页面。
-
zookeeper.sasl.client:将值设置为false以禁用 SASL 身份验证。默认为true。
-
zookeeper.sasl.clientconfig:指定 JAAS 登录文件中的上下文键。默认为“客户端”。
-
zookeeper.server.principal:当启用 Kerberos 身份验证时,指定客户端用于身份验证的服务器主体,同时连接到 zookeeper 服务器。如果提供了这个配置,那么 ZooKeeper 客户端将不会使用以下任何参数来确定服务器主体:zookeeper.sasl.client.username、zookeeper.sasl.client.canonicalize.hostname、zookeeper.server.realm 注意:这个config 参数仅适用于 ZooKeeper 3.5.7+、3.6.0+
-
zookeeper.sasl.client.username:传统上,主体分为三个部分:主、实例和领域。典型的 Kerberos V5 主体的格式是 primary/instance@REALM。zookeeper.sasl.client.username 指定服务器主体的主要部分。默认为“动物园管理员”。实例部分是从服务器 IP 派生的。最后server的principal是username/IP@realm,其中username是zookeeper.sasl.client.username的值,IP是服务器IP,realm是zookeeper.server.realm的值。
-
zookeeper.sasl.client.canonicalize.hostname:期望 zookeeper.server.principal 参数未提供,ZooKeeper 客户端将尝试确定 ZooKeeper 服务器主体的“实例”(主机)部分。首先,它将提供的主机名作为 ZooKeeper 服务器连接字符串。然后它尝试通过获取属于该地址的完全限定域名来“规范化”该地址。您可以通过设置禁用此“规范化”:zookeeper.sasl.client.canonicalize.hostname=false
-
zookeeper.server.realm:服务器主体的领域部分。默认情况下,它是客户端主体领域。
-
zookeeper.disableAutoWatchReset:此开关控制是否启用自动手表重置。默认情况下,客户端在会话重新连接期间自动重置手表,此选项允许客户端通过将 zookeeper.disableAutoWatchReset 设置为true来关闭此行为。
-
zookeeper.client.secure : 3.5.5 中的新功能:如果要连接到服务器安全客户端端口,则需要在客户端上将此属性设置为true。这将使用具有指定凭据的 SSL 连接到服务器。请注意,它需要 Netty 客户端。
-
zookeeper.clientCnxnSocket:指定要使用的 ClientCnxnSocket。可能的值为org.apache.zookeeper.ClientCnxnSocketNIO和org.apache.zookeeper.ClientCnxnSocketNetty。默认为org.apache.zookeeper.ClientCnxnSocketNIO。如果要连接到服务器的安全客户端端口,则需要在客户端将此属性设置为org.apache.zookeeper.ClientCnxnSocketNetty。
-
zookeeper.ssl.keyStore.location 和 zookeeper.ssl.keyStore.password:3.5.5 中的新功能:指定 JKS 的文件路径,其中包含用于 SSL 连接的本地凭据,以及解锁文件的密码。
-
zookeeper.ssl.keyStore.passwordPath:3.8.0 中的新功能:指定包含密钥库密码的文件路径
-
zookeeper.ssl.trustStore.location 和 zookeeper.ssl.trustStore.password:3.5.5 中的新功能:指定 JKS 的文件路径,其中包含用于 SSL 连接的远程凭据,以及解锁文件的密码。
-
zookeeper.ssl.trustStore.passwordPath:3.8.0 中的新功能:指定包含信任库密码的文件路径
-
zookeeper.ssl.keyStore.type和zookeeper.ssl.trustStore.type:3.5.5 中的新功能:指定用于建立与 ZooKeeper 服务器的 TLS 连接的密钥/信任存储文件的文件格式。值:JKS、PEM、PKCS12 或 null(按文件名检测)。默认值:空。3.6.3、3.7.0 中的新功能:添加了 BCFKS 格式。
-
jute.maxbuffer:在客户端,它指定来自服务器的传入数据的最大大小。默认值为 0xfffff(1048575) 字节,或略低于 1M。这真的是一个健全的检查。ZooKeeper 服务器旨在存储和发送以千字节为单位的数据。如果传入的数据长度大于此值,则会引发 IOException。客户端的这个值要和服务端保持一致(在客户端设置System.setProperty("jute.maxbuffer", "xxxx")可以),否则会出问题。
-
zookeeper.kinit:指定 kinit 二进制文件的路径。默认为“/usr/bin/kinit”。
C 绑定
C 绑定具有单线程和多线程库。多线程库最容易使用,并且与 Java API 最相似。该库将创建一个 IO 线程和一个事件调度线程,用于处理连接维护和回调。通过暴露多线程库中使用的事件循环,单线程库允许 ZooKeeper 在事件驱动的应用程序中使用。
该软件包包括两个共享库:zookeeper_st 和 zookeeper_mt。前者仅提供用于集成到应用程序事件循环中的异步 API 和回调。这个库存在的唯一原因是支持平台,如果pthread库不可用或不稳定(即 FreeBSD 4.x)。在所有其他情况下,应用程序开发人员应与 zookeeper_mt 链接,因为它包括对 Sync 和 Async API 的支持。
安装
如果您从 Apache 存储库中签出来构建客户端,请按照下面概述的步骤进行操作。如果您是从从 apache 下载的项目源包构建,请跳到第3步。
ant compile_jute
从 ZooKeeper 顶级目录 ( .../trunk )运行。这将在.../trunk/zookeeper-client/zookeeper-client-c下创建一个名为“generated”的目录。- 将目录更改为 *.../trunk/zookeeper-client/zookeeper-client-c* 并运行
autoreconf -if
以引导autoconf、automake和libtool。确保您已安装autoconf 版本 2.59或更高版本。跳到步骤4。 - 如果您是从项目源代码包构建,请将源 tarball 和 cd 解压缩/解压缩到* zookeeper-xxx/zookeeper-client/zookeeper-client-c* 目录。
- 运行
./configure <your-options>
以生成生成文件。以下是配置实用程序支持的一些在此步骤中有用的选项:
--enable-debug
启用优化并启用调试信息编译器选项。(默认禁用。)--without-syncapi
禁用同步 API 支持;zookeeper_mt 库不会被构建。(默认启用。)--disable-static
不要构建静态库。(默认启用。)--disable-shared
不要构建共享库。(默认启用。)
笔记
有关运行configure的一般信息,请参阅安装。1. 运行
make
或make install
构建库并安装它们。1. 要为 ZooKeeper API 生成 doxygen 文档,请运行make doxygen-doc
. 所有文档都将放置在名为 docs 的新子文件夹中。默认情况下,此命令仅生成 HTML。有关其他文档格式的信息,请运行./configure --help
构建自己的 C 客户端
为了能够在您的应用程序中使用 ZooKeeper C API,您必须记住
- 包括 ZooKeeper 标头:
#include <zookeeper/zookeeper.h>
- 如果您正在构建多线程客户端,请使用
-DTHREADED
编译器标志进行编译以启用库的多线程版本,然后链接到zookeeper_mt库。如果您正在构建单线程客户端,请不要使用 编译-DTHREADED
,并确保链接到 the_zookeeper_st_library。
笔记
有关C 客户端实现的示例,请参见.../trunk/zookeeper-client/zookeeper-client-c/src/cli.c
构建块:ZooKeeper 操作指南
本节调查开发人员可以对 ZooKeeper 服务器执行的所有操作。它是比本手册中早期概念章节更低级别的信息,但比 ZooKeeper API 参考更高级别。它涵盖了以下主题:
处理错误
Java 和 C 客户端绑定都可能报告错误。Java 客户端绑定通过抛出 KeeperException 来实现,对异常调用 code() 将返回特定的错误代码。C 客户端绑定返回枚举 ZOO_ERRORS 中定义的错误代码。API 回调指示两种语言绑定的结果代码。有关可能的错误及其含义的完整详细信息,请参阅 API 文档(Java 的 javadoc,C 的 doxygen)。
连接到 ZooKeeper
在我们开始之前,您必须设置一个正在运行的 Zookeeper 服务器,以便我们可以开始开发客户端。对于 C 客户端绑定,我们将使用多线程库(zookeeper_mt)和一个用 C 编写的简单示例。为了建立与 Zookeeper 服务器的连接,我们使用 C API - zookeeper_init,其签名如下:
int zookeeper_init(const char *host, watcher_fn fn, int recv_timeout, const clientid_t *clientid, void *context, int flags);
-
* host : 到zookeeper服务器的连接字符串,格式为host:port。如果有多个服务器,请在指定主机:端口对后使用逗号作为分隔符。例如:“127.0.0.1:2181,127.0.0.1:3001,127.0.0.1:3002”
-
fn : 触发通知时处理事件的观察函数。
-
recv_timeout:会话过期时间,以毫秒为单位。
-
* clientid:我们可以为新会话指定 0。如果会话之前已经建立,我们可以提供该客户端 ID,它会重新连接到之前的会话。
-
* context : 可以与 zkhandle_t 处理程序关联的上下文对象。如果不使用,我们可以将其设置为 0。
-
flags:在启动中,我们可以将其保留为 0。
我们将演示客户端在成功连接后输出“Connected to Zookeeper”或错误消息。让我们调用以下代码zkClient.cc:
#include <stdio.h>
#include <zookeeper/zookeeper.h>
#include <errno.h>
using namespace std;
// Keeping track of the connection state
static int connected = 0;
static int expired = 0;
// *zkHandler handles the connection with Zookeeper
static zhandle_t *zkHandler;
// watcher function would process events
void watcher(zhandle_t *zkH, int type, int state, const char *path, void *watcherCtx)
{
if (type == ZOO_SESSION_EVENT) {
// state refers to states of zookeeper connection.
// To keep it simple, we would demonstrate these 3: ZOO_EXPIRED_SESSION_STATE, ZOO_CONNECTED_STATE, ZOO_NOTCONNECTED_STATE
// If you are using ACL, you should be aware of an authentication failure state - ZOO_AUTH_FAILED_STATE
if (state == ZOO_CONNECTED_STATE) {
connected = 1;
} else if (state == ZOO_NOTCONNECTED_STATE ) {
connected = 0;
} else if (state == ZOO_EXPIRED_SESSION_STATE) {
expired = 1;
connected = 0;
zookeeper_close(zkH);
}
}
}
int main(){
zoo_set_debug_level(ZOO_LOG_LEVEL_DEBUG);
// zookeeper_init returns the handler upon a successful connection, null otherwise
zkHandler = zookeeper_init("localhost:2181", watcher, 10000, 0, 0, 0);
if (!zkHandler) {
return errno;
}else{
printf("Connection established with Zookeeper. \n");
}
// Close Zookeeper connection
zookeeper_close(zkHandler);
return 0;
}
使用前面提到的多线程库编译代码。
> g++ -Iinclude/ zkClient.cpp -lzookeeper_mt -o Client
运行客户端。
> ./Client
从输出中,如果连接成功,您应该会看到“Connected to Zookeeper”以及 Zookeeper 的 DEBUG 消息。
陷阱:常见问题和故障排除
所以现在你知道了 ZooKeeper。它快速、简单,您的应用程序可以工作,但是等等……出了点问题。以下是 ZooKeeper 用户陷入的一些陷阱:
- 如果您使用手表,则必须查找已连接的手表事件。当 ZooKeeper 客户端与服务器断开连接时,您将不会收到更改通知,直到重新连接。如果您正在观察一个 znode 的存在,如果 znode 在您断开连接时被创建和删除,您将错过该事件。
- 您必须测试 ZooKeeper 服务器故障。只要大多数服务器处于活动状态,ZooKeeper 服务就可以在故障中幸存下来。要问的问题是:您的应用程序可以处理它吗?在现实世界中,客户端与 ZooKeeper 的连接可能会中断。(ZooKeeper 服务器故障和网络分区是连接丢失的常见原因。)ZooKeeper 客户端库负责恢复您的连接并让您知道发生了什么,但您必须确保恢复您的状态和任何未完成的失败请求。看看您是否在测试实验室中做对了,而不是在生产环境中 - 使用由多个服务器组成的 ZooKeeper 服务进行测试,并让它们重新启动。
- 客户端使用的 ZooKeeper 服务器列表必须与每个 ZooKeeper 服务器拥有的 ZooKeeper 服务器列表匹配。如果客户端列表是 ZooKeeper 服务器的真实列表的子集,则事情可以工作,尽管不是最佳的,但如果客户端列出不在 ZooKeeper 集群中的 ZooKeeper 服务器,则不能。
- 小心放置事务日志的位置。ZooKeeper 对性能最关键的部分是事务日志。ZooKeeper 必须在返回响应之前将事务同步到媒体。专用事务日志设备是始终如一的良好性能的关键。将日志放在繁忙的设备上会对性能产生不利影响。如果你只有一个存储设备,把trace文件放到NFS上,增加snapshotCount;它不能消除问题,但可以减轻它。
- 正确设置 Java 最大堆大小。避免交换非常重要。不必要地访问磁盘几乎肯定会使您的性能下降到无法接受的程度。请记住,在 ZooKeeper 中,一切都是有序的,因此如果一个请求命中磁盘,则所有其他排队的请求都会命中磁盘。为避免交换,请尝试将堆大小设置为您拥有的物理内存量减去操作系统和缓存所需的量。为您的配置确定最佳堆大小的最佳方法是运行负载测试。如果由于某种原因您不能,请保守估计并选择一个远低于可能导致您的机器交换的限制的数字。例如,在 4G 机器上,3G 堆是开始时的保守估计。
其他信息的链接
在正式文档之外,ZooKeeper 开发人员还有其他几个信息来源。
-
API 参考:ZooKeeper API 的完整参考
-
2008 年 Hadoop 峰会上的 ZooKeeper 演讲:Yahoo! 的 Benjamin Reed 的 ZooKeeper 视频介绍 研究
-
屏障和队列教程:Flavio Junqueira 的优秀 Java 教程,使用 ZooKeeper 实现简单的屏障和生产者-消费者队列。
-
ZooKeeper - 可靠、可扩展的分布式协调系统:Todd Hoff 的文章 (07/15/2008)
-
ZooKeeper 秘诀:使用 ZooKeeper 实现各种同步解决方案的伪级讨论:事件句柄、队列、锁和两阶段提交。