Apache > ZooKeeper
 

ZooKeeper 程序员指南

开发使用 ZooKeeper 的分布式应用程序

介绍

本文档旨在指导希望利用 ZooKeeper 协调服务的开发者构建分布式应用程序。它包含概念性和实用性信息。

本指南的前四个部分从更高级别讨论了各种 ZooKeeper 概念。这些对于理解 ZooKeeper 的工作原理以及如何使用它都是必需的。它不包含源代码,但假定读者熟悉分布式计算相关的问题。第一组的这些部分是:

接下来的四个部分提供实用编程信息。这些部分是:

本文档以包含其他有用 ZooKeeper 相关信息的附录作为结尾。

本文档中的大部分信息都可作为独立参考材料使用。然而,在开始构建你的第一个 ZooKeeper 应用程序之前,你最好至少阅读有关ZooKeeper 数据模型ZooKeeper 基本操作的章节。

ZooKeeper 数据模型

ZooKeeper 拥有一个分层命名空间,非常类似于分布式文件系统。唯一的区别是命名空间中的每个节点都可以关联数据,也可以拥有子节点。这就像一个允许文件同时也是目录的文件系统。节点的路径总是表示为规范的、绝对的、斜杠分隔的路径;没有相对引用。路径中可以使用任何 Unicode 字符,但受以下限制:

ZNode

ZooKeeper 树中的每个节点都称为 znode。ZNode 维护一个 stat 结构,其中包含数据更改和 ACL 更改的版本号。stat 结构还包含时间戳。版本号与时间戳一起,使 ZooKeeper 能够验证缓存并协调更新。每次 ZNode 的数据发生变化时,版本号都会增加。例如,每当客户端检索数据时,它也会收到数据的版本。当客户端执行更新或删除操作时,它必须提供正在更改的 ZNode 的数据的版本。如果提供的版本与数据的实际版本不匹配,则更新将失败。(此行为可以被覆盖。)

注意

在分布式应用工程中,词语 node 可以指代通用主机、服务器、集群(ensemble)成员、客户端进程等。在 ZooKeeper 文档中,znodes 指代数据节点。Servers 指构成 ZooKeeper 服务的机器;quorum peers 指构成集群(ensemble)的服务器;client 指使用 ZooKeeper 服务的任何主机或进程。

ZNode 是程序员访问的主要实体。它们具有以下几个值得一提的特性:

Watch

客户端可以在 ZNode 上设置 Watch。ZNode 的更改会触发 Watch,然后清除 Watch。当 Watch 触发时,ZooKeeper 会向客户端发送通知。关于 Watch 的更多信息可以在 ZooKeeper Watch 机制部分找到。

数据访问

命名空间中每个 ZNode 上存储的数据都是原子地读取和写入的。读取操作获取与 ZNode 相关的所有数据字节,写入操作替换所有数据。每个节点都有一个访问控制列表 (ACL),用于限制谁可以执行什么操作。

ZooKeeper 设计的初衷不是作为一个通用的数据库或大型对象存储。相反,它管理协调数据。这些数据可以以配置、状态信息、集合点(rendezvous)等形式出现。各种形式的协调数据的一个共同特点是它们相对较小:以千字节为单位衡量。ZooKeeper 客户端和服务器实现都有健全性检查,以确保 ZNode 的数据小于 1M,但平均而言,数据应该远小于此。处理相对较大的数据量会导致某些操作比其他操作花费更多时间,并会影响某些操作的延迟,因为需要额外的时间通过网络将更多数据移动到存储介质上。如果需要存储大量数据,处理此类数据的通常模式是将其存储在大容量存储系统(例如 NFS 或 HDFS)上,并在 ZooKeeper 中存储指向这些存储位置的指针。

临时节点

ZooKeeper 还有临时节点(Ephemeral Nodes)的概念。这些 ZNode 只要创建它们的会话处于活动状态就存在。当会话结束时,该 ZNode 会被删除。由于这种行为,临时 ZNode 不允许有子节点。可以使用 getEphemerals() API 检索会话的临时节点列表。

