Zookeeper官网文档—第二章 2.概述-概述

介绍

该文档是指在指导开发者利用Zookeeper协同服务开发分布式服务应用.它包含许多概念和实用的知识.

该指南的前四部分主要阐述了Zookeeper许多高级概念.这能够帮助开发者理解Zookeeper是如何工作的和怎样利用Zookeeper去工作.文档中不包含源代码,但是它假定你对分布式系统的问题比较熟悉. 围绕四个方面:

接下来的四个方面提供实际的编程知识.有这些:

这本书的最后是一个附录,包含一些有用的连接和Zookeeper的相关信息.

本文档的大多数信息都是作为独立的参考资料而编写的. 但是,在启动你得第一个Zookeeper应用之前,你应该至少阅读Zookeeper数据模型ZooKeeper基本操作 的相关概念. 并且, 简单的应用实例 [tbd]可以帮助你来理解Zookeeper客户端应用的基本结构.

ZooKeeper数据模型

ZooKeeper有一个分层命名空间,类似于一个分布式文件系统.唯一的不同是命名空间中的每一个节点能够存储数据,子节点也是. 它看起来像是文件系统,并且允许文件也是一个目录. 一个规范的、绝对的、斜杠分隔的路径来表示一个节点路径. 不存在相对路径. 任何符号只要符合以下约束条件都可以被使用:

  • null字符(\u000)不能在一个路径名称被使用.(这会导致C语言编程的问题.)

  • 下列字符不能被使用,因为他们不能被很好的被展示:\u0001 - \u0019 and \u007F - \u009F.

  • 下列字符不允许使用: \ud800 -uF8FFF, \uFFF0 - uFFFF.

  • "."字符可以和其他字符组合使用,但是 "." and ".."不能被单独作为一个节点和路径使用,因为Zookeeper不能使用相对路径.下面是一些无效的例子: "/a/b/./c" 或者 "/a/b/../c".

  • 这个标识"zookeeper"是保留的.

ZNodes

在Zookeeper数中每一个节点都被称为znode. Znodes包含一个stat数据结构,包含版本号、acl改变. sta数据结构也有时间戳. 通过版本号和时间戳Zookeeper能够检查缓存和协调更新. 每次zode的数据改变,版本号会增加. 例如,每当客户端获取数据时,会同时获取数据的版本. 当客户端执行一个更新或删除时,它必须提供要改变节点数据的版本号. 如果它提供的版本号不等于真实数据的版本号,更新将失败. (这个行为也可以被重写,更多信息看...)[tbd...]

注意

在分布式应用中,node这个词可以用来表示一台主机,一个服务,一个集群中的成员,一个客户端进程,etc. 在Zookeeper文档中,znodes表示一个数据节点. 服务表示组成Zookeeper服务的机器; quorum peers表示组成集群的机器; 客户端表示任何使用Zookeper服务的主机或进程.

一个znode是需要程序员熟悉的重要抽象概念. Znodes有几个值得注意的特征.

观察者

客户端能够为znodes设置观察者. zonde节点发生改变时会触发观察者并在之后移除观察者. 当观察者被触发,Zookeeper会给客户端发送一个通知.关于观察者更多的内容,可以查看ZooKeeper Watches部分.

数据访问

在命名空间的每个znode的数据存储操作读和写都是原子性的. 读操作读取与这个znode节点相关的全部数据,写操作更新znode的全部数据. 每个节点都有一个访问控制清单(ACL)约束了谁能进行操作.

ZooKeeper不是为了设计成一般数据库或者为大对象数据存储而做的. 相反,他是用来管理协调数据.  数据能够以配置信息、状态信息、集合点等形式存在. 各式各样分布式数据的共同点是它们都比较小. 以千字节为标准. Zookeeper客户端和服务端有一个健康检查来确保znodes节点的数据小于1M, 然而数据通常情况下会更小. 操作大的数据会导致操作花费更多的时间,会影响操作的延迟,因为需要在网络和存储媒介中传输数据需要消耗额外的时间. 处理该数据的通常模式是将该数据存储在一个大容量的存储系统,例如NFS和HDFS,然后在Zookeeper中去存储数据的保存位置.

临时节点

ZooKeeper也有临时节点的概念. 这些znodes存活的时间和创建这个节点的会话有效期是一样的. 当会话结束,znode会被删除. 因为这个原因,所以临时的znodes是不允许有孩子节点的.

顺序节点 -- 唯一名称

