本文是为想要创建使用ZooKeeper协调服务优势的分布式应用的开发者准备的。
本指南的前四节对各种ZooKeeper概念进行较高层次的讨论。这些概念对于理解ZooKeeper是如何工作的,以及如何使用ZooKeeper来进行工作都是必要的。这几节没有代码,但却要求读者对分布式计算相关的问题较为熟悉。这四节是:
l 一致性保证
接下来的四节提供了实际编程的信息。这四节是:
l 绑定
本文最后的附录包含到其他有用的ZooKeeper相关信息的链接。
本文的大多数信息以可独立访问的参考材料的形式存在。
ZooKeeper有一个分层的名字空间,跟分布式文件系统很相似。这就像一个允许文件也是目录的文件系统。节点路径总是表达为规则的、斜杠分隔的绝对路径,不存在相对路径。路径可以使用任何Unicode字符,但是需要遵循下列限制:
l 不能使用空字符(\?)。(这在C绑定中会导致问题)
l 因为不能正确显示,或者容易弄混淆,不能使用这些字符:\ - \和\ - \。
l 不允许使用这些字符:\? - uF8FFF? - \?。
l 可以使用小数点,但是不能单独使用.和..来指示路径中的节点,因为ZooKeeper不使用相对路径。/a/b/./c或者/a/b/../c是无效的。
l 记号zookeeper是保留的。
2
ZooKeeper树中的节点称作znode。如果提供的版本号与数据的实际版本不匹配,则更新操作失败。(可以覆盖这个行为,更多信息请看……)
注意:
分布式应用工程中,node这个词可以指代主机。
znode是程序员访问的主要实体,它有一些值得讨论的特征。
2 观察
客户端可以在znode上设置观察。对znode的修改将触发观察,然后移除观察。观察被触发时,ZooKeeper向客户端发送一个通知ZooKeeper观察。
2 数据存取
存储在名字空间中每个znode节点里的数据是原子地读取和写入的。读取操作获取节点的所有数据,写入操作替换所有数据。节点的访问控制列表(ACL)控制可以进行操作的用户。
ZooKeeper不是设计用来作为通用数据库或者大型对象存储的,而是用来存储协调数据的。
2.1 临时节点
ZooKeeper有临时节点的概念。临时节点在创建它的会话活动期间存在。会话终止的时候,临时节点被删除,所以临时节点不能有子节点。
2 顺序节点:唯一命名
创建znode时,可以要求ZooKeeper在路径名后增加一个单调增加的计数器部分。
2.2 ZooKeeper 中的时间
ZooKeeper以多种方式跟踪时间:
l zxid
每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。
对节点的每次修改将使得节点的版本号增加一。版本号有三种:version(znode数据修改的次数)、cversion(znode子节点修改的次数),以及aversion(znode的ACL修改次数)。
l tick
多服务器ZooKeeper中,服务器使用tick来定义状态上传。
l 真实时间
除了在创建和修改znode时将时间戳放入stat结构体中之外,ZooKeeper不使用真实时间,或者说时钟时间。
2.3 ZooKeeper的Stat 结构体
ZooKeeper中每个znode的Stat结构体由下述字段构成:
l czxid:创建节点的事务的zxid
l mzxid:对znode最近修改的zxid
l ctime:以距离时间原点(epoch)的毫秒数表示的znode创建时间
l mtime:以距离时间原点(epoch)的毫秒数表示的znode最近修改时间
l version:znode数据的修改次数
l cversion:znode子节点修改次数
l aversion:znode的ACL修改次数
l ephemeralOwner:如果znode是临时节点,则指示节点所有者的会话ID;如果不是临时节点,则为零。
l dataLength:znode数据长度。
l numChildren:znode子节点个数。
3 ZooKeeper 会话
客户端使用某种语言绑定创建一个到服务的句柄时,就建立了一个ZooKeeper会话。通常操作中句柄将处于这两个状态之一。如果发生不可恢复的错误,如会话过期、身份鉴定失败,或者应用显式关闭,则句柄进入到CLOSED状态。下图显式了ZooKeeper客户端可能的状态转换:
要创建客户端会话,应用程序代码必须提供一个包含逗号分隔的列表的字符串,其中每个主机:端口对代表一个ZooKeeper服务器(例如,"127。
3.2.0版新增加:可以在连接字符串后增加可选的"chroot"后缀,这让客户端命令都是相对于指定的根的(类似于Unix的chroot命令)。这让重用更加简单,用户应用在编码时以/为根,但实际的根位置(如/app/a)可以在部署时确定。
客户端取得ZooKeeper服务句柄时,ZooKeeper创建一个会话,由一个64位数标识,这个数将返回给客户端。
客户端库创建会话时需要的参数之一是毫秒表示的会话超时。客户端发送请求的超时值,服务器以可以分配给客户端的超时值回应。
从 服务集群分裂开来时,客户端(会话)将搜索会话创建时给出的服务器列表。只需要在被通知会话已过期时创建新的会话(必须的)。
会话过期由ZooKeeper集群,而不是客户端来管理。
已过期会话的观察看到的状态转换过程示例:
1.已连接:会话建立,客户端与集群通信中(客户端/服务器通信正常进行)
2.客户端从集群中分离
3.连接已断开:客户端失去同集群的连接
4.时间流逝,超时时间过后,集群让会话过期,客户端并不知道,因为它还是同集群断开连接的。
5.时间流逝,客户端与集群间的网络恢复正常。
6.已过期:最终客户端重新连接到集群,此时被通知会话已经过期。
建 立会话时的另一个参数是默认观察。客户端发生状态改变时观察会被通知。新建连接时发送给观察的第一个事件通常是会话连接建立事件。
会话由客户端发送的请求保持为活动状态。PING的时序足够保守,确保能够在合理的时间内检测到死掉的连接,重新连接到新的服务器。
一旦到服务器的连接成功建立,则进行同步或者异步操作时,通常有两种情况导致客户端库产生连接丢失事件(C绑定中的错误码,Java中的异常:关于绑定特定的细节,请看API文档):
1.应用程序对已经不存活/有效的会话进行操作
2.在有到服务器的未决操作(例如,有一个进行中的异步调用)时,客户端断开同服务器的连接
3。
4 ZooKeeper 观察
ZooKeeper中的所有读操作:getData()。对于这个定义,有三点值得关注:
l 一次触发
观察事件将在数据修改后发送给客户端。
l 发送给客户端
这暗示着,在(导致观察事件被触发的)修改操作的成功返回码到达客户端之前,事件可能在去往客户端的路上,但是可能不会到达客户端。这里的要点是:不同客户端看到的事情都有一致的次序。
l 为哪些数据设置观察
节点有不同的改动方式。
观察维护在客户端连接到的ZooKeeper服 务器中。这让观察的设置。
4.1 ZooKeeper 关于观察的保证
l 观察与其他事件。
l 客户端将在看到znode的新数据之前收到其观察事件。
l 观察事件的次序与ZooKeeper服务看到的更新次序一致。
4.2 关于观察需要记住的
l 观察是一次触发的:如果想在收到观察事件之后收到未来修改的通知,必须再次设置观察。
l 因为观察是一次触发的,而收到观察事件和发送新的请求、再次建立观察之间是有延迟的,所以不能可靠地观察到节点的所有修改。应该要准备处理在收到观察事件和再次设置观察之间,节点被多次修改的情况。(可以不处理,但至少要知道这种情况是可能的)
l 一个观察对象,或者函数/上下文对,只会因为某个通知而触发一次。
l 与服务器断开连接期间(比如说,服务器故障)不能收到任何观察事件,直到连接重新建立。
5 使用ACL 的访问控制
ZooKeeper使用ACL控制对节点的访问。
还要注意的是,ACL仅仅用于某特定节点。特别是,ACL不会应用到子节点。ACL不是递归的。
ZooKeeper支持可插入式鉴权模式。使用scheme:id的形式指定ID,其中scheme是id对应的鉴权模式。比如说,ip:172.16.1的主机的ID。
客户端连接到ZooKeeper,验证自身的时候,ZooKeeper将所有对应客户端的ID都关联到客户端连接上.22开头的客户端以READ权限。
5.1 ACL 权限
ZooKeeper支持下述权限:
l CREATE:可创建子节点
l READ:可获取节点数据和子节点列表
l WRITE:可设置节点数据
l DELETE:可删除子节点
l ADMIN:可设置节点权限
从WRITE权限中分离出CREATE和DELETE可以取得更好的访问控制。使用CREATE和DELETE的情况:
l 希望A可以设置节点数据,但是不能CREATE或者DELETE子节点。
l 没有DELETE的CREATE权限:客户端通过在某父目录中创建节点来创建请求。此时希望所有客户端可以添加节点,但是只有请求处理器可以删除节点。(这与文件的APPEND权限类似)
此外,ADMIN权限存在的原因是,ZooKeeper没有文件所有者的概念。每个用户都隐含地拥有LOOKUP权限。这仅仅让用户可以取得节点状态。(问题是,如果想对一个不存在的节点进行zoo_exists()调用,没有权限可以检查)
5 模式
ZooKeeper内置下述ACL模式:
l world具有单独的ID,代表任何用户。
l auth不使用任何ID,代表任何已确认用户。
l digest使用username:password字符串来生成MD5散列值,用作ID。身份验证通过发送明文的username:password字符串来进行。用在ACL表达式中时将是username:base64编码的SHA1密码摘要。
l ip使用客户端主机IP作为ID。
5
ZooKeeper C库提供下述常量:
l const int ZOO_PERM_READ;//可读取节点的值,列出子节点
l const int ZOO_PERM_WRITE;//可设置节点数据
l const int ZOO_PERM_CREATE;//可创建子节点
l const int ZOO_PERM_DELETE;//可删除子节点
l const int ZOO_PERM_ADMIN;//可执行set_acl()
l const int ZOO_PERM_ALL;//OR连接的上述所有标志
下面是标准的ACL ID:
l struct Id ZOO_ANYONE_ID_UNSAFE;//('world','anyone')
l struct Id ZOO_AUTH_IDS;//('auth','')
空的ZOO_AUTH_IDS标识字符串应该解释为“创建者的标识”。
ZooKeeper有三种标准ACL:
l struct ACL_vector ZOO_OPEN_ACL_UNSAFE;//(ZOO_PERM_ALL,ZOO_ANYONE_ID_UNSAFE)
l struct ACL_vector ZOO_READ_ACL_UNSAFE;//(ZOO_PERM_READ,ZOO_ANYONE_ID_UNSAFE)
l struct ACL_vector ZOO_CREATOR_ALL_ACL;//(ZOO_PERM_ALL,ZOO_AUTH_IDS)
ZOO_OPEN_ACL_UNSAFE是完全开放自由的ACL:任何应用程序可以对节点进行任何操作,以及创建
下述ZooKeeper操作用于处理ACL: l 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函数向服务器验证自身。如果想使用不同的模式和/或标识来进行身份验证,可以多次调用这个函数。 l 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权限位。 l int zoo_get_acl(zhandle_t* zh,const char* path,struct ACL_vector* acl,struct Stat* stat); 这个函数返回节点的ACL信息。 l 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。 <……省略示例代码……> ZooKeeper运行在各种使用不同身份验证模式的环境中,所以它有一个完全插入式的身份验证框架。内置的身份验证模式也是使用这个框架的。 要理解身份验证框架如何工作,首先必须理解两种主要的身份验证操作。下面是身份验证插件必须实现的接口: 第一个方法,getScheme返回标识插件的字符串。ZooKeeper服务器使用身份验证插件返回的模式字符串来确定将模式应用到哪些id。 handleAuthentication在客户端发送与连接相关联的身份验证信息时被调用。 身份验证插件与设置和使用ACL相关。 如果新的ACL含有auth条目,则isAuthenticated用于确定与连接相关联的身份验证信息是否要添加到ACL中。比如说,如果指定了auth,则客户端的IP地址不会被看作是id,不应该添加到ACL中。 检查ACL时,ZooKeeper调用matches(String id,String aclExpr)。 有两个内置的身份验证插件:id和digest。可通过系统属性添加额外的插件。可使用-Dzookeeper.authProvider.X=com.f.MyAuth来设置这些属性,或者在系统配置文件中添加类似于下面的条目: 应该注意,要确保后缀是唯一的.X=com.f.MyAuth2,只会使用一个。此外,所有服务器必须定义有同样的插件,否则客户端使用插件提供的身份验证模式连接到某些服务器时会有问题。 ZooKeeper是高性能、可伸缩的服务。读和写操作都设计为高速操作,虽然读比写更快。原因是在读操作中,ZooKeeper可返回较老的数据,这源自ZooKeeper的一致性保证: l 顺序一致性:一个客户端的更新将以发送的次序被应用。 l 原子性:更新要么成功,要么失败,没有部分结果。 l 单一系统镜像:无论连接到哪个服务器,客户端将看到同样的视图。 l 可靠性:一旦应用了某更新,结果将是持久的,直到客户端覆盖了更新。这个保证有两个推论: 1.如果客户端得到成功的返回码,则更新已经被应用。(这称作是Paxos中的单一条件) 2.服务器从失败恢复时,客户端通过读请求或者成功更新看到的任何更新,都不会回滚。 l 及时性:保证客户端的系统视图在某个时间范围(大约为十几秒)内是最新的。在此范围内,客户端要么可看到系统的修改,要么检测到服务终止。 使用这些一致性保证,就可以很容易地单独在ZooKeeper客户端构建如领导者选举Recipes and Solutions 。 注意:有时候开发者会错误地假定一个ZooKeeper实际上没有提供的保证: l 跨客户端视图的并发一致性 ZooKeeper并不保证在某时刻,两个不同的客户端具有一致的数据视图。 所以,ZooKeeper本身不保证修改在多个服务器间同步地发生,但是可以使用ZooKeeper原语来构建高层功能,提供有用的客户端同步ZooKeeper Recipes ) ZooKeeper客户端库以两种方式提供:Java和C。下面几节描述这两种绑定。 ZooKeeper的Java绑定由两个包组成:org.apache.zookeeper和org.apache.zookeeper.data。组成ZooKeeper的其他包由内部使用或者是服务器实现的组成部分。org.apache.zookeeper.data由简单地用作容器的类构成。 ZooKeeper Java客户端使用的主要类是ZooKeeper类。 创建ZooKeeper对象的时候,会同时创建两个线程:一个IO线程和一个事件线程。对于这个设计,有一些事情需要注意: l 所有同步调用和观察回调将按次序进行,一次一个。 l 回调不会阻塞IO线程或者同步调用的处理。 l 同步调用可能不会以正确的次序返回。(可能是不好的实现,但是是合法的,这只是一个简单的例子) 如果在异步读取和同步读取之间,对/a进行了修改,则客户端库将在同步读取返回之前接收到一个事件,表明/a已经被修改。 最后,关于关闭的规则很直接:一旦被关闭或者接收到致命事件(SESSION_EXPIRED和AUTH_FAILED),ZooKeeper对象就变成无效的了。 C绑定有单线程和多线程库。通过暴露在多线程库中使用的事件循环,单线程库允许在事件驱动应用中使用ZooKeeper。 有两个共享库:zookeeper_st和zookeeper_mt.x)。在其他场合,应用开发者应该链接zookeeper_mt,它同时支持同步和异步API。 如果从Apache代码仓库检出的代码创建客户端库,执行下面的步骤。 1.在ZooKeeper顶级目录(.../trunk)执行ant compile_jute
2.修改当前目录为../trunk/src/c,执行autoreconf -if,以启动autoconf。 3.如果从工程源代码包开始创建,解压缩源代码包,cd到zookeeper-x.x/src/c目录。 4.执行./configure <your-options>以生成makefile。对于这一步,configure工具支持下述有用的选项: l --enable-debug 启用优化和调试信息。(默认是禁用的) l --without-syncapi 禁止同步API支持,不创建zookeeper_mt库。(默认是启用的) l --disable-static 不创建静态库。(默认是启用的) l --disable-shared 不创建共享库。(默认是启用的) l 注意:关于执行configure的一般信息,请看INSTALL文件。 5.执行make或者make install,创建并且安装库。 6.要生成ZooKeeper API的doxygen文档,可执行doxygen-doc。关于其他文档格式的信息,请执行./congiure --help。 要测试客户端,可运行ZooKeeper服务器(关于如何运行,请看工程wiki页面的指示),使用作为安装过程一部分创建的某个cli应用程序来连接到服务器。下面的例子显示了使用cli_mt(多线程,与zookeeper_mt库一同创建),但是也可以使用cli_st(单线程,与zookeeper_st库一同创建): 这个客户端应用程序提供了一个执行简单ZooKeeper命令的Shell。成功启动并且连接到服务器之后,程序显示shell提示符。现在就可以输入ZooKeeper命令了。比如说,创建一个节点: 验证节点已经创建: 应该可以看到根节点的子节点列表。 在应用程序中使用ZooKeeper API时,应该记住: 1.包含ZooKeeper头文件:#include <zookeeper/zookeeper.h> 2.如果创建多线程客户端,请使用-DTHREADED编译器标志,以启用库的多线程版本,并且链接到zookeeper_mt库。如果创建单线程客户端,不要使用-DTHREADED,并且链接到zookeeper_st库。 关于Java和C的使用示例,请看程序结构和简单示例。 本节描述开发者可对ZooKeeper服务器执行的所有操作。这些信息比本手册前面章节的内容要更底层,但是比ZooKeeper API参考的层次要高。 Java和C绑定都可能报告错误。Java客户端绑定通过抛出KeeperException来报告错误,对异常对象调用code()可取得特定的错误码。 现在你了解ZooKeeper了,它高效。下面是ZooKeeper用户遇到的一些陷阱: 1.使用观察的时候,必须处理已连接的观察事件。如果观察一个节点的出现,则断开连接期间会错过节点的创建和删除事件。 2.必须测试ZooKeeper服务失败。 3.客户端和服务器使用的服务器列表应该一致。如果客户端的列表只是真正的服务器列表的一部分,程序可以工作,虽然不是最优的;但是如果客户端包含不在集群中的服务器,则不能工作。 4.注意在哪里放置事务日志。事务日志是ZooKeeper中最关乎性能的部分。 5.正确设置Java的最大堆大小。避免交换是非常重要的。大多数情况下,不必要地放入磁盘肯定会降低性能到不可接受的程度。记住,在ZooKeeper中,一切都是顺序的,如果一个请求触及磁盘,其他排队的请求也会触及磁盘。 为避免交换,试试将堆大小设置为拥有的物理内存大小减去操作系统和缓存需要的大小。比如说,在4GB的机器上,3GB是一个保守的开始值。 除了正式的文档之外,开发者还有其他一些信息来源: l ZooKeeper白皮书 由Yahoo!研究院发布的关于ZooKeeper设计和性能的权威讨论。 l API参考 关于ZooKeeper API的完整参考。 Yahoo。 l 护栏和队列教程 Flavio Junqueira编写的。 Todd Hoff编写的一篇文章(07/15/2008)。 关于ZooKeeper各种同步解决方案实现的虚拟层面的讨论:事件处理、队列、锁和两阶段提交。6 插入式身份验证
7 一致性保证
8 绑定
8.1 Java 绑定
8.2 C 绑定
8.2 安装
8.2 客户端
9 创建块:ZooKeeper 操作指南
9 处理错误
9
9.3 读取操作
9 写入操作
9.5 处理观察
9 操作
10 程序结构和简单示例
11 转向:常见问题和解决