getEphemerals()

检索会话为给定路径创建的临时节点列表。如果路径为空,它将列出该会话的所有临时节点。用例 - 一个示例用例可能是,如果需要收集会话的临时节点列表以进行重复数据录入检查,并且节点是按顺序创建的,因此您不知道用于重复检查的名称。在这种情况下,可以使用 getEphemerals() API 获取会话的节点列表。这可能是服务发现的典型用例。

顺序节点 -- 唯一命名

创建 ZNode 时,您还可以请求 ZooKeeper 在路径末尾追加一个单调递增的计数器。此计数器对于父 ZNode 来说是唯一的。计数器的格式为 %010d -- 即 10 位数字,用 0(零)填充(计数器采用这种格式是为了简化排序),例如 ""0000000001"。请参阅队列教程 (Queue Recipe),了解此功能的示例用法。注意:用于存储下一个序列号的计数器是父节点维护的带符号整数 (4 字节),当递增超过 2147483647 时,计数器会溢出(导致名称变为 ""-2147483648").

容器节点

添加于 3.5.3

ZooKeeper 引入了容器节点(Container ZNode)的概念。容器节点是用于实现领导者选举、分布式锁等典型用例(recipes)的特殊用途 ZNode。当容器的最后一个子节点被删除时,该容器将成为服务器在未来某个时间点删除的候选对象。

鉴于此特性,在容器节点内创建子节点时,您应该准备好捕获 KeeperException.NoNodeException 异常。也就是说,在容器节点内创建子节点时,始终检查 KeeperException.NoNodeException 异常,并在发生时重新创建容器节点。

TTL 节点

添加于 3.5.3

创建 PERSISTENT 或 PERSISTENT_SEQUENTIAL ZNode 时,您可以选择性地为 ZNode 设置一个以毫秒为单位的 TTL(存活时间)。如果在 TTL 内未修改该 ZNode 且它没有子节点,它将成为服务器在未来某个时间点删除的候选对象。

注意:TTL 节点必须通过系统属性启用,因为它们默认是禁用的。详情请参阅管理员指南。如果您尝试在未设置正确的系统属性的情况下创建 TTL 节点,服务器将抛出 KeeperException.UnimplementedException 异常。

ZooKeeper 中的时间

ZooKeeper 以多种方式跟踪时间:

ZooKeeper Stat 结构

ZooKeeper 中每个 ZNode 的 Stat 结构由以下字段组成:

ZooKeeper 会话

ZooKeeper 客户端通过使用语言绑定创建服务句柄来与 ZooKeeper 服务建立会话。创建后,句柄以 CONNECTING 状态开始,客户端库尝试连接到组成 ZooKeeper 服务的服务器之一,此时它切换到 CONNECTED 状态。正常操作期间,客户端句柄将处于这两种状态之一。如果发生不可恢复的错误(例如会话过期或认证失败),或者应用程序显式关闭句柄,句柄将移至 CLOSED 状态。下图显示了 ZooKeeper 客户端可能的状态转换:

State transitions

要创建客户端会话,应用程序代码必须提供一个连接字符串,其中包含一个逗号分隔的 host:port 对列表,每个对对应一个 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" 为根,所有路径都将相对于此根目录 - 即获取/设置等操作 "/foo/bar" 将导致(从服务器角度来看)在 "/app/a/foo/bar" 上运行操作。此功能在多租户环境中特别有用,其中特定 ZooKeeper 服务的每个用户可以有不同的根目录。这使得重用更加简单,因为每个用户都可以像应用程序根目录在 "/" 一样编写代码,而实际位置(例如 /app/a)可以在部署时确定。