当创建一个znode时,你可以请求Zookeeper在路径的末尾添加一个自增计数器. 对于父节点来说这个计数器是独一无二的. 计数器的格式是%010d -- 这是一个十位数.(客户端以这种格式化来简化排序), i.e. "<path>0000000001". 看 Queue Recipe 关于这个特征使用的例子. 注意:这个计数器用来存储下一个序列号是一个4字节的数,当增加到2147483647 之后,计数器会溢出.

Zookeeper中的时间

ZooKeeper跟踪时间的多种方式:

  • Zxid

    每次改变Zookeeper的状态都会接收一个zxid形式的标志.(ZooKeeper事务Id). 这里展示了所有Zookeeper变更顺序.每次改变都会获得一个独一无二的zxid,如果zxid1小于zxid2,那么zid1产生在zxid2之前.

  • version

    每次改变节点都会使节点的版本号之一的增加. 这三个版本号是:version (一个节点数据的变化次数), cversion (一个节点的孩子节点的变化次数), 和 aversion (一个节点ALC的变化次数).

  • Ticks

    当使用集群Zookeeper时,服务使用ticks作为事件的定义时间,例如状态更新、session超时、集群节点间连接超时时间. 这个tick时间仅仅间接通过最小session超时时间暴露出来. (2个tick的时间); 如果一个客户端请求session超时时间是小于最小超时时间,这个服务会告诉客户端的超时时间值等于最小超时时间.

  • Real time

    ZooKeeper不使用real time时间或者时钟时间,除了在创建或修改znode时将该时间放入stat结构中.

ZooKeeper Stat Structure

每个Zookeeper的znode的stat结构由以下这些字段组成:

  • czxid

    znode节点创建时的事务ID.

  • mzxid

    znode最后修改时间的事务ID

  • pzxid

    znode孩子节点最后修改时的事务ID.

  • ctime

    znode被创建时的时间,以毫秒为时间单位.

  • mtime

    znode被修改时的时间,以毫秒为时间单位.

  • version

    znode节点数据改变的次数

  • cversion

    znode孩子节点改变的次数

  • aversion

    znode节点ACL变化的次数

  • ephemeralOwner

    如果这个节点是临时节点,那么这个标识znode的所属会话ID. 如果不是临时节点,那么会是0.

  • dataLength

    znode数据的长度

  • numChildren

    znode孩子节点的数量

ZooKeeper会话

通过一种使用一种开发语言可以使客户端与服务端创建一个句柄而与Zokkeeper服务建立会话. 一旦建立连接,处理状态会处于CONNECTINS状态,客户端库会尝试和组成Zookeeper服务的其中一台服务建立连接,这时处理状态会转变为CONNECTEDz状态. 在正常操作时,将会处于这两个状态之一的状态.  如果遇到不可恢复的错误,例如会话超时、身份验证失败、应用处理句柄关闭,句柄状态会变为CLOSED状态. 下面图标显示Zokeeper客户单可能的状态变化:

创建客户端会话的代码字符串应该包含一个由逗号分隔的host:port组成,每一个单元相当于一个Zookeeper服务. (e.g. "127.0.0.1:4545" 或 "127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002"). ZooKeeper客户端库会选择任意的服务尝试连接. 如果连接失败,或者客户端和服务端因为任何原因断开连接,客户端会自动选择列表中下一个服务自动连接,直到建立连接为止.

Added in 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",所有节点都会与这个根节点相关联 - ie getting/setting/etc... 在这个操作中"/foo/bar"实际结果是运行在"/app/a/foo/bar" (从服务器角度). 这个特征在多租户环境中是特别有用的,Zokkeeper服务的每个用户都使用不同的根节点. 这让重用变得更加简单,每个用户都能在好像在根节点"/"编写程序,而真实的地址(say /app/a)在部署时确定.

当客户端从Zokeeper服务获得一个句柄,Zokeeper会创建一个Zookeeper会话,标识为一个64位的数字,它会被分配给客户端. 如果客端户连接不同的Zookeeper服务,它会将会话ID作为连接握手的一部分一同发送.  作为一个安全保障,服务端会创建一个让任意Zookeeper服务都能够验证的seesion id 密码. 这个密码会在客户端与会话建立连接后与session id 一同发送. 每当客户端与一个新服务建立会话时,它会和会话ID一同发送.

Zookeeper客户端库和Zookeeper会话创建连接时,有一个参数是以毫秒为单位的会话超时时间. 客户端发送一个请求超时时间,而服务端则回复客户端一个超时时间. 当前实际的超时时间最小是tick时间的两倍,最大是tick时间的20倍.  可以使用Zookeeper客户端API来协商设定该值.

