Apache > ZooKeeper
 

ZooKeeper 程序员指南

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

介绍

本文档是为希望创建利用 ZooKeeper 协调服务的分布式应用程序的开发人员提供的指南。它包含概念和实践信息。

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

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

本书最后附有一个附录,其中包含指向其他有用的 ZooKeeper 相关信息的链接。

本文档中的大部分信息都可以作为独立的参考资料访问。但是,在开始您的第一个 ZooKeeper 应用程序之前,您可能至少应该阅读有关ZooKeeper 数据模型ZooKeeper 基本操作的章节。

ZooKeeper 数据模型

ZooKeeper 有一个分层命名空间,很像分布式文件系统。唯一的区别是命名空间中的每个节点都可以有与其关联的数据以及子节点。这就像拥有一个允许文件也成为目录的文件系统。节点的路径始终表示为规范的、绝对的、斜线分隔的路径;没有相对参考。任何 unicode 字符都可以在受以下约束的路径中使用:

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(零)填充(计数器以这种方式格式化以简化排序),即“0000000001"。有关此功能的使用示例,请参见队列配方。注意:用于存储下一个序列号的计数器是由父节点维护的有符号整数(4 字节),当递增超过 2147483647 时,计数器将溢出(导致姓名 ”-2147483648")。

容器节点

在 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 以多种方式跟踪时间:

ZooKeeper 统计结构

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

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 的人)。此时过期会话的客户端仍然与集群断开连接,直到/除非它能够重新建立与集群的连接,它才会被通知会话过期。

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

  1. 'connected' :会话已建立并且客户端正在与集群通信(客户端/服务器通信正常运行)
  2. ....客户端从集群中分区
  3. 'disconnected' : 客户端已失去与集群的连接
  4. ....时间过去了,在“超时”期限之后,集群使会话过期,客户端没有看到任何内容,因为它与集群断开连接
  5. .... 时间过去了,客户端重新获得与集群的网络级连接
  6. 'expired' :最终客户端重新连接到集群,然后通知它过期

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

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

一旦成功建立(连接)到服务器的连接,基本上有两种情况,客户端库在同步或执行异步操作,并满足以下条件之一:

  1. 应用程序在不再活动/有效的会话上调用操作
  2. 当服务器有挂起的操作时,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实现。

localSessionsUpgradingEnabled禁用时:

localSessionsUpgradingEnabled启用时:

ZooKeeper 手表

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

监视在客户端连接到的 ZooKeeper 服务器上本地维护。这允许手表在设置、维护和调度方面是轻量级的。当客户端连接到新服务器时,将针对任何会话事件触发监视。与服务器断开连接时将不会收到手表。当客户端重新连接时,任何以前注册的手表都将重新注册并在需要时触发。一般来说,这一切都是透明地发生的。有一种情况可能会丢失监视:如果在断开连接时创建和删除 znode,则会丢失尚未创建的 znode 存在的监视。

3.6.0 中的新功能:客户端还可以在 znode 上设置永久的递归监​​视,这些监视在触发时不会被删除,并且会以递归方式触发已注册 znode 以及任何子 znode 上的更改。

手表的语义

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

持久的、递归的手表

3.6.0 中的新功能:上述标准手表现在有一个变体,您可以设置一个在触发时不会被移除的手表。此外,这些监视触发事件类型NodeCreatedNodeDeletedNodeDataChanged ,并且可选地,对于从监视注册的 znode 开始的所有 znode 递归。请注意,持久递归监视不会触发NodeChildrenChanged事件,因为它是多余的。

使用方法addWatch()设置持久手表。触发语义和保证(一次性触发除外)与标准手表相同。关于事件的唯一例外是递归持久观察者永远不会触发子更改事件,因为它们是多余的。使用具有观察者类型WatcherType.Any的removeWatches()删除持久观察。

删除手表

我们可以通过调用 removeWatches 来删除在 znode 上注册的手表。此外,即使没有服务器连接,ZooKeeper 客户端也可以通过将本地标志设置为 true 来在本地删除手表。以下列表详细说明了成功移除手表后将触发的事件。