当客户端获取 ZooKeeper 服务的句柄时,ZooKeeper 会创建一个 ZooKeeper 会话,表示为一个 64 位数字,并将其分配给客户端。如果客户端连接到不同的 ZooKeeper 服务器,它将在连接握手时发送会话 ID。作为一项安全措施,服务器为会话 ID 创建一个密码,任何 ZooKeeper 服务器都可以验证该密码。客户端建立会话时,会将密码连同会话 ID 一起发送给客户端。客户端在与新服务器重新建立会话时,都会随会话 ID 发送此密码。

ZooKeeper 客户端库调用创建 ZooKeeper 会话的参数之一是会话超时时间(毫秒)。客户端发送一个请求的超时时间,服务器响应一个它可以提供给客户端的超时时间。当前实现要求超时时间最小为 tickTime(在服务器配置中设置)的 2 倍,最大为 tickTime 的 20 倍。ZooKeeper 客户端 API 允许访问协商后的超时时间。

当客户端(会话)与 ZK 服务集群隔离时,它将开始搜索在会话创建期间指定的服务器列表。最终,当客户端与至少一个服务器之间的连接重新建立时,会话将要么再次转换为“connected”状态(如果在会话超时值内重新连接),要么转换为“expired”状态(如果在会话超时后重新连接)。对于断开连接,不建议创建新的会话对象(Java 绑定中的 ZooKeeper.class 或 C 绑定中的 zookeeper 句柄)。ZK 客户端库将为您处理重新连接。特别是,我们在客户端库中内置了启发式算法来处理诸如“羊群效应”(herd effect)等问题。仅当您收到会话过期通知时才创建新会话(这是强制性的)。

会话过期由 ZooKeeper 集群本身管理,而非客户端。当 ZK 客户端与集群建立会话时,它会提供上面详述的“timeout”值。集群使用此值来确定客户端会话何时过期。当集群在指定的会话超时时间内未收到客户端的任何信息(即没有心跳)时,就会发生过期。会话过期时,集群将删除该会话拥有的所有临时节点,并立即通知所有连接的客户端此更改(所有监听这些 ZNode 的客户端)。此时,已过期会话的客户端仍然与集群断开连接,它不会收到会话过期通知,除非/直到它能够重新建立与集群的连接。客户端将保持断开连接状态,直到与集群的 TCP 连接重新建立,届时过期会话的观察者将收到“session expired”通知。

过期会话的观察者看到的示例状态转换:

  1. 'connected':会话已建立,客户端正在与集群通信(客户端/服务器通信正常运行)
  2. .... 客户端与集群隔离
  3. 'disconnected':客户端已失去与集群的连接
  4. .... 时间流逝,经过 'timeout' 时期后,集群使会话过期,客户端由于与集群断开连接而看不到任何内容
  5. .... 时间流逝,客户端重新获得与集群的网络连接
  6. 'expired':最终客户端重新连接到集群,然后收到过期通知

ZooKeeper 会话建立调用的另一个参数是默认观察者(default watcher)。当客户端发生任何状态变化时,观察者会收到通知。例如,如果客户端失去与服务器的连接,客户端将收到通知,或者客户端会话过期等。此观察者应将初始状态视为断开连接(即客户端库向观察者发送任何状态更改事件之前)。在建立新连接的情况下,发送给观察者的第一个事件通常是会话连接事件。

会话通过客户端发送的请求保持活动状态。如果会话空闲时间长到会导致会话超时,客户端将发送 PING 请求以保持会话活动。此 PING 请求不仅让 ZooKeeper 服务器知道客户端仍然活跃,还允许客户端验证其与 ZooKeeper 服务器的连接仍然处于活动状态。PING 的时机足够保守,以确保有合理的时间检测死连接并重新连接到新服务器。

一旦成功建立与服务器的连接(connected),客户端库基本上在两种情况下会生成 connectionloss(C 绑定中的结果码,Java 中的异常 -- 详情请参阅特定绑定的 API 文档):当执行同步或异步操作且满足以下任一条件时:

  1. 应用程序在不再存活/有效的会话上调用操作
  2. 当存在针对该服务器的待处理操作(即,存在待处理的异步调用)时,ZooKeeper 客户端断开与服务器的连接。

