ZooKeeper 开发者指南
开发使用ZooKeeper的分布式应用程序
- 简介
- ZooKeeper 数据模型
- ZooKeeper 会话
- ZooKeeper 监视器
- ZooKeeper访问控制使用ACLs
- 可插拔的ZooKeeper认证
- 一致性保证
- 绑定
- 构建模块:ZooKeeper操作指南
- 常见陷阱:问题排查与解决方案
简介
本文档是为希望利用ZooKeeper协调服务开发分布式应用程序的开发者提供的指南。它包含概念性和实用性的信息。
本指南的前四部分对ZooKeeper的各个概念进行了更高层次的讨论。这些内容对于理解ZooKeeper的工作原理以及如何使用它都是必要的。这部分不包含源代码,但假设读者熟悉分布式计算相关的问题。第一组的内容包括:
接下来的四个部分提供了实用的编程信息。这些内容包括:
本书最后附有一个附录,包含其他与ZooKeeper相关的实用信息链接。
本文档中的大部分信息都可以作为独立的参考资料使用。不过,在开始开发您的第一个ZooKeeper应用程序之前,您至少应该阅读关于ZooKeeper数据模型和ZooKeeper基本操作的章节。
ZooKeeper 数据模型
ZooKeeper拥有一个层次化的命名空间,类似于分布式文件系统。唯一的区别在于命名空间中的每个节点既可以关联数据,也可以拥有子节点。这就像允许文件同时作为目录的文件系统。节点的路径始终以规范的、绝对的正斜杠分隔路径表示;不存在相对引用。路径中可以使用任何Unicode字符,但需遵守以下限制:
- 路径名中不能包含空字符(\u0000)。(这会导致C绑定出现问题。)
- 以下字符不能使用,因为它们显示效果不佳或呈现方式令人困惑:\u0001 - \u001F 和 \u007F
- .
- 以下字符不允许使用:\ud800 - uF8FF, \uFFF0 - uFFFF。
- 字符"."可以作为其他名称的一部分使用,但单独使用"."和".."不能表示路径中的节点,因为ZooKeeper不使用相对路径。以下路径是无效的:"/a/b/./c" 或 "/a/b/../c"。
- 令牌 "zookeeper" 是保留字。
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填充(采用这种格式是为了简化排序),例如"
容器节点
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通过多种方式跟踪时间:
- Zxid 对ZooKeeper状态的每次更改都会以zxid(ZooKeeper事务ID)的形式获得一个时间戳。这展示了ZooKeeper所有变更的全局顺序。每个变更都会有一个唯一的zxid,如果zxid1小于zxid2,则表示zxid1发生在zxid2之前。
- 版本号 对节点的每次更改都会导致该节点的一个版本号增加。这三个版本号分别是:version(对znode数据内容的更改次数)、cversion(对znode子节点列表的更改次数)和aversion(对znode ACL权限的更改次数)。
- Ticks 在使用多服务器ZooKeeper时,服务器使用ticks来定义事件的时间安排,例如状态上传、会话超时、对等节点之间的连接超时等。tick时间仅通过最小会话超时(tick时间的2倍)间接暴露;如果客户端请求的会话超时小于最小会话超时,服务器会告知客户端实际会话超时是最小会话超时。
- 实时性 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是临时节点,则表示其所有者的会话ID。如果不是临时节点,则该值为零。
- dataLength 该znode数据字段的长度。
- numChildren 该znode的子节点数量。
ZooKeeper 会话
ZooKeeper客户端通过使用语言绑定创建服务句柄来与ZooKeeper服务建立会话。创建后,句柄初始处于CONNECTING状态,客户端库会尝试连接到组成ZooKeeper服务的某个服务器,此时将切换到CONNECTED状态。在正常操作期间,客户端句柄将处于这两种状态之一。如果发生不可恢复的错误(例如会话过期或身份验证失败),或者应用程序显式关闭句柄,句柄将转为CLOSED状态。下图展示了ZooKeeper客户端可能的状态转换:

要创建客户端会话,应用程序代码必须提供一个连接字符串,其中包含以逗号分隔的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连接,届时过期会话的监视器将收到"会话过期"通知。
过期会话的观察者所看到的会话过期状态转换示例:
- 'connected' : 会话已建立,客户端正在与集群通信(客户端/服务器通信运行正常)
- .... 客户端与集群分区断开
- 'disconnected' : 客户端已与集群失去连接
- .... 时间流逝,在 'timeout' 周期后集群将使会话过期,由于客户端已与集群断开连接,客户端不会感知到任何变化
- .... 时间流逝后,客户端重新获得与集群的网络连接
- 'expired' : 最终客户端会重新连接到集群,随后会收到过期通知
ZooKeeper会话建立调用的另一个参数是默认监视器。当客户端发生任何状态变化时,监视器会收到通知。例如,如果客户端与服务器失去连接,客户端将收到通知;或者如果客户端的会话过期等...该监视器应将初始状态视为断开连接(即在客户端库向监视器发送任何状态变更事件之前)。对于新连接的情况,发送给监视器的第一个事件通常是会话连接事件。
客户端通过发送请求来保持会话活跃。如果会话空闲时间过长导致可能超时,客户端将发送PING请求以维持会话。该PING请求不仅能让ZooKeeper服务器知晓客户端仍处于活跃状态,同时也让客户端能够验证其与ZooKeeper服务器的连接是否仍然有效。PING的发送时机经过保守计算,确保有足够时间检测死连接并重新连接到新服务器。
一旦成功建立与服务器的连接(已连接),在以下两种情况下,当执行同步或异步操作时,客户端库会生成连接丢失(C绑定中的结果代码,Java中的异常——具体细节请参阅绑定特定的API文档):
- 应用程序在一个已失效/无效的会话上调用了操作
- 当存在待处理操作时,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实现。
- 背景:在ZooKeeper中,会话的创建和关闭成本很高,因为它们需要法定人数确认,当需要处理数千个客户端连接时,它们会成为ZooKeeper集群的瓶颈。因此,在3.5.0版本之后,我们引入了一种新型会话:本地会话,它不具备普通(全局)会话的全部功能,通过启用localSessionsEnabled参数即可使用此功能。
当 localSessionsUpgradingEnabled 被禁用时:
-
本地会话无法创建临时节点
-
一旦本地会话丢失,用户将无法使用会话ID/密码重新建立连接,该会话及其监视器将永久消失。注意:TCP连接断开并不一定意味着会话丢失。如果在会话超时之前能够与同一个ZooKeeper服务器重新建立连接,客户端可以继续操作(只是无法切换到其他服务器)。
-
当本地会话连接时,会话信息仅保存在其所连接的zookeeper服务器上。领导者节点不会感知到该会话的创建,并且不会将状态写入磁盘。
-
ping检测、会话过期及其他会话状态维护由当前会话所连接的服务器处理。
当localSessionsUpgradingEnabled启用时:
-
本地会话可以自动升级为全局会话。
-
当创建新会话时,它会被保存在本地封装的LocalSessionTracker中。随后可以根据需要(例如创建临时节点)将其升级为全局会话。如果请求升级,会话会从本地集合中移除,同时保持相同的会话ID。
-
目前,只有创建临时节点操作需要将本地会话升级为全局会话。原因是临时节点的创建高度依赖于全局会话。如果本地会话无需升级为全局会话就能创建临时节点,将导致不同节点间的数据不一致。领导者节点也需要了解会话的生命周期,以便在会话关闭/过期时清理临时节点。这就要求使用全局会话,因为本地会话是绑定到特定服务器的。
-
在升级过程中,一个会话可以同时作为本地会话和全局会话,但升级操作不能被两个线程并发调用。
-
ZooKeeperServer(独立模式)使用SessionTrackerImpl;LeaderZookeeper使用包含SessionTrackerImpl(全局)和LocalSessionTracker(如果启用)的LeaderSessionTracker;FollowerZooKeeperServer和ObserverZooKeeperServer使用包含LocalSessionTracker的LearnerSessionTracker。以下是会话相关的类UML图:
+----------------+ +--------------------+ +---------------------+ | | --> | | ----> | LocalSessionTracker | | SessionTracker | | SessionTrackerImpl | +---------------------+ | | | | +-----------------------+ | | | | +-------------------------> | LeaderSessionTracker | +----------------+ +--------------------+ | +-----------------------+ | | | | | | | +---------------------------+ +---------> | | | UpgradeableSessionTracker | | | | | ------------------------+ +---------------------------+ | | | v +-----------------------+ | LearnerSessionTracker | +-----------------------+ -
问答
- What's the reason for having the config option to disable local session upgrade?
- 在需要处理大量客户端的大型部署中,我们知道客户端应仅通过观察者(observers)建立本地会话连接。因此这更像是一种防护机制,防止有人意外创建大量临时节点和全局会话。
-
会话何时创建?
- 在当前实现中,当处理ConnectRequest时以及当createSession请求到达FinalRequestProcessor时,系统会尝试创建一个本地会话。
-
如果创建会话的请求最初发送到服务器A,然后客户端断开连接并连接到其他服务器B,导致请求被再次发送,接着又断开并重新连接回服务器A,会发生什么情况?
- 当客户端重新连接到B时,其sessionId不会存在于B的本地会话跟踪器中。因此B会发送验证数据包。如果A发出的CreateSession在验证数据包到达前已提交,客户端将能够连接。否则,由于仲裁集群尚未知晓该会话,客户端将收到会话过期通知。如果客户端又尝试重新连接回A,该会话已从本地会话跟踪器中移除。因此A需要向领导者发送验证数据包。根据请求的时间点,最终结果应该与B的情况相同。
ZooKeeper 监视器
ZooKeeper中的所有读取操作 - getData()、getChildren() 和 exists() - 都可以选择设置监听作为附带效果。以下是ZooKeeper对监听的定义:监听事件是一次性触发器,当被监听的数据发生变化时,会发送给设置该监听的客户端。在这个监听定义中有三个关键点需要考虑:
- 一次性触发器 当数据发生变化时,会向客户端发送一次监视事件。例如,如果客户端执行了getData("/znode1", true)操作,之后/znode1的数据被修改或删除,客户端将收到关于/znode1的监视事件。如果/znode1再次发生变化,除非客户端执行了另一次读取并设置了新的监视,否则不会发送监视事件。
- 发送给客户端 这意味着事件正在发送给客户端的途中,但在变更操作的成功返回码到达发起变更的客户端之前,事件可能还未送达。监视器会异步发送给观察者。ZooKeeper提供顺序保证:客户端在首次看到监视事件之前,永远不会看到它设置了监视的变更。网络延迟或其他因素可能导致不同客户端在不同时间看到监视和更新返回码。关键在于不同客户端看到的所有内容都将保持一致的顺序。
- 设置监视的数据 这里指的是节点可能发生变化的不同方式。可以将ZooKeeper理解为维护两种监视列表:数据监视和子节点监视。getData()和exists()设置数据监视,getChildren()设置子节点监视。或者也可以理解为根据返回的数据类型来设置监视:getData()和exists()返回节点数据相关的信息,而getChildren()返回子节点列表。因此,setData()会触发被设置znode的数据监视(假设设置成功)。成功的create()操作会触发被创建znode的数据监视以及父节点znode的子节点监视。成功的delete()操作会同时触发被删除znode的数据监视和子节点监视(因为该节点不再可能有子节点),以及父节点znode的子节点监视。
监视器(Watches)在客户端连接的ZooKeeper服务器本地维护。这使得监视器的设置、维护和分发变得轻量级。当客户端连接到新服务器时,任何会话事件都会触发监视器。在与服务器断开连接期间不会接收到监视通知。当客户端重新连接时,所有先前注册的监视器将重新注册并在需要时触发。通常情况下,这一切都是透明发生的。存在一种可能丢失监视通知的情况:如果在断开连接期间创建并删除了某个尚未创建的znode,那么对该znode存在性的监视通知将会丢失。
3.6.0 新特性: 客户端现在可以为 znode 设置永久性递归监视器,这些监视器在触发后不会被移除,并且会递归触发注册 znode 及其所有子节点上的变更通知。
监视器的语义
我们可以通过读取ZooKeeper状态的三个调用来设置监视器:exists、getData和getChildren。以下列表详细说明了监视器可以触发的事件以及启用这些事件的调用:
- 创建事件:通过调用exists启用。
- 删除事件:通过调用exists、getData和getChildren来启用。
- 变更事件:通过调用exists和getData来启用。
- 子节点事件:通过调用getChildren启用。
持久化递归监视器
3.6.0版本新增功能: 现在对上述标准监视机制进行了扩展,您可以设置一种触发后不会被移除的持久监视。此外,这些监视会触发节点创建、节点删除和节点数据变更事件类型,并可选择性地从注册监视的znode开始递归监视所有子节点。请注意,持久递归监视不会触发子节点变更事件,因为这会导致重复通知。
持久性监视器通过addWatch()方法设置。其触发语义和保证机制(除一次性触发外)与标准监视器相同。关于事件的唯一例外是:递归持久性监视器永远不会触发子节点变更事件,因为这些事件是冗余的。持久性监视器可通过removeWatches()方法配合WatcherType.Any监视器类型来移除。
移除监视器
我们可以通过调用removeWatches来移除在znode上注册的监视器。此外,即使没有服务器连接,ZooKeeper客户端也可以通过将local标志设置为true来在本地移除监视器。以下列表详细说明了成功移除监视器后将触发的事件。
- 子节点移除事件:通过调用getChildren添加的监视器。
- 数据删除事件: 通过调用exists或getData添加的监视器。
- 持久性移除事件: 通过调用添加持久监视器而添加的观察者。
ZooKeeper关于监视器的保证
关于监视器,ZooKeeper 提供以下保证:
-
监视器(Watches)相对于其他事件、其他监视器以及异步响应是有序的。ZooKeeper客户端库确保所有内容都按顺序分发。
-
客户端在查看对应znode的新数据之前,会先收到该znode的监视事件通知。
-
ZooKeeper发出的监视事件顺序与ZooKeeper服务端所处理的更新顺序保持一致。
关于监视器的注意事项
-
标准监视器是一次性触发器;如果您收到监视事件并希望获取未来变更的通知,必须重新设置另一个监视器。
-
由于标准监视器是一次性触发器,并且在获取事件和发送新请求以设置监视之间存在延迟,因此您无法可靠地看到ZooKeeper中节点发生的每一次更改。请准备好处理这种情况:在获取事件和重新设置监视之间,znode可能发生多次更改。(您可能不关心,但至少要意识到这种情况可能会发生。)
-
对于给定的通知,一个监视对象或函数/上下文对只会被触发一次。例如,如果同一个监视对象为同一个文件注册了exists和getData调用,并且该文件随后被删除,那么该监视对象只会被调用一次,并收到该文件的删除通知。
-
当你与服务器断开连接时(例如服务器发生故障),在重新建立连接之前,你将无法收到任何监视通知。因此会话事件会被发送给所有未完成的监视处理器。请利用会话事件进入安全模式:断开连接期间你将无法接收事件通知,因此你的进程在该模式下应采取保守策略。
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: 您可以创建一个子节点
- READ: 你可以从节点获取数据并列出其子节点。
- WRITE: 你可以为节点设置数据
- DELETE: 可以删除子节点
- ADMIN: 您可以设置权限
为了更细粒度的访问控制,CREATE(创建)和DELETE(删除)权限已从WRITE(写入)权限中分离出来。CREATE和DELETE的适用场景如下:
您希望A能够对ZooKeeper节点执行set操作,但不能创建或删除子节点。
CREATE 无 DELETE 权限:客户端通过在父目录中创建ZooKeeper节点来生成请求。您希望所有客户端都能添加节点,但只有请求处理器可以删除。(这类似于文件的APPEND权限。)
此外,由于ZooKeeper没有文件所有者的概念,因此设置了ADMIN权限。从某种意义上说,ADMIN权限将实体指定为所有者。ZooKeeper不支持LOOKUP权限(目录上的执行权限位,允许您LOOKUP即使您无法列出目录)。每个人隐式地拥有LOOKUP权限。这允许您查看节点状态,但仅此而已。(问题是,如果您想在一个不存在的节点上调用zoo_exists(),则没有权限可检查。)
ADMIN 权限在ACL方面还有一个特殊作用:用户必须拥有READ或ADMIN权限才能获取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 内置了以下方案:
- world 拥有一个单一ID anyone,代表任何人。
- auth是一种特殊方案,它会忽略任何提供的表达式,转而使用当前用户、凭据和方案。ZooKeeper服务器在持久化ACL时会忽略提供的任何表达式(无论是像SASL认证中的user还是像DIGEST认证中的user:password)。但是,ACL中仍必须提供该表达式,因为ACL必须匹配scheme:expression:perms的形式。提供此方案是为了方便,因为用户创建znode然后限制只有该用户才能访问该znode是一种常见用例。如果没有经过认证的用户,使用auth方案设置ACL将会失败。
- digest 使用用户名:密码字符串生成MD5哈希值,该哈希值随后被用作ACL ID身份标识。认证过程通过明文发送用户名:密码来完成。在ACL中使用时,表达式将是用户名:base64编码的SHA1密码摘要。
- ip 使用客户端主机IP作为ACL ID标识。ACL表达式的格式为addr/bits,其中addr的最高有效bits位将与客户端主机IP的最高有效bits位进行匹配。
- x509 使用客户端的X500主体作为ACL ID身份标识。ACL表达式是客户端确切的X500主体名称。当使用安全端口时,客户端会自动进行身份验证,并为其x509方案设置认证信息。
ZooKeeper C客户端API
ZooKeeper C 库提供了以下常量:
- const int ZOO_PERM_READ; //可以读取节点值并列出其子节点
- const int ZOO_PERM_WRITE;// 可以设置节点的值
- const int ZOO_PERM_CREATE; //可以创建子节点
- const int ZOO_PERM_DELETE; // 可以删除子节点
- const int ZOO_PERM_ADMIN; //可以执行set_acl()
- const int ZOO_PERM_ALL;// 以上所有标志位的或运算组合
以下是标准的ACL ID:
- struct Id ZOO_ANYONE_ID_UNSAFE; //('world','anyone' 的标识)
- 结构体 Id ZOO_AUTH_IDS;// ('认证','')
ZOO_AUTH_IDS 空身份字符串应解释为"创建者的身份"。
ZooKeeper客户端附带三种标准ACL:
- struct ACL_vector ZOO_OPEN_ACL_UNSAFE; //(ZOO_PERM_ALL,ZOO_ANYONE_ID_UNSAFE)
- 结构体 ACL_vector ZOO_READ_ACL_UNSAFE; // (ZOO_PERM_READ, ZOO_ANYONE_ID_UNSAFE)
- struct ACL_vector ZOO_CREATOR_ALL_ACL; //(拥有所有权限,认证ID)
ZOO_OPEN_ACL_UNSAFE是完全开放的ACL:任何应用程序都可以对节点执行任何操作,并且可以创建、列出和删除其子节点。ZOO_READ_ACL_UNSAFE是任何应用程序的只读访问权限。CREATE_ALL_ACL授予节点创建者所有权限。创建者必须已通过服务器认证(例如使用"digest"方案)才能使用此ACL创建节点。
以下ZooKeeper操作涉及ACLs:
- 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>对。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内置了两种认证插件: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);
}
无需实现AuthenticationProvider,而是扩展ServerAuthenticationProvider。您的handleAuthentication()和matches()方法随后将接收额外的参数(通过ServerObjs和MatchValues)。
- ZooKeeperServer ZooKeeper服务器实例
- ServerCnxn 当前连接
- path 正在操作的ZNode路径(如果未使用则为null)
- perm 操作值或0
- setAcls 当执行setAcl()方法操作时,正在设置的ACL列表
一致性保证
ZooKeeper是一个高性能、可扩展的服务。读写操作都设计得非常快速,尽管读取比写入更快。这是因为在读取情况下,ZooKeeper可以提供旧数据,这又源于ZooKeeper的一致性保证:
-
顺序一致性 : 来自客户端的更新将按照发送顺序被应用。
-
原子性:更新要么成功要么失败——不存在部分结果。
-
单一系统镜像:无论客户端连接到哪台服务器,都将看到相同的服务视图。也就是说,即使客户端在保持相同会话的情况下故障转移到其他服务器,也永远不会看到系统的旧视图。
-
可靠性:一旦应用了更新,该更新将从此持续存在,直到客户端覆盖更新为止。这一保证有两个推论:
- 如果客户端收到成功的返回码,则表示更新已应用。在某些失败情况下(如通信错误、超时等),客户端将无法确定更新是否已应用。我们会采取措施尽量减少失败,但只有成功的返回码才能确保更新已应用。(这在Paxos中被称为单调性条件。)
- 客户端通过读取请求或成功更新所看到的任何更新,在从服务器故障恢复时都不会回滚。
-
时效性:系统保证客户端视图在一定时间范围内(约数十秒)保持最新状态。在此时间范围内,客户端要么能感知到系统变更,要么会检测到服务中断。
利用这些一致性保证,可以轻松地在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.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线程的处理或同步调用的处理。
- 同步调用可能不会按正确顺序返回。例如,假设客户端执行以下操作:发起一个对节点/a的异步读取(设置watch为true),然后在读取完成的回调函数中又执行了一个对/a的同步读取。(这可能不是最佳实践,但也不违规,这里仅作为简单示例。)需要注意的是,如果在异步读取和同步读取之间/a发生了变更,客户端库会在同步读取响应之前收到/a变更的watch事件通知,但由于完成回调阻塞了事件队列,同步读取会在watch事件被处理前就返回/a的新值。
最后,与关闭相关的规则很简单:一旦ZooKeeper对象被关闭或接收到致命事件(SESSION_EXPIRED和AUTH_FAILED),该ZooKeeper对象即失效。关闭时,两个线程会停止运行,任何对zookeeper句柄的后续访问都属于未定义行为,应当避免。
客户端配置参数
以下列表包含Java客户端的配置属性。您可以使用Java系统属性设置其中任何属性。有关服务器属性,请查阅管理员指南中的服务器配置部分。ZooKeeper维基还提供了关于ZooKeeper SSL支持和ZooKeeper的SASL认证的有用页面。
-
zookeeper.sasl.client : 将该值设为false可禁用SASL认证。默认值为true。
-
zookeeper.sasl.clientconfig : 指定JAAS登录文件中的上下文键。默认为"Client"。
-
zookeeper.server.principal : 指定客户端在启用Kerberos认证时连接zookeeper服务器所使用的服务主体。如果提供了此配置,则ZooKeeper客户端将不会使用以下任何参数来确定服务主体:zookeeper.sasl.client.username、zookeeper.sasl.client.canonicalize.hostname、zookeeper.server.realm。注意:此配置参数仅适用于ZooKeeper 3.5.7+和3.6.0+版本
-
zookeeper.sasl.client.username : 传统上,一个主体(principal)由三部分组成:主名称(primary)、实例(instance)和域(realm)。典型的Kerberos V5主体格式为primary/instance@REALM。zookeeper.sasl.client.username指定了服务器主体的主名称部分,默认值为"zookeeper"。实例部分由服务器IP派生。最终服务器的主体格式为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 : 服务器主体(principal)的领域(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版本新增: 指定包含用于SSL连接的本地凭证的JKS文件路径,以及解锁该文件的密码。
-
zookeeper.ssl.keyStore.passwordPath : 3.8.0版本新增: 指定包含密钥库密码的文件路径
-
zookeeper.ssl.trustStore.location 和 zookeeper.ssl.trustStore.password : 3.5.5版本新增: 指定包含用于SSL连接的远程凭证的JKS文件路径,以及解锁该文件的密码。
-
zookeeper.ssl.trustStore.passwordPath : 3.8.0版本新增: 指定包含信任库密码的文件路径
-
zookeeper.ssl.keyStore.type 和 zookeeper.ssl.trustStore.type: 3.5.5版本新增: 指定用于建立与ZooKeeper服务器TLS连接的密钥/信任存储文件的格式。可选值: JKS, PEM, PKCS12 或 null (根据文件名自动检测)。默认值: 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和回调功能,用于集成到应用程序的事件循环中。这个库存在的唯一原因是支持那些
安装
如果您是从Apache代码库检出构建客户端,请按照以下步骤操作。如果您是从Apache下载的项目源码包构建,请直接跳至步骤3。
- 在zookeeper-jute目录(.../trunk/zookeeper-jute)中运行
mvn compile命令。这将在.../trunk/zookeeper-client/zookeeper-client-c下创建一个名为"generated"的目录。 - 切换到目录*.../trunk/zookeeper-client/zookeeper-client-c*并运行
autoreconf -if来引导autoconf、automake和libtool。请确保已安装autoconf 2.59版本或更高版本。直接跳转到步骤4。 - 如果是从项目源码包构建,请解压源码压缩包并切换到* zookeeper-x.x.x/zookeeper-client/zookeeper-client-c* 目录。
- 运行
./configure生成 makefile。以下是 configure 工具在此步骤中支持的一些可能有用的选项:
--enable-debug启用优化并开启调试信息编译器选项。(默认禁用。)--without-syncapi禁用Sync API支持;zookeeper_mt库将不会被构建。(默认启用。)--disable-static不构建静态库。(默认启用。)--disable-shared不构建共享库。(默认启用。)
注意
See INSTALL for general information about running configure. 1. Run
makeormake installto build the libraries and install them. 1. To generate doxygen documentation for the ZooKeeper API, runmake 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,您必须记住
- 包含ZooKeeper头文件:
#include - 如果您正在构建多线程客户端,请使用
-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);
-
*host : 连接Zookeeper服务器的字符串,格式为host:port。如果有多个服务器,在指定host:port对后使用逗号分隔。例如:"127.0.0.1:2181,127.0.0.1:3001,127.0.0.1:3002"
-
fn : 当通知被触发时用于处理事件的Watcher函数。
-
recv_timeout : 会话过期时间(毫秒)。
-
*clientid : 对于新会话可以指定为0。如果之前已经建立过会话,我们可以提供该客户端ID,它将重新连接到之前的会话。
-
*context : 可与zkhandle_t句柄关联的上下文对象。如果不需要使用,可以将其设置为0。
-
flags : 在初始化时,我们可以将其保留为0。
我们将演示一个客户端,在成功连接后输出"已连接到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用户常遇到的一些陷阱:
- 如果正在使用监视器(watches),必须关注连接监视事件。当ZooKeeper客户端与服务器断开连接时,在重新连接之前将不会收到变更通知。如果正在监视某个znode的创建事件,在断开连接期间若该znode被创建后又删除,将会错过这个事件。
- 你必须测试ZooKeeper服务器故障。只要大多数服务器处于活动状态,ZooKeeper服务就能在故障中存活。关键问题是:你的应用程序能处理这种情况吗?在现实世界中,客户端与ZooKeeper的连接可能会中断(ZooKeeper服务器故障和网络分区是连接丢失的常见原因)。ZooKeeper客户端库会负责恢复连接并通知你发生的情况,但你必须确保能恢复状态和任何失败的未完成请求。务必在测试实验室验证是否正确处理,而不是在生产环境中——使用由多台服务器组成的ZooKeeper服务进行测试,并对这些服务器进行重启测试。
- 客户端使用的ZooKeeper服务器列表必须与每个ZooKeeper服务器自身维护的服务器列表相匹配。如果客户端列表是实际ZooKeeper服务器列表的子集,系统仍可运行(尽管不是最优状态);但如果客户端列出了不属于ZooKeeper集群的服务器,则无法正常工作。
- 请注意事务日志的存放位置。ZooKeeper性能最关键的部分就是事务日志。ZooKeeper必须在返回响应前将事务同步到存储介质。使用专用的事务日志设备是保持稳定高性能的关键。如果将日志存放在繁忙的设备上,将会严重影响性能。如果只有单一存储设备,可以将跟踪文件放在NFS上并增加snapshotCount值;这虽然不能彻底解决问题,但可以缓解影响。
- 正确设置Java最大堆内存大小。非常重要的一点是避免内存交换。不必要的磁盘操作几乎必然会导致性能下降到不可接受的程度。请记住,在ZooKeeper中所有操作都是有序的,因此如果一个请求触发了磁盘操作,所有其他排队中的请求都会触发磁盘操作。为防止内存交换,建议将堆内存设置为物理内存总量减去操作系统和缓存所需的内存。确定最优堆内存大小的最佳方式是对您的配置进行负载测试。如果由于某些原因无法进行测试,建议采用保守估计,选择一个远低于会导致机器发生内存交换的临界值。例如,在4G内存的机器上,3G的堆内存设置就是一个保守的初始估计值。
其他信息链接
除了正式文档外,ZooKeeper开发者还可以通过其他多种渠道获取信息。
-
API Reference : ZooKeeper API的完整参考文档
-
ZooKeeper Talk at the Hadoop Summit 2008 : 雅虎研究院Benjamin Reed带来的ZooKeeper视频介绍
-
Barrier and Queue Tutorial : Flavio Junqueira 编写的优秀 Java 教程,演示了如何使用 ZooKeeper 实现简单的屏障和生产者-消费者队列。
-
ZooKeeper - 一个可靠、可扩展的分布式协调系统 : Todd Hoff 的文章 (2008年7月15日)
-
ZooKeeper Recipes : 关于使用ZooKeeper实现各种同步解决方案的伪层级讨论:事件处理、队列、锁和两阶段提交。
