Apache > ZooKeeper
 

ZooKeeper 开发者指南

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

简介

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

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

接下来的四个部分提供了实用的编程信息。这些内容包括:

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

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

ZooKeeper 数据模型

ZooKeeper拥有一个层次化的命名空间,类似于分布式文件系统。唯一的区别在于命名空间中的每个节点既可以关联数据,也可以拥有子节点。这就像允许文件同时作为目录的文件系统。节点的路径始终以规范的、绝对的正斜杠分隔路径表示;不存在相对引用。路径中可以使用任何Unicode字符,但需遵守以下限制:

ZNodes节点

ZooKeeper树中的每个节点都被称为znode。Znode维护一个状态结构,其中包含数据变更的版本号、ACL变更等信息。该状态结构还包含时间戳。版本号与时间戳的结合使ZooKeeper能够验证缓存并协调更新。每当znode的数据发生变化时,版本号就会递增。例如,每当客户端检索数据时,它也会接收到数据的版本号。当客户端执行更新或删除操作时,必须提供所更改znode的数据版本号。如果提供的版本号与数据的实际版本不匹配,更新将失败。(此行为可以被覆盖。

注意

在分布式应用工程中,节点一词可以指代通用主机、服务器、集群成员、客户端进程等。在ZooKeeper文档中,znodes特指数据节点。服务器指构成ZooKeeper服务的机器;法定节点指组成集群的服务器;客户端指任何使用ZooKeeper服务的主机或进程。

Znodes是程序员访问的主要实体。它们有几个值得在此提及的特性。

监视器

客户端可以在znode上设置监视器。对该znode的更改会触发监视器并随后清除监视。当监视器触发时,ZooKeeper会向客户端发送通知。更多关于监视器的信息可以在章节ZooKeeper Watches中找到。

数据访问

命名空间中每个znode节点存储的数据都是原子性读写。读取操作会获取与znode关联的所有数据字节,而写入操作则会替换全部数据。每个节点都有一个访问控制列表(ACL),用于限制不同用户的操作权限。

ZooKeeper并非设计用作通用数据库或大型对象存储。相反,它用于管理协调数据。这些数据可能以配置、状态信息、会合点等形式存在。各类协调数据的共同特点是数据量相对较小:以千字节为单位。ZooKeeper客户端和服务端实现都设有完整性检查,确保znode数据不超过1M,但实际数据量通常应远小于此阈值。操作较大数据量会导致某些操作耗时显著增加,并影响部分操作的延迟,因为需要额外时间通过网络传输更多数据并写入存储介质。如需存储大型数据,常规处理模式是将数据存储在批量存储系统(如NFS或HDFS)中,而在ZooKeeper中仅存储指向这些存储位置的指针。

临时节点

ZooKeeper还支持临时节点的概念。这些znode仅在创建它们的会话处于活动状态时存在。当会话结束时,znode会被自动删除。由于这种特性,临时znode不允许拥有子节点。可以通过getEphemerals()接口获取当前会话的所有临时节点列表。

getEphemerals()

获取给定路径下由会话创建的临时节点列表。如果路径为空,将列出该会话的所有临时节点。使用场景 - 一个典型用例可能是:当需要收集会话的临时节点列表以进行重复数据项检查,且这些节点是按顺序创建导致无法预知节点名称时。在这种情况下,可以使用getEphemerals()接口来获取该会话的节点列表。这可能是服务发现的典型应用场景。

序列节点 -- 唯一命名

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

容器节点

3.5.3版本新增

ZooKeeper引入了容器znode的概念。容器znode是特殊用途的znode,适用于领导者选举、锁等场景。当容器的最后一个子节点被删除时,该容器将成为服务器在未来某个时间点删除的候选对象。

基于这一特性,在容器znode内创建子节点时,您应准备好处理KeeperException.NoNodeException异常。也就是说,当在容器znode内部创建子znode时,始终要检查KeeperException.NoNodeException,并在出现该异常时重新创建容器znode。

TTL节点

3.5.3版本新增

在创建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客户端可能的状态转换:

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会创建一个表示为64位数字的会话,并将其分配给客户端。如果客户端连接到不同的ZooKeeper服务器,它会在连接握手过程中发送会话ID。作为安全措施,服务器会为会话ID创建一个密码,任何ZooKeeper服务器都可以验证该密码。当客户端建立会话时,密码会随会话ID一起发送给客户端。每当客户端与新服务器重新建立会话时,都会同时发送这个密码和会话ID。

ZooKeeper客户端库调用创建ZooKeeper会话的参数之一是会话超时时间(以毫秒为单位)。客户端发送请求的超时时间,服务器则返回它能为客户端提供的超时时间。当前实现要求超时时间至少为tickTime的2倍(在服务器配置中设置),最多为tickTime的20倍。ZooKeeper客户端API允许访问协商后的超时时间。

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

会话过期由ZooKeeper集群自身管理,而非客户端控制。当ZK客户端与集群建立会话时,它会提供上述详述的"timeout"值。集群利用该值判定客户端会话何时过期。若集群在指定的会话超时期限内未收到客户端通信(即无心跳),则触发会话过期。会话过期时,集群将删除该会话拥有的所有临时节点,并立即向所有连接的客户端通知此变更(任何监视这些znode的客户端)。此时,已过期会话的客户端仍与集群断开连接,除非/直到它能重新建立集群连接,否则不会收到会话过期通知。客户端将保持断开状态,直至与集群重新建立TCP连接,届时过期会话的监视器将收到"会话过期"通知。

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

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

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

客户端通过发送请求来保持会话活跃。如果会话空闲时间过长导致可能超时,客户端将发送PING请求以维持会话。该PING请求不仅能让ZooKeeper服务器知晓客户端仍处于活跃状态,同时也让客户端能够验证其与ZooKeeper服务器的连接是否仍然有效。PING的发送时机经过保守计算,确保有足够时间检测死连接并重新连接到新服务器。

一旦成功建立与服务器的连接(已连接),在以下两种情况下,当执行同步或异步操作时,客户端库会生成连接丢失(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的概率决定断开连接,但一旦做出决定,它将尝试连接到一个随机的新服务器,只有在无法连接到任何新服务器时才会尝试连接旧服务器。在找到服务器,或尝试了新列表中的所有服务器但连接失败后,客户端将返回正常操作模式,从connectString中任意选择一个服务器并尝试连接。如果连接失败,将继续以轮询方式尝试不同的随机服务器。(参见上文初始选择服务器时使用的算法)

本地会话。在3.5.0版本中新增,主要通过ZOOKEEPER-1147实现。

localSessionsUpgradingEnabled 被禁用时:

localSessionsUpgradingEnabled启用时:

ZooKeeper 监视器

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

监视器(Watches)在客户端连接的ZooKeeper服务器本地维护。这使得监视器的设置、维护和分发变得轻量级。当客户端连接到新服务器时,任何会话事件都会触发监视器。在与服务器断开连接期间不会接收到监视通知。当客户端重新连接时,所有先前注册的监视器将重新注册并在需要时触发。通常情况下,这一切都是透明发生的。存在一种可能丢失监视通知的情况:如果在断开连接期间创建并删除了某个尚未创建的znode,那么对该znode存在性的监视通知将会丢失。

3.6.0 新特性: 客户端现在可以为 znode 设置永久性递归监视器,这些监视器在触发后不会被移除,并且会递归触发注册 znode 及其所有子节点上的变更通知。

监视器的语义

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

持久化递归监视器

3.6.0版本新增功能: 现在对上述标准监视机制进行了扩展,您可以设置一种触发后不会被移除的持久监视。此外,这些监视会触发节点创建节点删除节点数据变更事件类型,并可选择性地从注册监视的znode开始递归监视所有子节点。请注意,持久递归监视不会触发子节点变更事件,因为这会导致重复通知。

持久性监视器通过addWatch()方法设置。其触发语义和保证机制(除一次性触发外)与标准监视器相同。关于事件的唯一例外是:递归持久性监视器永远不会触发子节点变更事件,因为这些事件是冗余的。持久性监视器可通过removeWatches()方法配合WatcherType.Any监视器类型来移除。

移除监视器

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

ZooKeeper关于监视器的保证

关于监视器,ZooKeeper 提供以下保证:

关于监视器的注意事项

ZooKeeper访问控制使用ACLs

ZooKeeper使用ACL(访问控制列表)来控制对其znodes(ZooKeeper数据树的数据节点)的访问。ACL的实现与UNIX文件访问权限非常相似:它采用权限位来允许/禁止针对节点的各种操作以及这些权限位适用的范围。与标准UNIX权限不同,ZooKeeper节点不受用户(文件所有者)、组和其他(全局)这三个标准范围的限制。ZooKeeper没有znode所有者的概念,而是通过ACL指定与这些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方案的主机ID,地址为172.16.16.1;而digest:bob:password是使用digest方案的用户ID,用户名为bob

当客户端连接到ZooKeeper并进行身份验证时,ZooKeeper会将与该客户端对应的所有ID与其连接关联起来。当客户端尝试访问节点时,这些ID会与znode的ACL进行比对。ACL由(scheme:expression, perms)这样的配对组成。expression的格式取决于具体的scheme。例如,配对(ip:19.22.0.0/16, READ)表示授予所有IP地址以19.22开头的客户端READ读取权限。

ACL权限

ZooKeeper支持以下权限:

为了更细粒度的访问控制,CREATE(创建)和DELETE(删除)权限已从WRITE(写入)权限中分离出来。CREATEDELETE的适用场景如下:

您希望A能够对ZooKeeper节点执行set操作,但不能创建删除子节点。

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

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

ADMIN 权限在ACL方面还有一个特殊作用:用户必须拥有READADMIN权限才能获取znode的ACL信息,但如果没有ADMIN权限,摘要哈希值将被屏蔽显示。

As of versions 3.9.2 / 3.8.4 / 3.7.3 the exists() call will now verify ACLs on nodes that exist and the client must have READ permission otherwise 'Insufficient permission' error will be raised.

内置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操作涉及ACLs:

应用程序使用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>对。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服务器使用认证插件返回的scheme来确定该方案适用于哪些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服务器会查找每个条目的方案(scheme),如果该客户端具有该方案的认证信息,则会调用matches(String id, String aclExpr)方法,其中id参数设置为先前通过handleAuthentication添加到连接的认证信息,aclExpr参数设置为ACL条目的ID。认证插件使用其自身的逻辑和匹配方案来确定id是否包含在aclExpr中。

ZooKeeper内置了两种认证插件: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客户端构建更高级别的功能,如领导者选举、屏障、队列和可撤销的读写锁(无需对ZooKeeper进行任何添加)。详情请参阅Recipes and Solutions

注意

有时开发者会错误地假设ZooKeeper实际上并不提供的另一个保证。这就是:*跨客户端视图的即时一致性*: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系统属性设置其中任何属性。有关服务器属性,请查阅管理员指南中的服务器配置部分。ZooKeeper维基还提供了关于ZooKeeper SSL支持ZooKeeper的SASL认证的有用页面。

C语言绑定

C语言绑定提供了单线程和多线程两种库。多线程库使用起来最简单,与Java API最为相似。该库会创建一个IO线程和一个事件分发线程,用于处理连接维护和回调。单线程库通过暴露多线程库中使用的事件循环,允许在事件驱动应用中使用ZooKeeper。

该软件包包含两个共享库:zookeeper_st和zookeeper_mt。前者仅提供异步API和回调功能,用于集成到应用程序的事件循环中。这个库存在的唯一原因是支持那些库不可用或不稳定的平台(例如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 生成 makefile。以下是 configure 工具在此步骤中支持的一些可能有用的选项:
注意

See INSTALL for general information about running configure. 1. Run make or make install to build the libraries and install them. 1. To generate doxygen documentation for the ZooKeeper API, run make doxygen-doc. All documentation will be placed in a new subfolder named docs. By default, this command only generates HTML. For information on other document formats, run ./configure --help

构建您自己的C语言客户端

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

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

查看.../trunk/zookeeper-client/zookeeper-client-c/src/cli.c获取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);

我们将演示一个客户端,在成功连接后输出"已连接到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. 如果正在使用监视器(watches),必须关注连接监视事件。当ZooKeeper客户端与服务器断开连接时,在重新连接之前将不会收到变更通知。如果正在监视某个znode的创建事件,在断开连接期间若该znode被创建后又删除,将会错过这个事件。
  2. 你必须测试ZooKeeper服务器故障。只要大多数服务器处于活动状态,ZooKeeper服务就能在故障中存活。关键问题是:你的应用程序能处理这种情况吗?在现实世界中,客户端与ZooKeeper的连接可能会中断(ZooKeeper服务器故障和网络分区是连接丢失的常见原因)。ZooKeeper客户端库会负责恢复连接并通知你发生的情况,但你必须确保能恢复状态和任何失败的未完成请求。务必在测试实验室验证是否正确处理,而不是在生产环境中——使用由多台服务器组成的ZooKeeper服务进行测试,并对这些服务器进行重启测试。
  3. 客户端使用的ZooKeeper服务器列表必须与每个ZooKeeper服务器自身维护的服务器列表相匹配。如果客户端列表是实际ZooKeeper服务器列表的子集,系统仍可运行(尽管不是最优状态);但如果客户端列出了不属于ZooKeeper集群的服务器,则无法正常工作。
  4. 请注意事务日志的存放位置。ZooKeeper性能最关键的部分就是事务日志。ZooKeeper必须在返回响应前将事务同步到存储介质。使用专用的事务日志设备是保持稳定高性能的关键。如果将日志存放在繁忙的设备上,将会严重影响性能。如果只有单一存储设备,可以将跟踪文件放在NFS上并增加snapshotCount值;这虽然不能彻底解决问题,但可以缓解影响。
  5. 正确设置Java最大堆内存大小。非常重要的一点是避免内存交换。不必要的磁盘操作几乎必然会导致性能下降到不可接受的程度。请记住,在ZooKeeper中所有操作都是有序的,因此如果一个请求触发了磁盘操作,所有其他排队中的请求都会触发磁盘操作。为防止内存交换,建议将堆内存设置为物理内存总量减去操作系统和缓存所需的内存。确定最优堆内存大小的最佳方式是对您的配置进行负载测试。如果由于某些原因无法进行测试,建议采用保守估计,选择一个远低于会导致机器发生内存交换的临界值。例如,在4G内存的机器上,3G的堆内存设置就是一个保守的初始估计值。

其他信息链接

除了正式文档外,ZooKeeper开发者还可以通过其他多种渠道获取信息。