添加于 3.2.0 -- SessionMovedException。有一个内部异常,客户端通常不会看到,称为 SessionMovedException。发生此异常的原因是,在连接上接收到一个请求,而该会话已在不同的服务器上重新建立。此错误通常是由于客户端向服务器发送请求,但网络数据包延迟,导致客户端超时并连接到新服务器。当延迟的数据包到达第一个服务器时,旧服务器检测到会话已迁移,并关闭客户端连接。客户端通常看不到此错误,因为它们不会从那些旧连接读取。(旧连接通常会关闭。)可以看到此情况的一种情况是,当两个客户端尝试使用保存的会话 ID 和密码重新建立同一个连接时。其中一个客户端将重新建立连接,而第二个客户端将被断开连接(导致这对客户端无限期地尝试重新建立其连接/会话)。

更新服务器列表。我们允许客户端通过提供新的逗号分隔的 host:port 对列表(每个对应一个 ZooKeeper 服务器)来更新连接字符串。该函数会调用一个概率负载均衡算法,该算法可能导致客户端断开与当前主机的连接,以在新列表中实现每个服务器连接数的预期均匀分布。如果客户端连接的当前主机不在新列表中,则此调用总是会导致连接断开。否则,决策取决于服务器数量是否增加或减少以及减少的程度。

例如,如果之前的连接字符串包含 3 个主机,而现在列表包含这 3 个主机和另外 2 个主机,则连接到这 3 个主机中的每个主机的 40% 的客户端将迁移到其中一个新主机,以平衡负载。该算法将以 0.4 的概率导致客户端断开与当前连接的主机的连接,并在这种情况下导致客户端连接到 2 个新主机中的一个(随机选择)。

另一个例子——假设我们有 5 个主机,现在更新列表以移除其中 2 个主机,连接到剩余 3 个主机的客户端将保持连接,而连接到被移除的 2 个主机的所有客户端都需要迁移到 3 个主机中的一个(随机选择)。如果连接断开,客户端将进入一种特殊模式,在这种模式下,它使用概率算法选择要连接的新服务器,而不仅仅是轮询。

在第一个示例中,每个客户端以 0.4 的概率决定断开连接,但一旦做出决定,它将尝试连接到一个随机的新服务器,并且只有在无法连接到任何新服务器的情况下,才会尝试连接到旧服务器。在找到服务器或尝试新列表中的所有服务器均未能连接后,客户端将恢复正常操作模式,从连接字符串中选择一个任意服务器并尝试连接。如果失败,它将继续轮询尝试不同的随机服务器。(请参阅上面用于最初选择服务器的算法)

本地会话。添加于 3.5.0,主要由 ZOOKEEPER-1147 实现。

localSessionsUpgradingEnabled 被禁用时

localSessionsUpgradingEnabled 被启用时

ZooKeeper Watch 机制

ZooKeeper 中的所有读取操作 - getData()getChildren()exists() - 都可以选择设置一个 Watch 作为副作用。以下是 ZooKeeper 对 Watch 的定义:Watch 事件是一次性触发器,发送给设置 Watch 的客户端,当设置 Watch 的数据发生变化时发生。此 Watch 定义中有三个关键点需要考虑:

Watch 在客户端连接的 ZooKeeper 服务器本地维护。这使得 Watch 的设置、维护和分派都很轻量。当客户端连接到新服务器时,任何会话事件都会触发 Watch。与服务器断开连接时,不会收到 Watch。当客户端重新连接时,任何先前注册的 Watch 将被重新注册并在需要时触发。一般来说,这一切都是透明发生的。有一种情况 Watch 可能会丢失:如果在断开连接期间创建并删除了尚未创建的 ZNode,则针对该 ZNode 存在性的 Watch 将会丢失。

3.6.0 新增功能: 客户端还可以在 ZNode 上设置永久的、递归的 Watch,这些 Watch 在触发后不会被移除,并且会对注册的 ZNode 以及任何子 ZNode 的更改进行递归触发。

