译自
译自http://zookeeper.apache.org/doc/trunk/zookeeperProgrammers.html 1 简介
本文是为想要创建使用ZooKeeper协调服务优势的分布式应用的开发者准备的。本文包含理论信息和实践信息。
本指南的前四节对各种ZooKeeper概念进行较高层次的讨论。这些概念对于理解ZooKeeper是如何工作的,以及如何使用ZooKeeper来进行工作都是必要的。这几节没有代码,但却要求读者对分布式计算相关的问题较为熟悉。这四节是:
l ZooKeeper数据模型
l ZooKeeper会话
l ZooKeeper观察
l 一致性保证
接下来的四节提供了实际编程的信息。这四节是:
l 创建块:ZooKeeper操作指南
l 绑定
l 程序结构和简单示例
l 转向:常见问题和解决
本文最后的附录包含到其他有用的ZooKeeper相关信息的链接。
本文的大多数信息以可独立访问的参考材料的形式存在。但是,在编写第一个ZooKeeper应用程序之前,你应该至少读过ZooKeeper数据模型和ZooKeeper基本操作。此外,简单示例程序也有助于理解ZooKeeper客户端应用程序的基本结构。 2 ZooKeeper数据模型
ZooKeeper有一个分层的名字空间,跟分布式文件系统很相似。唯一的不同是,名字空间中的每个节点都可以有关联的数据和子节点。这就像一个允许文件也是目录的文件系统。节点路径总是表达为规则的、斜杠分隔的绝对路径,不存在相对路径。路径可以使用任何Unicode字符,但是需要遵循下列限制:
l 不能使用空字符(\\u0000)。(这在C绑定中会导致问题)
l 因为不能正确显示,或者容易弄混淆,不能使用这些字符:\\u0001 - \\u0019和\\u007F - \\u009F。
l 不允许使用这些字符:\\ud800 - uF8FFF、\\uFFF0 - uFFFF、\\uXFFFE - \\uXFFFF(X是1到E之间的一个数字)、\\uF0000 - \\uFFFFF。
l 可以使用小数点,但是不能单独使用.和..来指示路径中的节点,因为ZooKeeper不使用相对路径。/a/b/./c或者/a/b/../c是无效的。
l 记号zookeeper是保留的。 2.1 ZNode
ZooKeeper树中的节点称作znode。znode会维护一个包含数据修改和ACL修改版本号的Stat结构体,这个结构体还包含时间戳字段。版本号和时间戳让ZooKeeper可以校验缓存,协调更新。每次修改znode数据的时候,版本号会增加。客户端获取数据的同时,也会取得数据的版本号。执行更新或者删除操作时,客户端必须提供版本号。如果提供的版本号与数据的实际版本不匹配,则更新操作失败。(可以覆盖这个行为,更多信息请看……) 注意:
分布式应用工程中,node这个词可以指代主机、服务器、集群成员、客户端进程等等。ZooKeeper文档用znode指代数据节点;用server指代组成ZooKeeper服务的机器;用quorum peer指代组成集群的服务器;用client指代任何使用ZooKeeper服务的主机或者进程。
znode是程序员访问的主要实体,它有一些值得讨论的特征。 2.1.1 观察
客户端可以在znode上设置观察。对znode的修改将触发观察,然后移除观察。观察被触发时,ZooKeeper向客户端发送一个通知。关于观察的更多信息请看ZooKeeper观察。 2.1.2 数据存取
存储在名字空间中每个znode节点里的数据是原子地读取和写入的。读取操作获取节点的所有数据,写入操作替换所有数据。节点的访问控制列表(ACL)控制可以进行操作的用户。
ZooKeeper不是设计用来作为通用数据库或者大型对象存储的,而是用来存储协调数据的。协调数据的形式可能是配置、状态信息、聚合等等。各种形式的协调数据的一个共同特点是:它们通常比较小,以千字节来衡量。ZooKeeper客户端和服务器实现会进行检查,以保证znode数据小于1MB,但是平均的实际数据量应该远小于1MB。对较大数据的操作将导致某些操作比其他操作耗费更多时间,进而影响某些操作的延迟,因为需要额外的时间在网络和存储媒体间移动更多数据。如果需要大数据存储,通常方式是存储到块存储系统,如NFS或者HDFS中,然后在ZooKeeper中保存到存储位置的指针。 2.1.3 临时节点
ZooKeeper有临时节点的概念。临时节点在创建它的会话活动期间存在。会话终止的时候,临时节点被删除,所以临时节点不能有子节点。 2.1.4 顺序节点:唯一命名
创建znode时,可以要求ZooKeeper在路径名后增加一个单调增加的计数器部分。这个计数器相对于znode的父节点是唯一的。计数器的格式是%010d,也就是带有0填充的10个数字(这种格式是为了方便排序),比如说,0000000001。队列接收节里有一个使用这种特征的例子。注意:用于存储下一个顺序号的计数器是一个由父节点维护的有符号整数(4字节),所以计数器将在超过2147483647的时候溢出(导致名字成为-2147483647)。 2.2 ZooKeeper中的时间
ZooKeeper以多种方式跟踪时间:
l zxid
每次修改ZooKeeper状态都会收到一个zxid形式的时间戳,也就是ZooKeeper事务ID。事务ID是ZooKeeper中所有修改总的次序。每个修改都有唯一的zxid,如果zxid1小于zxid2,那么zxid1在zxid2之前发生。
l 版本号
对节点的每次修改将使得节点的版本号增加一。版本号有三种:version(znode数据修改的次数)、cversion(znode子节点修改的次数),以及aversion(znode的ACL修改次数)。
l tick
多服务器ZooKeeper中,服务器使用tick来定义状态上传、会话超时、节点间连接超时等事件的时序。tick仅被最小会话超时(2倍的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会话。会话创建后,句柄处于CONNECTING状态,客户端库会试图连接到组成ZooKeeper服务的某个服务器;连接成功则进入到CONNECTED状态。通常操作中句柄将处于这两个状态之一。如果发生不可恢复的错误,如会话过期、身份鉴定失败,或者应用显式关闭,则句柄进入到CLOSED状态。下图显式了ZooKeeper客户端可能的状态转换:
要创建客户端会话,应用程序代码必须提供一个包含逗号分隔的列表的字符串,其中每个主机:端口对代表一个ZooKeeper服务器(例如,"127.0.0.1:4545"或者"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位数标识,这个数将返回给客户端。如果连接到其他服务器,客户端将在连接握手时发送会话ID。出于安全考虑,服务器会为会话ID创建一个密码,ZooKeeper服务器可以校验这个密码。这个密码将在创建会话时与会话ID一同发送给客户端。与新的服务器重新建立会话的时候,客户端会和会话ID一同发送这个密码。
客户端库创建会话时需要的参数之一是毫秒表示的会话超时。客户端发送请求的超时值,服务器以可以分配给客户端的超时值回应。当前实现要求超时值最小是2倍的tickTime(在服务器配置文件中设置),最大是20倍的tickTime。客户端API可以获取商定的超时值。
从 服务集群分裂开来时,客户端(会话)将搜索会话创建时给出的服务器列表。最终,客户端至少和一个服务器重新建立连接,会话再次进入“已连接”状态(如果在会话超时之前重新连接上),或者进入到“已过期”状态(如果在会话超时后才重新连接上)。不建议在断开连接时创建一个新的会话对象(即一个新的ZooKeeper.class对象,或者C绑定中的zookeeper句柄),因为客户端库会进行重新连接。特别是客户端库具有试探特征,可以处理“羊群效应”等问题。只需要在被通知会话已过期时创建新的会话(必须的)。
会话过期由ZooKeeper集群,而不是客户端来管理。客户端与集群建立会话时会提供上面讨论的超时值。集群使用这个值来确定客户端会话何时过期。集群在指定的超时时间内没有得到客户端的消息时发生会话过期。会话过期时集群将删除会话的所有临时节点,立即通知所有(观察节点的)客户端。此时已过期会话的客户端还是同集群断开连接的,不会被通知会话已经过期,直到(除非)客户端重新建立到集群的连接,这时候已过期会话的观察才会收到“会话已过期”通知。
已过期会话的观察看到的状态转换过程示例:
1.已连接:会话建立,客户端与集群通信中(客户端/服务器通信正常进行)
2.客户端从集群中分离
3.连接已断开:客户端失去同集群的连接
4.时间流逝,超时时间过后,集群让会话过期,客户端并不知道,因为它还是同集群断开连接的。
5.时间流逝,客户端与集群间的网络恢复正常。
6.已过期:最终客户端重新连接到集群,此时被通知会话已经过期。
建 立会话时的另一个参数是默认观察。客户端发生状态改变时观察会被通知。比如说,客户端将在失去同服务器的连接,或者会话过期时被通知。观察应该认为初始状态是连接已经断开(在客户端库向观察发送任何状态改变事件之前)。新建连接时发送给观察的第一个事件通常是会话连接建立事件。
会话由客户端发送的请求保持为活动状态。如果要空闲一段将导致超时的时间,客户端将发送PING请求,保持会话是活动的。PING请求不仅让服务器知道客户端仍然是存活的,也让客户端可以确认,到服务器的连接依然是活动的。PING的时序足够保守,确保能够在合理的时间内检测到死掉的连接,重新连接到新的服务器。
一旦到服务器的连接成功建立,则进行同步或者异步操作时,通常有两种情况导致客户端库产生连接丢失事件(C绑定中的错误码,Java中的异常:关于绑定特定的细节,请看API文档):
1.应用程序对已经不存活/有效的会话进行操作
2.在有到服务器的未决操作(例如,有一个进行中的异步调用)时,客户端断开同服务器的连接 3.2.0版增加:SessionMovedException。有一种称作SessionMovedException的内部异常。通常客户端看不到这个异常。在某连接上收到一个会话请求,但是这个会话已经重建到另一个服务器上的时候会发生这种异常。导致这种错误的原因通常是,客户端向服务器发送请求,但是数据分组被延迟,以致客户端超时并且连接到一个新的服务器。延迟的分组到达先前的服务器的时候,服务器检测到会话已经移走,会关闭客户端连接。客户端通常看不到这个错误,因为客户端不会从较早的连接上读取数据(通常关闭了较早的连接)。两个客户端试图使用已保存的会话ID和密码重新建立相同的连接时会看到这种错误。其中一个客户端将重新建立连接,而另一个客户端会被断开连接(导致无限次地试图重新建立连接/会话)。 4 ZooKeeper观察
ZooKeeper中的所有读操作:getData()、getChildren()和exists(),都有一个设置观察作为边效应的选项。ZooKeeper对观察的定义是:观察事件是在被观察数据发生变化时,发送给建立观察的客户端的一次性触发器。对于这个定义,有三点值得关注:
l 一次触发
观察事件将在数据修改后发送给客户端。比如说,如果客户端执行getData("/znode1",true),然后/znode1的数据发生变化,或者被删除,则客户端将收到/znode1的观察事件。如果再次修改/znode1,则不会给客户端发送观察事件,除非客户端再执行一次读取操作,设置新的观察。
l 发送给客户端
这暗示着,在(导致观察事件被触发的)修改操作的成功返回码到达客户端之前,事件可能在去往客户端的路上,但是可能不会到达客户端。观察事件是异步地发送给观察者(客户端)的。ZooKeeper会保证次序:在收到观察事件之前,客户端不会看到已经为之设置观察的节点的改动。网络延迟或者其他因素可能会让不同的客户端在不同的时间收到观察事件和更新操作的返回码。这里的要点是:不同客户端看到的事情都有一致的次序。
l 为哪些数据设置观察
节点有不同的改动方式。可以认为ZooKeeper维护两个观察列表:数据观察和子节点观察。getData()和exists()设置数据观察。getChildren()设置子节点观察。此外,还可以认为不同的返回数据有不同的观察。getData()和exists()返回节点的数据,而getChildren()返回子节点列表。所以,setData()将为znode触发数据观察。成功的create()将为新创建的节点触发数据观察,为其父节点触发子节点观察。成功的delete()将会为被删除的节点触发数据观察以及子节点观察(因为节点不能再有子节点了),为其父节点触发子节点观察。
观察维护在客户端连接到的ZooKeeper服 务器中。这让观察的设置、维护和分发是轻量级的。客户端连接到新的服务器时,所有会话事件将被触发。同服务器断开连接期间不会收到观察。客户端重新连接 时,如果需要,先前已经注册的观察将被重新注册和触发。通常这都是透明的。有一种情况下观察事件将丢失:对还没有创建的节点设置存在观察,而在断开连接期间创建节点,然后删除。 4.1 ZooKeeper关于观察的保证
l 观察与其他事件、其他观察和异步回应是顺序的。ZooKeeper客户端库保证一切都是按顺序分发的。
l 客户端将在看到znode的新数据之前收到其观察事件。
l 观察事件的次序与ZooKeeper服务看到的更新次序一致。 4.2 关于观察需要记住的
l 观察是一次触发的:如果想在收到观察事件之后收到未来修改的通知,必须再次设置观察。
l 因为观察是一次触发的,而收到观察事件和发送新的请求、再次建立观察之间是有延迟的,所以不能可靠地观察到节点的所有修改。应该要准备处理在收到观察事件和再次设置观察之间,节点被多次修改的情况。(可以不处理,但至少要知道这种情况是可能的)
l 一个观察对象,或者函数/上下文对,只会因为某个通知而触发一次。比如说,对同一个文件使用exists和getData调用,设置相同的观察对象,然后文件被删除,则观察对象只会被调用一次,带有文件删除通知。
l 与服务器断开连接期间(比如说,服务器故障)不能收到任何观察事件,直到连接重新建立。因此,会话事件是发送给所有未决观察处理器的。可使用会话事件进入到安全模式:断开连接期间不会收到任何事件,进程应该谨慎操作。 5 使用ACL的访问控制
ZooKeeper使用ACL控制对节点的访问。ACL的实现同Unix文件访问权限非常相似:采用权限位来定义允许/禁止的各种节点操作,以及位应用的范围。与标准Unix权限不同的是,ZooKeeper节点不由用户(文件所有者)、组和其他这三个标准范围来限制。ZooKeeper没有节点所有者的概念。取而代之的是,ACL指定一个ID集合,以及与这些ID相关联的权限。
还要注意的是,ACL仅仅用于某特定节点。特别是,ACL不会应用到子节点。比如说,/app只能被ip:172.16.16.1读取,/app/status可以被所有用户读取。ACL不是递归的。
ZooKeeper支持可插入式鉴权模式。使用scheme:id的形式指定ID,其中scheme是id对应的鉴权模式。比如说,ip:172.16.16.1是地址为172.16.16.1的主机的ID。
客户端连接到ZooKeeper,验证自身的时候,ZooKeeper将所有对应客户端的ID都关联到客户端连接上。客户端试图存取节点的时候,ZooKeeper会在节点的ACL中校验这些ID。ACL由(scheme:expression,perms)对组成。expression的格式是特定于scheme的。比如说,(ip:19.22.0.0/16,READ)给予任何IP地址以19.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没有文件所有者的概念。某些情况下ADMIN权限可以指定实体的所有者。ZooKeeper不支持LOOKUP权限(目录上的、允许进行LOOKUP的执行权限位,即使不能列出目录内容)。每个用户都隐含地拥有LOOKUP权限。这仅仅让用户可以取得节点状态。(问题是,如果想对一个不存在的节点进行zoo_exists()调用,没有权限可以检查) 5.1.1 内置的ACL模式
ZooKeeper内置下述ACL模式:
l world具有单独的ID,代表任何用户。
l auth不使用任何ID,代表任何已确认用户。
l digest使用username:password字符串来生成MD5散列值,用作ID。身份验证通过发送明文的username:password字符串来进行。用在ACL表达式中时将是username:base64编码的SHA1密码摘要。
l ip使用客户端主机IP作为ID。ACL表达式的形式是addr/bits,表示addr的最高bits位将与客户端主机IP的最高bits位进行匹配。 5.1.2 ZooKeeper C客户端API
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>
l struct>
l struct> 空的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:任何应用程序可以对节点进行任何操作,以及创建、列出和删除子节点。ZOO_READ_ACL_UNSAFE给予任何应用程序以只读访问权限。CREATE_ALL_ACL给予节点创建者所有权限。创建者在使用这种ACL创建节点之前,必须已经通过服务器的身份鉴别(比如说,使用"digest"模式)。
下述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客户端实现示例请看../trunk/src/c/src/cli.c。