当一个客户单(会话)从Zokeeper集群中分掉线时,它会查询当会话创建时的服务列表. 最终,当客户端和其中的一个服务建立连接,会话会转为为"connected" 状态 (如果在会话超时时间内回复连接)或者会转变为"expired"状态(如果在会话超时时间之后回复连接). 在断开连接时不应创建一个新的会话对象(在C中是一个新的Zookeeper类或者Zookeeper句柄). Zookeeper客户端库会为你处理句柄重连.尤其是我们内置构建的客户端库会处理类似"羊群效应"这样的问题(从众心理)等...  只有当通知你会话过期时,才应创建一个新的连接.

会话过期由Zookeeper集群进行管理,而不由客户端进行管理. 当客户端与集群建立连接时,会提供一个上面所说的超时时间. 这个值被用来确定客户端会话合适过期. 当客户端与集群在制定的超时时间内没有心跳,超时会发生.(i.e. no heartbeat).  在会话超时时集群会删除所有属于这个会话的临时节点,并立刻通知所有客户端这个改变. (任何管着这些znodes的客户端). 此刻会话过期的客户端与集群依然是断开连接的,他不会收到过期通知,除非它能够与集群重新建立连接. 这个客户端会一直停留在断开连接的状态,直到TCP连接能够与集群重新建立连接. 此时过期Session的观察者会收到"会话过期"的消息.

对于一个过期会话的观察者,其观察者的状态变化:

  1. 'connected' : 会话和集群是连接和通信的 (client/server运行正常)

  2. .... 客户端与集群断开连接

  3. 'disconnected' : 客户单与集群失去通信

  4. 转瞬间,在超时时间过后,集群失效这个会话,客户单不能收到任何通知,因为它此时已经与集群断开连接.

  5. 转瞬间,客户单恢复与集群的网络连通性.

  6. 'expired' : 最后客户端与集群恢复连接,客户端会收到过期消息.

Zookeeper建立会话连接的另一个参数是默认观察者. 当客户端发生任何状态改变时观察者都会收到通知. 例如如果客户端失去与服务器连接,客户端会收到通知,或者如果客户端会话超时也一样. 这个监听者应该考虑初始状态到断开状态.(i.e. 在任何状态变动时间之前通过客户端库发送给观察者). 对于一个新的连接,首要事件应该是通知观察者会话连接事件.

客户端通过发送请求使会话保持存活. 如果会话在一段时间是空闲的,会导致会话超时,客户端会发送一个PING请求去保持会话的活跃. 这个PING请求不仅仅让Zookeeper服务端知道客户端是存活的,还允许客户端确认连接的Zookeeper服务是活跃的. PING的时间是足够保守的合理时间来确保发现一个死掉的连接和恢复一个新的服务.

一旦与服务端建立连接成功,当客户单发生connectionloss异常时(在C中是异常代码,在Java中是异常,详细情况查看API文档)一般有两种情况,同步或异步操作导致:

  1. 当执行一个操作时,会话已经长时间失效.

  2. 当等待服务器端操作时,Zookeeper客户端断开连接,例如:等待一个异步操作.

Added in 3.2.0 -- SessionMovedException. 用一个通常不被客户单发现的异常被称为SessionMovedException. 这个异常的原因是一个已经建立连接的会话重新与一台不同的服务建立连接并接受请求. 通常导致这个异常的原因是客户端发送一个请求到一个服务器,但是网络数据延迟,所以客户端超时并连接到一台新的服务器.  当延迟的数据发送到第一个服务器,旧的服务器会发现会话已经移除,客户端连接被关闭. 客户端通常情况下不会发现这个错误,因为客户端不会从哪些老的连接中读取数据.(老的连接通常是关闭的). 另一种情况也会发生这种情况,当两个客户端都尝试使用同一个session id和密码恢复同一个连接时. 其中一台客户单会恢复连接,而另一台客户端将会断开连接.(原因是它会一直重新尝试连接会话).

ZooKeeper观察者