Watch 的语义

我们可以通过三个读取 ZooKeeper 状态的调用来设置 Watch:exists、getData 和 getChildren。以下列表详细说明了 Watch 可以触发的事件以及启用它们的调用:

持久、递归的 Watch

3.6.0 新增功能: 现在有一种对上述标准 Watch 的变体,您可以设置一个在触发后不会被移除的 Watch。此外,这些 Watch 会触发事件类型 NodeCreatedNodeDeletedNodeDataChanged,并且可以选择性地递归触发从注册 Watch 的 ZNode 开始的所有 ZNode 的这些事件。请注意,对于永久递归 Watch,不会触发 NodeChildrenChanged 事件,因为这将是冗余的。

永久 Watch 使用 addWatch() 方法设置。触发语义和保证(除了一次性触发)与标准 Watch 相同。关于事件的唯一例外是,递归永久 Watch 永远不会触发子节点更改事件,因为这将是冗余的。永久 Watch 使用 removeWatches() 方法和观察者类型 WatcherType.Any 移除。

移除 Watch

我们可以通过调用 removeWatches 移除在 ZNode 上注册的 Watch。此外,ZooKeeper 客户端甚至在没有服务器连接的情况下,通过将 local 标志设置为 true,可以在本地移除 Watch。以下列表详细说明了成功移除 Watch 后将触发的事件:

ZooKeeper 关于 Watch 的保证

关于 Watch,ZooKeeper 保证以下几点:

关于 Watch 需要记住的事项

使用 ACL 进行 ZooKeeper 访问控制

ZooKeeper 使用 ACL 来控制对其 znode(ZooKeeper 数据树的数据节点)的访问。ACL 的实现与 UNIX 文件访问权限非常相似:它使用权限位来允许/禁止对节点的各种操作以及这些位适用的范围。与标准 UNIX 权限不同,ZooKeeper 节点不受用户(文件所有者)、组和全局(其他)这三个标准范围的限制。ZooKeeper 没有 znode 所有者的概念。相反,ACL 指定了与这些 ID 相关联的 ID 集和权限。

另请注意,ACL 仅适用于特定的 znode。特别是它不适用于子节点。例如,如果只有 ip:172.16.16.1 可读 /app,而 /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 是使用 digest 方案,用户名为 bob 的用户的 ID。

当客户端连接到 ZooKeeper 并进行认证时,ZooKeeper 会将与该客户端对应的所有 ID 与客户端连接关联起来。当客户端尝试访问节点时,会根据 znode 的 ACL 检查这些 ID。ACL 由 (scheme:expression, perms) 对组成。expression 的格式特定于方案。例如,对 (ip:19.22.0.0/16, READ) 赋予 IP 地址以 19.22 开头的任何客户端 READ 权限。

ACL 权限

ZooKeeper 支持以下权限:

CREATEDELETE 权限已从 WRITE 权限中分离出来,以实现更细粒度的访问控制。CREATEDELETE 的用例如下:

您希望用户 A 能够对 ZooKeeper 节点执行 set 操作,但不能 CREATEDELETE 子节点。

CREATE 但没有 DELETE:客户端通过在父目录中创建 ZooKeeper 节点来创建请求。您希望所有客户端都能添加,但只有请求处理器可以删除。(这有点像文件的 APPEND 权限。)

此外,由于 ZooKeeper 没有文件所有者的概念,因此存在 ADMIN 权限。在某种意义上,ADMIN 权限指定了实体为所有者。ZooKeeper 不支持 LOOKUP 权限(目录上的执行权限位,即使您无法列出目录,也允许您进行 LOOKUP)。每个人都隐式拥有 LOOKUP 权限。这允许您 stat 一个节点,但仅此而已。(问题在于,如果您想对一个不存在的节点调用 zoo_exists(),则没有权限可检查。)