ZooKeeper 对手表的保证

关于手表,ZooKeeper 维护以下保证:

关于手表要记住的事情

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 支持以下权限:

CREATEDELETE权限已从WRITE权限中分离出来,以实现更精细的访问控制。CREATEDELETE的情况如下:

您希望 A 能够在 ZooKeeper 节点上进行设置,但不能创建删除子节点。

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

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

ADMIN权限在 ACL 方面也有特殊作用:为了检索 znode 用户的 ACL,必须具有READADMIN权限,但如果没有ADMIN权限,摘要哈希值将被屏蔽掉。

内置 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 将所有权限授予节点的创建者。创建者必须通过服务器的身份验证(例如,使用“ digest ”方案)才能使用此 ACL 创建节点。

以下 ZooKeeper 操作处理 ACL:

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

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

此操作返回节点的 ACL 信息。该节点必须具有 READ 或 ADMIN 权限集。如果没有 ADMIN 权限,摘要哈希值将被屏蔽掉。

此函数将节点的 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中。

有两个内置的身份验证插件: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);
}

您扩展了 ServerAuthenticationProvider,而不是实现 AuthenticationProvider。然后,您的 handleAuthentication() 和 matches() 方法将接收附加参数(通过 ServerObjs 和 MatchValues)。

一致性保证

ZooKeeper 是一种高性能、可扩展的服务。读取和写入操作都被设计为快速,尽管读取比写入快。这样做的原因是,在读取的情况下,ZooKeeper 可以服务较旧的数据,这又是由于 ZooKeeper 的一致性保证:

使用这些一致性保证可以很容易地在 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.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 系统属性设置任何这些属性。有关服务器属性,请查看管理员指南的服务器配置部分。ZooKeeper Wiki 也有关于ZooKeeper SSL 支持ZooKeeper 的 SASL 身份验证的有用页面。

C 绑定

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

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

安装

如果您从 Apache 存储库中签出来构建客户端,请按照下面概述的步骤进行操作。如果您是从从 apache 下载的项目源包构建,请跳到第3步。

  1. ant compile_jute从 ZooKeeper 顶级目录 ( .../trunk )运行。这将在.../trunk/zookeeper-client/zookeeper-client-c下创建一个名为“generated”的目录。
  2. 将目录更改为 *.../trunk/zookeeper-client/zookeeper-client-c* 并运行autoreconf -if以引导autoconfautomakelibtool。确保您已安装autoconf 版本 2.59或更高版本。跳到步骤4
  3. 如果您是从项目源代码包构建,请将源 tarball 和 cd 解压缩/解压缩到* zookeeper-xxx/zookeeper-client/zookeeper-client-c* 目录。
  4. 运行./configure <your-options>以生成生成文件。以下是配置实用程序支持的一些在此步骤中有用的选项:
笔记

有关运行configure的一般信息,请参阅安装。1. 运行makemake install构建库并安装它们。1. 要为 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,并确保链接到 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);

我们将演示客户端在成功连接后输出“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 必须在返回响应之前将事务同步到媒体。专用事务日志设备是始终如一的良好性能的关键。将日志放在繁忙的设备上会对性能产生不利影响。如果你只有一个存储设备,把trace文件放到NFS上,增加snapshotCount;它不能消除问题,但可以减轻它。
  5. 正确设置 Java 最大堆大小。避免交换非常重要。不必要地访问磁盘几乎肯定会使您的性能下降到无法接受的程度。请记住,在 ZooKeeper 中,一切都是有序的,因此如果一个请求命中磁盘,则所有其他排队的请求都会命中磁盘。为避免交换,请尝试将堆大小设置为您拥有的物理内存量减去操作系统和缓存所需的量。为您的配置确定最佳堆大小的最佳方法是运行负载测试。如果由于某种原因您不能,请保守估计并选择一个远低于可能导致您的机器交换的限制的数字。例如,在 4G 机器上,3G 堆是开始时的保守估计。

其他信息的链接

在正式文档之外,ZooKeeper 开发人员还有其他几个信息来源。