在Zookeeper所有读取操作 - getData(), getChildren(), 和 exists() - 可以选着设置一个watch作为一个监听者. 这是Zokkeeper种watch的定义 :一个监听事件只会触发一次,当监听到数据的改变,会发送给设置了监听者的客户端. 关于观察者的定义,有三个点需要思考:

  • 触发一次

    当数据发生变化时,一个监听事件会发送给客户端. 例如,如果一个客户端一个客户端执行了getData("/znode1",true),之后对/znode1节点执行修改和删除操作,客户端将会触发一个关于/znode1的观察者事件.如果/znode1节点又一次改变,不会再次触发观察者事件,除非再次通过另外一个getData("/znode1",true)设置一个新的观察者.

  • 发送客户端

    意味着事件可能在送达客户端的路上,但是在操作成功的返回码到达发起这个变更操作的客户端之前,事件可能还没到达监听的客户端. 观察者会异步发送到观察者. Zookeeper提供一个顺序保障:客户端从来不会看到它设置的监听者改变,知道第一个发现监听事件.网络延迟或者其他的因素可能导致不同的客户端能够在不同时间发现观察者并获得返回码.关键的一点是每一个客户端看到每一个事件有一个始终如一的顺序.

  • 被设置监听的数据

    这是指一个节点能够变化的不同方式. 可以认为Zokkeeper有两个监听列表: 数据监听和孩子节点监听. getData() 和 exists() 设置数据监听器. getChildren()设置孩子观察者. 二者选一,可以根据返回数据的类型来设置观察者. getData() and exists()会返回关于节点数据的信息, 而 getChildren()会返回一个孩子列表. 因此, setData()会触发znode设置的数据观察者. 一个成功的 create()会触发一个数据监听者. 一个成功的delete()会同时触发数据监听者和一个子节点监听者(因为可能没有更多的子节点).

在客户端连接服务端时,监听者会被保存在本地. 这使得观察者可以轻量级、可维护、可分派. 当客户端连接一个新的服务,监听器会触发一些会话事件. 当与服务器端断开连接,观察者将不会收到通知. 当客户端重连,如果需要所有之前注册的监听者将会重新注册并且触发. 这是显而易见的. 这里有可能导致监听者丢失:如果在断网期间,znode被创建或删除,一个已创建的节点监听器没有创建,将会丢失.

监听者的含义

我们可以用读取Zookeeper状态的三个调用方法来设置观察者:exists, getData, 和 getChildren. 下面是一个观察者能够触发和调用的事件列表:

  • Created event:

    激活调用exitsts.

  • Deleted event:

    激活调用exitsts,getData和getChildren.

  • Changed event:

    激活调用exists和getData.

  • Child event:

    激活调用getChildren

关于观察者的Zookeeper保障是什么.

关于观察者,Zookeeper提供这些保障:

  • 观察者是根据其他事件、其他观察者、异步应答而确定的.Zookeeper客户端库确保事件都能顺序被分发.

  • 客户端会在看到znode节点对应数据之前发现观察者事件.

  • Zookeeper服务的观察者事件的顺序对应Zookeeper服务更新的顺序.

关于观察者要记住的事情

  • 观察者只能被触发一次;如果你想获得其未来的改变通知,那你必须设置另一个观察者.

  • 因为观察者只能被触发一次,你不能发现在获得事件和发送一个请求设置一个新的观察这间的Zookeeper的任何变化. 准备处理这种在获得观察者事件和再次设置观察者事件之间的znode节点多次改变的情况. (你可能不关注,但是至少应该认识到它可能发生)

  • 一个观察者对象,或者方法/上下文对,为一个通知只会被触发一次. 例如,如果相同的观察者在exitsts和getData调用中注册到了相同的文件,这个文件被删除,观察者对象对于这个文件只会触发一次删除通知.

  • 当你从服务断开连接(例如,服务异常),在你重连之前观察者不会收到任何观察者. 由于这个原因会话事件会被发送给所有未处理的监听器. 使用会话事件应该进入安全模式:你将不会在断开期间接受到任何事件,所以你在该模式下应该保持谨慎.

ZooKeeper使用ACLs控制访问

ZooKeeper使用ACL来控制访问znodes(Zookeeper数据树上的数据节点). ACL的实现与UNIX的文件访问权限十分相似: 它使用权限bit来允许/拒绝一个节点和比特许可范围的各种操作. 不同于传统的UNITX权限系统,一个Zookeeper节点没有三个传统作用域的限制:user(文件所属用户)、group,和world(其他). ZooKeeper没有节点所有者的概念. 而是ACL指定集合,与该权限相关的ID集合.

也要注意ACL仅适用于一个特定的znode. 它不适用于其子节点.例如,如果/appp仅仅被ip为172.16.16.1的读取,/app/status是有开放全部读取权限的,任何人都可以读取/app/status;ACLs不是递归的.