ADMIN 权限在 ACL 方面也扮演着特殊角色:为了检索 znode 的 ACL,用户必须拥有 READADMIN 权限,但没有 ADMIN 权限,digest 哈希值将被屏蔽。

内置 ACL 方案

ZooKeeper 有以下内置方案:

ZooKeeper C 客户端 API

ZooKeeper C 库提供了以下常量:

以下是标准 ACL ID:

ZOO_AUTH_IDS 的空身份字符串应被解释为“创建者的身份”。

ZooKeeper 客户端提供三种标准 ACL:

ZOO_OPEN_ACL_UNSAFE 是一个完全开放的自由访问 ACL:任何应用程序都可以对节点执行任何操作,并可以创建、列出和删除其子节点。ZOO_READ_ACL_UNSAFE 是对任何应用程序的只读访问。CREATE_ALL_ACL 授予节点创建者所有权限。创建者在使用此 ACL 创建节点之前,必须已由服务器进行认证(例如,使用“digest”方案)。

以下 ZooKeeper 操作涉及 ACL:

应用程序使用 zoo_add_auth 函数向服务器进行认证。如果应用程序想使用不同的方案和/或身份进行认证,可以多次调用此函数。

zoo_create(...) 操作创建一个新节点。acl 参数是与节点关联的 ACL 列表。父节点必须设置 CREATE 权限位。

此操作返回节点的 ACL 信息。节点必须设置 READ 或 ADMIN 权限。如果没有 ADMIN 权限,digest 哈希值将被屏蔽。

此函数将节点的 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> 对。idspec 可以是对与连接关联的认证信息进行简单的字符串匹配,也可以是针对该信息进行评估的表达式。由认证插件的实现来完成匹配。以下是认证插件必须实现的接口:

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 时调用 matches(String id, String aclExpr)。它需要将客户端的认证信息与相关的 ACL 条目进行匹配。为了找到适用于客户端的条目,ZooKeeper 服务器会找到每个条目的方案,并且如果客户端针对该方案有认证信息,则会调用 matches(String id, String aclExpr),其中 id 设置为之前通过 handleAuthentication 添加到连接的认证信息,而 aclExpr 设置为 ACL 条目的 id。认证插件使用自己的逻辑和匹配方案来确定 id 是否包含在 aclExpr 中。

有两个内置的认证插件:ipdigest。可以使用系统属性添加额外的插件。在启动时,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);
}

您无需实现 AuthenticationProvider 接口,而是扩展 ServerAuthenticationProvider。然后您的 handleAuthentication() 和 matches() 方法将接收额外的参数(通过 ServerObjs 和 MatchValues)。

一致性保证

ZooKeeper 是一个高性能、可伸缩的服务。读写操作都被设计得很快,尽管读操作比写操作更快。原因在于,对于读操作,ZooKeeper 可以提供旧数据,这反过来是由于 ZooKeeper 的一致性保证:

利用这些一致性保证,可以很容易地仅在 ZooKeeper 客户端构建更高层的功能,例如 leader 选举、屏障、队列和可撤销读写锁(无需向 ZooKeeper 添加任何内容)。更多详情请参阅 Recipes and Solutions

注意

有时,开发者会错误地假定 ZooKeeper 实际上并 提供另一项保证。即:*跨客户端的同时一致视图 (Simultaneously Consistent Cross-Client Views)*:ZooKeeper 不保证在任何时刻,两个不同的客户端都会看到 ZooKeeper 数据的完全相同的视图。由于网络延迟等因素,一个客户端可能在另一个客户端收到变更通知之前完成更新。考虑客户端 A 和 B 的场景。如果客户端 A 将 znode /a 的值从 0 设置为 1,然后告诉客户端 B 读取 /a,客户端 B 可能会读取旧值 0,具体取决于它连接到哪个服务器。如果客户端 A 和客户端 B 读取相同的值很重要,则客户端 B 应在执行读取之前调用 ZooKeeper API 方法中的 sync() 方法。因此,ZooKeeper 本身不保证变更在所有服务器上同步发生,但可以使用 ZooKeeper 原语来构建提供有用的客户端同步的更高层功能。(更多信息,请参阅 ZooKeeper Recipes。)