Zookeeer支持可插拔式的插件认证方案. Ids指定使用这个形式scheme:id,shceme是ID对应的认证方案. 例如, ip:172.16.16.1是一个主机地址为172.16.16.1的ID.

当客户端连接Zookeeper并进行认证,Zookeeper将与符合这个客户端的所有ids关联起来. 当客户端试图访问一个节点时,这些ids会检查znodes节点的ALC.  ACLs是由成对组成的(scheme:expression,perms). ACLs are made up of pairs of (scheme:expression, perms). 表达式的格式指定了权限. 例如,这个键值对给所有19.22开头的客户端体哦概念股了读取权限.

ACL权限

ZooKeeper支持如下权限:

  • CREATE: 你可以创建一个子节点.

  • READ: 你可以读取节点的数据和获得子节点列表.

  • WRITE: 你可以设置节点的数据

  • DELETE: 你可以删除一个子节点

  • ADMIN: 你可以设置权限

CREATE和DELETE权限从WRITE权限中分离开来,为了更好的访问控制. CREATEDELETE的情况如下:

你想要A有权限设置节点数据,而不能CREATEDELETE子节点.

CREATE而无DELETE: 客户端在父目录发起创建Zookeeper节点的请求.你想要所有客户端都能够添加,但是只有发起该请求的能够删除.(这个情况类似与追加文件权限)

而且,ADMIN权限存在是因为Zookeeper没有文件所有者的概念. 某种意义上,ADMIN权限就相当于文件所有者. ZooKeeper不支持LOOKUP权限(目录上执行权限位允许你查看,即使你不能列出目录). 所有人都隐含LOOKUP权限. 允许你查看节点状态,但是不能进行其他操作.(有些问题,在一个不存在的节点上执行调用zoo_exists(),没有权限进行安全检查.)

内置的ACL方案

ZooKeeeper有如下内置构建:

  • world 有一个独立ID, 代表任意一个人.

  • auth 不使用任何ID,代表任何权限使用者.

  • digest 使用用户名:密码字符串生成一个MD5,当作时ACL ID. 认证时通过发送用户名:密码的明文来进行的. 当使用ACL时,表达式将会使用用户名:base64,base64时SHA1密码加密.

  • ip 使用客户端IP作为一个ACL ID身份. 这个ACL表达式的格式为addr/bits ,此时addr中的有效位与客户端addr中的有效位进行比对.

ZooKeeper C client API

The following constants are provided by the ZooKeeper C library:

  • const int ZOO_PERM_READ; //can read node’s value and list its children

  • const int ZOO_PERM_WRITE;// can set the node’s value

  • const int ZOO_PERM_CREATE; //can create children

  • const int ZOO_PERM_DELETE;// can delete children

  • const int ZOO_PERM_ADMIN; //can execute set_acl()

  • const int ZOO_PERM_ALL;// all of the above flags OR’d together

The following are the standard ACL IDs:

  • struct Id ZOO_ANYONE_ID_UNSAFE; //(‘world’,’anyone’)

  • struct Id ZOO_AUTH_IDS;// (‘auth’,’’)

ZOO_AUTH_IDS empty identity string should be interpreted as “the identity of the creator”.

ZooKeeper client comes with three standard ACLs:

  • struct ACL_vector ZOO_OPEN_ACL_UNSAFE; //(ZOO_PERM_ALL,ZOO_ANYONE_ID_UNSAFE)

  • struct ACL_vector ZOO_READ_ACL_UNSAFE;// (ZOO_PERM_READ, ZOO_ANYONE_ID_UNSAFE)

  • struct ACL_vector ZOO_CREATOR_ALL_ACL; //(ZOO_PERM_ALL,ZOO_AUTH_IDS)

The ZOO_OPEN_ACL_UNSAFE is completely open free for all ACL: any application can execute any operation on the node and can create, list and delete its children. The ZOO_READ_ACL_UNSAFE is read-only access for any application. CREATE_ALL_ACL grants all permissions to the creator of the node. The creator must have been authenticated by the server (for example, using “digest” scheme) before it can create nodes with this ACL.

The following ZooKeeper operations deal with ACLs:

  • int zoo_add_auth (zhandle_t *zh,const char* scheme,const char* cert, int certLen, void_completion_t completion, const void *data);

The application uses the zoo_add_auth function to authenticate itself to the server. The function can be called multiple times if the application wants to authenticate using different schemes and/or identities.

  • 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(...) operation creates a new node. The acl parameter is a list of ACLs associated with the node. The parent node must have the CREATE permission bit set.

  • int zoo_get_acl (zhandle_t *zh, const char *path,struct ACL_vector *acl, struct Stat *stat);

This operation returns a node’s ACL info.

  • int zoo_set_acl (zhandle_t *zh, const char *path, int version,const struct ACL_vector *acl);

This function replaces node’s ACL list with a new one. The node must have the ADMIN permission set.

Here is a sample code that makes use of the above APIs to authenticate itself using the “foo” scheme and create an ephemeral node “/xyz” with create-only permissions.

Note

This is a very simple example which is intended to show how to interact with ZooKeeper ACLs specifically. See .../trunk/src/c/src/cli.c for an example of a C client implementation

#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来确定shceme适用的ids.

当一个客户端发送与连接相关的权限认证信息时,handleAuthentication方法被调用. 客户端指定信息对应的方案. ZooKeeper服务将这个信息传递给可插拔认证,这个getScheme需要与客户端传递的scheme相一致. 如果这个信息是错误的,handleAuthentication将会返回一个错误码,或者它会使用cnxn.getAuthInfo().add(new Id(getScheme(), data))与信息相关联.

这个认证插件包含配置和ACLs. 当一个节点设置一个ACL时, ZooKeeper服务将条目中id部分传递给isVlid(String id)方法. 由插件来验证id是否是正确的格式. 例如, ip:172.16.0.0/16一个正确的ID,但是ip:host.com则不是..如果新的ACL包含一个"认证"条目,isAuthenticated被使用来看看将与连接有关联的认证信息添加到ACL.有些schemes不应该包含认证.例如,如果auth已经确定,则客户端IP不应被认为是一个要加入ACL的ID.

当检查ACL时,ZooKeeper会调用matches(String id, String aclExpr)方法. 它需要匹配含有相关ACL条目的客户端认证信息. 为了发现那个应用到客户端的标识, ZooKeeper将会找出每一个标识的scheme,如果有一个对应scheme的客户端认证信息,matches(String id, String aclExpr)方法会在使用了提前被添加到handleAuthentication连接的验证信息的id和用于ACL标识id的aclExpr这两个参数后被调用.认证插件使用自身逻辑和认证设计去确认id是否属于aclExpr.

身份认证插件由两部分组成: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, 只会使用其中一个. 另外所有服务都必须有相同的插件定义,否则使用了插件提供的认证的客户端会在连接一些服务器时存在问题.

一致性保障

ZooKeeper是一个高性能,可扩展服务. 读和写都被设计的十分迅速,虽然读要比写更加迅速. 这是因为在某些读的情况下,Zookeeper可以使用旧的数据,这得益于Zookeeper的一致性保障:

连续性保障

来自客户端的更新将会按照它们的发送顺序被应用.

原子性

更新要么成功,要么失败 -- 没有中间结果.

单独系统视图

客户端将会看到与其连接服务器的相同服务视图.

可靠性

Once an update has been applied, 它将会一直存在,知道一个客户端再次更新. 这个担保有两个推论:

  1. 如果客户端获得一个成功的返回码,这个更新将会被应用. 有些错误(通讯错误,超时错误)客户端不会知道更新是否被应用.我们尽量将错误降到最低,但是保障只能是在返回成功码时有用(这在Paxos算法中被称为单调性情况)

  2. 任何已被客户端看到的更新,读请求或者更新成功的,当从服务异常中恢复过来时也不会回滚.

及时性

客户端的系统视图在一定时间内是最新的.(在命令的数十秒内). 系统的改变在这个范围内既不会被客户端看见,客户端也不会知道服务的运行中断。

使用这些一致性保障很容易构造高级的方法,例如:leader选举、障碍、队列、在客户端构造可撤销的读写锁.(附件不被Zookeeper需要). 更多内容看 Recipes and Solutions.

注意

有时候开发可能会错误的以为Zookeeper有事实上不存在的保障. 比如:

同一时间一致的跨客户端视图

ZooKeeper不保障在同一时刻,两个不同的客户端会拥有完全相同的数据视图. 导致这些情况的原因,比如网络延迟,一个客户端可能在另一个客户端获得改变通知之前执行更新. 考虑到拥有两个客户端的情况,A和B. 如果客户端给znode /a从0赋值为1,然后告诉B去读取/a,B可能仍然读取到旧的值0,取决于它连接的是哪个服务器. 如果客户端A和客户端B取得相同的值是十分重要的,那么客户端B应该在读取数据前执行Zookeeper API方法sync().