绑定

ZooKeeper 客户端库有两种语言版本:Java 和 C。以下部分对此进行描述。

Java 绑定

组成 ZooKeeper Java 绑定的有两个包:org.apache.zookeeperorg.apache.zookeeper.data。组成 ZooKeeper 的其余包要么是内部使用,要么是服务器实现的一部分。org.apache.zookeeper.data 包由生成的类组成,仅用作容器。

ZooKeeper Java 客户端使用的主要类是 ZooKeeper 类。它的两个构造函数仅在可选的会话 ID 和密码上有所不同。ZooKeeper 支持跨进程实例的会话恢复。Java 程序可以将其会话 ID 和密码保存到持久存储,重新启动,并恢复程序先前实例使用的会话。

创建 ZooKeeper 对象时,也会创建两个线程:一个 IO 线程和一个事件线程。所有 IO 都发生在 IO 线程上(使用 Java NIO)。所有事件回调都发生在事件线程上。会话维护(例如重新连接到 ZooKeeper 服务器和维护心跳)在 IO 线程上完成。同步方法的响应也在 IO 线程中处理。所有异步方法的响应和监听事件都在事件线程上处理。由这种设计产生的一些注意事项如下:

最后,与关闭相关的规则非常简单:一旦 ZooKeeper 对象关闭或收到致命事件(SESSION_EXPIRED 和 AUTH_FAILED),ZooKeeper 对象将变为无效。在关闭时,两个线程会关闭,对 zookeeper 句柄的任何进一步访问都是未定义行为,应避免。

客户端配置参数

以下列表包含 Java 客户端的配置属性。您可以使用 Java 系统属性设置其中任何属性。有关服务器属性,请查看 Admin Guide 的服务器配置部分。ZooKeeper Wiki 上也有关于 ZooKeeper SSL 支持ZooKeeper 的 SASL 认证 的有用页面。

C 绑定

C 绑定有一个单线程库和一个多线程库。多线程库最易于使用,并且与 Java API 最相似。该库将创建一个 IO 线程和一个事件分发线程用于处理连接维护和回调。单线程库通过暴露多线程库中使用的事件循环,允许在事件驱动应用程序中使用 ZooKeeper。

该包包含两个共享库:zookeeper_st 和 zookeeper_mt。前者仅提供异步 API 和回调,用于集成到应用程序的事件循环中。该库存在的唯一原因是支持没有 pthread 库或 pthread 库不稳定的平台(即 FreeBSD 4.x)。在所有其他情况下,应用程序开发者应链接 zookeeper_mt 库,因为它支持同步和异步 API。

安装

如果您正在从 Apache 仓库的检出构建客户端,请按照以下步骤操作。如果您正在从 Apache 下载的项目源代码包构建,请跳到步骤 3

  1. 在 zookeeper-jute 目录 (.../trunk/zookeeper-jute) 中运行 mvn compile。这将在 .../trunk/zookeeper-client/zookeeper-client-c 下创建一个名为 "generated" 的目录。
  2. 将目录更改到 .../trunk/zookeeper-client/zookeeper-client-c 并运行 autoreconf -if 以引导 autoconfautomakelibtool。确保您安装了 autoconf 版本 2.59 或更高版本。跳到步骤 4
  3. 如果您正在从项目源代码包构建,请解压/解包源代码压缩包并进入 zookeeper-x.x.x/zookeeper-client/zookeeper-client-c 目录。
  4. 运行 ./configure <your-options> 生成 makefile。以下是 configure 工具在此步骤中可能有用的一些选项:
注意

有关运行 configure 的一般信息,请参阅 INSTALL。1. 运行 makemake install 来构建库并安装它们。2. 要为 ZooKeeper API 生成 doxygen 文档,运行 make doxygen-doc。所有文档都将放在一个名为 docs 的新子文件夹中。默认情况下,此命令只生成 HTML。有关其他文档格式的信息,请运行 ./configure --help