所有,Zookeeper不能保障所有服务同时发生改变,但是Zookeeper原始(方法)可以用其来构造更加高级的客户端同步方法. (关于更多信息,参考ZooKeeper Recipes. [tbd:..]).

绑定

Zookeeper客户端类库有两种语言:Java和C. 下面是它们的描述.

Java绑定

有两个库来组成Zookeeper Java绑定: org.apache.zookeeperorg.apache.zookeeper.data. 其他包是构成Zookeeper的内部实现或者服务实现的部分. org.apache.zookeeper.data 包主要由生产类组成,这些类可以仅用作容器.

Zookeeper Java客户端主类是Zookeeper类.  它的两个构造函数不同仅仅在于可选的session id 和 password. ZooKeeper支持跨进程实例的会话恢复. Java程序能够将session id和password保存在一个稳定的存储中,重新启动,可以恢复会话所使用的早先的进程实例.

当一个Zookeeper对象创建时,两个线程同时也被创建.  一个是IO线程,另一个用于事件线程. 所有的IO发生在IO线程(使用Java NIO). 所有事件回调发生在事件线程. 会话维持,例如Zookeeper服务重连和维持心跳都在IO线程. 同步应答也在IO线程中处理. 所有异步应答方法和监听事件处理都在事件线程. 对于这种设计,应注意以下这些事情:

  • 所有异步方法调用和监听者回调都是顺序的,一次一个. 调用者可以进行它们希望的任何处理,但是在这个时间内不会处理任何其他回调.

  • 回调不会阻塞IO线程的处理或者同步调用的处理.

  • 同步调用可能不会按照顺序返回. 例如,假设一个客户端要做以下处理: 一个请求异步读取节点/a,并且设置监听器,然后再读回调完成时,发起一个同步读.(这也许不是个好办法,但是不是非法的,这只是个简单的例子)

    注意,如果在异步读和同步读间发生了变化,在同步读响应之前,客户端库会接收到一个关于/a改变的事件,但是因为异步读回调是会阻塞事件队列,在观察者事件处理之前,同步读将会返回/a的新值.

最后, 与shutdown相关联的是清晰的:一旦一个Zookeeper对象关闭或者接收到一个异常事件(SESSION_EXPIRED 和 AUTH_FAILED), Zookeeper对象会变为无效. 关闭后,其两个线程也会关闭,任何在Zookeeper句柄上的操作都会变得不可预测,应该避免这种情况.


C绑定有一个单线程库和多线程库。多线程库用起来最简单,并且与JavaAPI很相似。这个库将创建一个IO线程和事件分发器线程,后者处理连接维护和回调。单线程库允许ZooKeeper用在事件驱动应用程序中,此时,它暴露事件循环,这与多线程库中的一样。

程序包包含两个共享库:zookeeper_st和zookeeper_mt,前者仅提供异步API和回调函数,它们可以整合到应用程序的事件循环中。这个库存在的唯一理由是它是针对那些不支持pthread库或pthread库运行不稳定的平台(即FreeBSD 4.x)。其他情况下,程序开发者应链接zookeeper_mt,它同时支持同步和异步API。


安装

如果你从Apache库中通过check-out构建客户端, 参考以下步骤.如果你是通过从Apache中下载源码的方式构造,则调到步骤3.

  1. 在Zookeeper的顶级目录(.../trunk)运行ant compile_jute. 这将会在.../trunk/src/c创建一个名为"generated"的目录.

  2. 进入至目录.../trunk/src/c 运行autoreconf -if 需要引导程序 autoconf, automakelibtool. 确保你已经有 autoconf version 2.59 或者已经安装. 跳至步骤4.

  3. 如果你通过项目源码构建,unzip/untar源码包,cd到zookeeper-x.x.x/src/c目录.

  4. 运行 ./configure <your-options> 去生成markfile.这里有一些configure支持的的选项,可以用于这些步骤:

    • --enable-debug

      编译器选项,运行优化和debug信息. (缺省是不运行的.)

    • --without-syncapi

      不支持同步API;zookeeper_mt库不构建. (缺省是支持.)

    • --disable-static

      不构建静态库. (缺省是支持)

    • --disable-shared

      不构建共享库 (缺省是支持)

    Note

    关于运行的配置信息,参考INSTALL.

  5. 运行make或make install来构建类库和安装它们.

  6. 为Zookeeper生成doxygen文档, run make doxygen-doc. 所有文件将会被放置在一个叫docs的新目录. 默认情况下,这个命令只生成HTML. 关于其他文档格式信息,运行 ./configure --help

Building Your Own C Client

In order to be able to use the ZooKeeper API in your application you have to remember to

  1. Include ZooKeeper header: #include <zookeeper/zookeeper.h>

  2. If you are building a multithreaded client, compile with -DTHREADED compiler flag to enable the multi-threaded version of the library, and then link against against the zookeeper_mt library. If you are building a single-threaded client, do not compile with -DTHREADED, and be sure to link against the zookeeper_st library.

Note

See .../trunk/src/c/src/cli.c for an example of a C client implementation

构建积木:指导Zookeeper操作.

这部分调查了开发人员对Zookeeper服务的全部操作.  与本手册前面章节相比,它是更底层的信息,但比ZooKeeperAPI参考信息高,它包含如下主题:

处理错误

JAVA和C客户端都可能报错.  Java客户端是通过抛出KeeperException,在异常中调用code()将会返回明确的错误码. C客户端返回一个明确的ZOO_ERRORS枚举错误码. 对于这两种语言,API回调都由结果码指定结果. 关于更多可能错误详细信息,查看API文档(java是Javadoc,C是doxygan).

连接ZooKeeper

读取操作

写操作

处理观察者

其他Zookeeper操作

程序结果,简单的例子

[tbd]

陷阱:常见问题和解决方案

所以你现在知道Zookeeper. 它是快速、简单、你得应用程序工作,但是等等... 有些事情不对.  这里有些陷阱会导致Zookeeper用户错误:

  1. 如果你使用观察者,你必须关注连接观察者事件. 当一个Zookeeper客户端与服务器断线, 在重连之前你不会接收到任何改变的通知. 如果你正在监听一个znode节点存在, 在你连接断开期间,如果znode是创建或者删除的你将会失去该事件.

  2. 你必须测试Zookeeper服务是否失效. ZooKeeper服务只要大多数是有效的,就可以有效的. 这个问题:你得应用是否能处理它? 现实中,一个客户端连接Zookeeper可能断开. (对于连接丢失,ZooKeeper服务异常和网络异常是常见原因.) ZooKeeper客户端类库关注恢复你得连接并且让你知道在何时发生.  但是你必须确保你恢复你的状态和任何失败的请求. 在测试环境下而不是正式环境下. 在由多个服务组成的Zookeeper服务商测试,使机器重启.

  3. 客户单使用的Zookeeper服务列表必须和每一个Zookeeper服务列表一致.  如果客户端使用Zookeeper服务真实列表的子集,依然可以工作,但是不是最优, 但是如果客户端列表有不存在zookeeper集群中的列表则不会工作.

  4. 小心放置你得事务日志. Zookeeper性能关键部分是事务日志. Zookeepr在应答之前,必须同步事务到媒体中.  一个专门的事务日志设备是优秀性能的关键. 日志放置在繁忙的服务将会影响性能. 如果你仅有一个存储设备,将追踪文件放置在NFS然后增加snapshotCount; 它不会消除问题,但是会降低它.

  5. 设置合适的Java堆大小. 最重要的是避开交换. 没必要的磁盘访问肯定会降低你得性. 记住,在Zookeeper中,每一个事情都是顺序的,如果有一个请求访问了磁盘,队列中的其他请求也将访问磁盘.

    为了避开交换,你应该设置你得堆大小为物理内存大小减去你得操作系统和缓存大小. 确定你配置堆大小的最佳方式是去运行负载测试. 如果因为某些原因你不能这样做,那么对你的估算保守一些,选择一个刚好低于内存交换的值.例如,在4G的机器上,选择3G作为大小的起点.


除了正式的官方文档外,还有一些其他的信息源,供ZooKeeper开发者参考。

ZooKeeperWhitepaper [url待定]

它明确地讨论了ZooKeeper的设计和性能,由Yahoo!Research编写

API Reference[url待定]

ZooKeeperAPI的完整参考

ZooKeeper Talkat the Hadoup Summit 2008

Yahoo!Research的Benjamin Reed讲的一个介绍ZooKeeper的视频

Barrier andQueue Tutorial

Theexcellent Java tutorial by Flavio Junqueira编写的一个优秀的Java教程,用ZooKeeper实现了一个简单的壁垒(barriers)以及producer-consumer 模式的队列。

ZooKeeper - AReliable, Scalable Distributed Coordination System

ToddHoff (07/15/2008)写的一篇文章

ZooKeeperRecipes

采用ZooKeeper来实现的各种同步方案(模拟级别的讨论):事件处理,队列,锁及两段提交。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值