构建自己的 C 客户端

为了能够在应用程序中使用 ZooKeeper C API,您必须记住:

  1. 包含 ZooKeeper 头文件:#include <zookeeper/zookeeper.h>
  2. 如果您正在构建多线程客户端,请使用 -DTHREADED 编译器标志进行编译以启用库的多线程版本,然后链接 zookeeper_mt 库。如果您正在构建单线程客户端,请不要使用 -DTHREADED 进行编译,并确保链接 zookeeper_st 库。
注意

有关 C 客户端实现的示例,请参阅 .../trunk/zookeeper-client/zookeeper-client-c/src/cli.c

构建块:ZooKeeper 操作指南

本节概述了开发者可以对 ZooKeeper 服务器执行的所有操作。这些信息比本手册前面概念章节中的信息级别低,但比 ZooKeeper API 参考级别高。它涵盖以下主题:

错误处理

Java 和 C 客户端绑定都可能报告错误。Java 客户端绑定通过抛出 KeeperException 来实现,调用异常上的 code() 方法将返回特定的错误码。C 客户端绑定返回 enum 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);

我们将演示一个客户端,该客户端在成功连接后输出“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 用户容易陷入的陷阱:

  1. 如果您正在使用监听,则必须查找连接成功的监听事件。当 ZooKeeper 客户端与服务器断开连接时,在重新连接之前您将不会收到变更通知。如果您正在监听某个 znode 的出现,则如果在您断开连接期间该 znode 被创建并删除,您将错过该事件。
  2. 您必须测试 ZooKeeper 服务器故障。只要大多数服务器处于活动状态,ZooKeeper 服务就能在故障中幸存下来。需要问的问题是:您的应用程序能处理它吗?在现实世界中,客户端与 ZooKeeper 的连接可能会断开。(ZooKeeper 服务器故障和网络分区是连接丢失的常见原因。)ZooKeeper 客户端库会负责恢复您的连接并告知您发生了什么,但您必须确保恢复您的状态以及任何失败的未完成请求。请在测试环境中确认您做得对,而不是在生产环境中——使用由多个服务器组成的 ZooKeeper 服务并对其进行重启测试。
  3. 客户端使用的 ZooKeeper 服务器列表必须与每个 ZooKeeper 服务器拥有的 ZooKeeper 服务器列表相匹配。如果客户端列表是 ZooKeeper 服务器实际列表的子集,系统可以工作,尽管不是最优的;但如果客户端列出的 ZooKeeper 服务器不在 ZooKeeper 集群中,则不能工作。
  4. 小心存放事务日志的位置。ZooKeeper 中对性能最关键的部分是事务日志。在返回响应之前,ZooKeeper 必须将事务同步到介质。使用专用的事务日志设备是保证良好性能的关键。将日志放在繁忙的设备上会对性能产生不利影响。如果只有一个存储设备,可以将跟踪文件放在 NFS 上并增加 snapshotCount;这不能完全解决问题,但可以缓解它。
  5. 正确设置你的 Java 最大堆大小。非常重要的是要 避免交换(swapping)。不必要地读写磁盘几乎肯定会使你的性能下降到无法接受的地步。记住,在 ZooKeeper 中,所有东西都是有序的,所以如果一个请求访问了磁盘,所有其他排队的请求都会访问磁盘。为了避免交换,尝试将堆大小设置为你拥有的物理内存量减去操作系统和缓存所需的内存量。确定最佳堆大小的最佳方法是 运行负载测试。如果由于某种原因不能进行负载测试,请保守估计并选择一个远低于会导致机器交换的限制的数值。例如,在 4G 内存的机器上,3G 的堆大小是一个保守的起始估计。

其他信息链接

除了正式文档之外,还有其他几个 ZooKeeper 开发者可以获取信息的来源。