本文Github地址:https://github.com/sundiontheway/zookeeper-guide-cnhtml
本文假设你已经具备必定分布式计算的基础知识。你将在第一部分看到如下内容:java
ZooKeeper数据模型node
ZooKeeper Sessionsgit
ZooKeeper Watchesgithub
一致性保证(Consistency Guarantees)算法
接下来的4小节讲述了程序开发的实际应用:数据库
建立模块——ZooKeeper操做指引express
编程语言接口apache
简单示例演示程序的结构编程
常见问题和故障
本文的附录中包含和ZooKeeper相关的有用信息。
ZooKeeper有一个相似分布式文件系统的命名体系。区别在于Zookeeper每一个一个节点或子节点均可以拥有数据。节点路径是一个由斜线分开的绝对路径,注意没有相对路径。只要知足下面要求的unicode字符均可以做为节点路径:
空字符不能出如今路径名
不能出现如下字符: \u0001 - \u0019 and \u007F - \u009F
如下字符不容许使用: \ud800 -uF8FFF, \uFFF0-uFFFF, \uXFFFE - \uXFFFF (where X is a digit 1 - E), \uF0000 - \uFFFFF
字符"."能够做为一个名字的一部分, 可是"."和".."不能单独做为相对路径使用, 如下用法都是无效的: "/a/b/./c"或者"/a/b/../c"
"zookeeper"为保留字符
ZooKeeper树结构中的节点被称为znode。各个znode维护着一组用来标记数据和访问权限发生变化的版本号。这些版本号组成的状态结构 具备时间戳。Zookeeper使用版本号和时间戳来验证缓存状态,调整更新。 每次znode中的数据发生变化,znode的版本号增长。例如,每当一个客户端恢复数据时,它就接收这个版本的数据,而当一个客户端提交了更新或删除记 录,它必须同时提供这个znode当前正在发生变化的数据的版本。若是这个版本和目前真实的版本不匹配,则提交无效。 __提示,在分布式程序中,一个字节点能够表明一个通用的主机,服务器,集群中的一员,客户端程序等。可是在Zookeeper中,znode表明数据节 点,Servers表明组成了Zookeeper服务的机器; quorum peers refer to the servers that make up an ensemble; 客户端表明任何使用ZooKeeper服务的主机或程序。
znode做为对程序开发来讲最重要的信息,有几个特性须要特别关注下:
Watches 客户端能够在znode上设置Watch。znode发生的变化会触发watch而后清除watch。当一个watch被触发,Zookeeper给客户端发送一个通知。更多关于watch的内容请查看ZooKeeper Watches一节。
数据存取 命名空间中每一个znode中的数据读写是原子操做。读操做读取znode中的全部数据位,写操做则替换全部数据。每一个节点都有一个访问权限控制表 (ACL)来标记谁能够作什么。 zookeeper不是设计成普通的数据库或大型对象存储的。它是用来管理coordination data。coordination data包括配置文件、状态信息、rendezvous等。这些数据结构的一个共同特色就是相对较小——以千字节为准。Zookeeper的客户端和服务 会检查确保每一个znode上的数据小于1M,实际平均数据要远远小于1M。 大规模数据的操做会引起一些潜在的问题而且延长在网络和介质之间传输的时间。若是确实须要大型数据的存储,那么能够采用如NFS或HDFS之类的大型数据 存储系统,亦或是在zookeeper中存储指向存储位置的指针。
临时节点(Ephemeral Nodes) zookeeper还有临时节点的概念,这些节点的生命周期依赖于建立它们的session是否活跃。session结束时节点即被销毁。也因为这种特性,临时节点不容许有子节点。
序列节点——命名不惟一 当你建立节点的时候,你会须要zookeeper提供一组单调递增的计数来做为路径结尾。这个计数对父znode是惟一的。用%010d
的格式——用0来填充的10位数(计数如此命名是为了简单排序)。例如"0000000001",注意计数器是有符号整型,超过表示范围会溢出。
zookeeper有不少记录时间的方式:
Zxid(ZooKeeper Transaction Id): zookeeper每次发生改动都会增长zxid,zxid越大,发生的时间越靠后。
Version numbers: 对znode的改动会增长版本号。版本号包括version (znode上数据的修改数), cversion (znode的子节点的修改数), aversion (znode上ACL(权限)的修改数)。
Ticks : 多个server构成zookeeper服务时,各个server用ticks来标记如状态上报、链接超时等事件。ticks time还间接反映了session超时的最小值(两次tick time);若是客户端请求的最小session timeout低于这个最小值,服务端会通知客户端最小超时置为这个最小值。
Real time : 除了每次znode建立或改动时候将时间戳记录到状态结构中外,zookeeper不使用时钟时间。
存在于znode中的状态结构,由如下各个部分组成:
czxid - znode建立产生的zxid
mzxid - znode最后一次修改的zxid
ctime - znode建立的时间的绝对毫秒数
mtime - znode最后一次修改的绝对毫秒数
version - znode上数据的修改数
cversion - 子节点修改数
aversion - znode的ACL修改数
ephemeralOwner - 临时节点的全部者的session id。若是此节点非临时节点,该值为0
dataLength - znode的数据长度
numChildren - znode子节点数
客户端经过建立一个handle和服务端创建session链接。一旦建立完成,handle就进入了CONNECTING状态,客户端库尝试链接一台构成zookeeper的server,届时进入CONNECTED状态。一般状况下操做会介于这两种状态之间。 一旦出现了不可恢复的错误:如session停止,鉴权失败或者应用直接结束handle,则handle会进入到CLOSED状态。下图是客户端的状态转换图:
应用在建立客户端session时必须提供一串逗号分隔的主机号:端口号,每对主机端口号对应一个ZooKeeper的 server(如:"127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002"),客户端库会尝试链接任意一台服务 器,若是链接失败或是客户端主动断开链接,客户端会自动继续与下一台服务器链接,直到链接成功。
3.2.0版本新增内容: 一个新的操做“chroot”能够添加在链接字符串的尾部,用来指明客户端命令运行的根目录地址。相似unix的chroot命令,例如: "127.0.0.1:4545/app/a" or "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服务的用户能够设置不一样的根目录。
当客户端得到和zookeeper服务链接的handle时,zookeeper会建立一个Zookeeper session分配给客户端,用一个64-bit数字表示。一旦客户端链接了其余服务器,客户端必须把这个session id也做为链接握手的一部分发送。出于安全目的,zookeeper给session id建立一个密码,任何zookeeper服务器均可以验证密码。 当客户端建立session时密码和session id一块儿发送到客户端来,当客户端从新链接其余服务器时,同时要发送密码和session id。
zookeeper客户端库里有一个建立zookeeper session的参数,叫作session timeout(超时),用毫秒表示。客户端发送请求超时,服务端在超时范围内响应客户端。session超时最小为2个ticktime,最大为20个 ticktime。zookeeper客户端API能够协调超时时间。 当客户端和zookeeper服务器集群断开时,它会搜索session建立时的服务器列表。最后,当至少一个服务器和客户端从新创建连 接,session或被从新置为"connected"状态(超时时间内从新链接),或被置为"expired(过时)"状态(超出超时时间)。不建议在 断开链接后从新建立session。ZK客户端库会帮你从新链接。特别地,咱们将启发式学习模式植入客户的库中来处理相似“羊群效应”等问题。只有当你的 session过时时才从新建立(托管的)。 session过时的状态转换图示例同过时session的watcher:
'connected' : session正确建立,客户端和服务集群正常链接
.... 客户端从服务器集群断开
'disconnected' : 客户端失去和服务器集群的链接
.... 过了一段时间, 超过了集群断定session过时的超时时间, 客户端并无发觉本身和服务集群断开了链接
.... 又过一段时间, 客户端恢复了同集群的网络链接
'expired' : 最终客户端从新连上集群,而后被通知已经到期
另外一个session创建时zookeeper须要的参数是默认watcher(监视者)。在客户端发生任何变化时,watcher都会发出通知。 例如客户端失去和服务器的链接、客户端session到期等。watcher默认的初始状态是disconnected。(也就是说任何状态改变事件都由 客户端库发送到watcher)。当新建一个链接时,第一个发送给watcher的事件一般就是session链接事件。
客户端发送请求会使session保持活动状态。客户端会发送ping包(译者注:心跳包)以保持session不会超时。Ping包不只让服务端 知道客户端仍然活动,并且让客户端也知道和服务端的链接没有中断。Ping包发送时间正好能够判断是否链接中断或是从新启动一个新的服务器链接。
和服务器的链接创建成功,当一个同步或异步操做执行后,有两种状况会让客户端库判断失去链接:
应用在已经失效的session上调用了一个操做时
zookeeper服务器有未完成的操做,客户端这时会断开链接。即服务器有未完成的异步调用时
3.2.0版本新增内容 —— SessionMovedException 一个客户端没法查看的内部异常SessionMovedException。这个异常发生在服务端收到一个请求,这个请求的session已经在另外一个服 务器上从新链接。发生这种状况的缘由一般是客户端发送完请求后,因为网络延时,客户端超时从新和其余服务器创建链接,当请求包到达第一台服务器时,服务器 发现session已经移除并关闭了和客户端的链接。客户端通常不用理会这个问题,可是有一种状况值得注意,当两台客户端使用事先存储的session id和密码试图建立同一个链接时,第一台客户端重建链接,第二台则会被中断。
全部zookeeper的读操做——getData(), getChildren(), exists()——均可以设置一个watch。Zookeeper的watch的定义是:watch事件是一次性触发的,发送到客户端的。在监视的数据 发生变化时产生watch事件。如下三点是watch(事件)定义的关键点:
一次性触发: 当数据发生变化时,一个watch事件被发送给客户端。例如,若是一个客户端作了一次getData("/znode1", true)
而后节点/znode1
发生数据变化或删除,这个客户端将收到/znode1
的watch事件。若是/znode1
继续发生改变,不会再有watch发送,除非客户端又作了其余读操做产生了新的watch。
发送给客户端: 这就意味着,事件在发往客户端的过程当中,可能没法在修改操做成功的返回值到达客户端以前到达客户端。watch是异步发送给watchers的。 zookeeper提供一种保证顺序的方法:客户端在第一次看到某个watch事件以前不可能看到产生watch的修改的返回值。网络延时或其余因素可能 致使不一样客户端看到watch并返回不一样时间更新的返回值。关键的一点是,不一样的客户端看到发生的一切都必须是按照相同顺序的。
watch依附的数据: 这是说改变一个节点有不通方式。用好理解的话说,zookeeper维护两组watch:data watch和child watch。getData()和exists()产生data watch。getChildren()引发child watch。watch根据数据返回的种类不一样而不一样。getData()和exists()返回关于节点的数据信息,而getChildren()返回 子节点列表。所以setData()触发某个znode的data watch(假设事件成功)。create()成功会触发被建立的znode上的data watch和在它父节点上的child watch。delete()成功会触发data watch和child watch(由于没有了子节点)。
watch在客户端已链接上的服务器里维护,这样能够保证watch轻量便于设置,维护和分发。当客户端链接了一台新的服务器,watch会在任何 session事件时触发。当断开和服务器的链接时,watch不会触发。当客户端从新链接上时,任何以前注册过的watch都会从新注册并在须要的时候 被触发。通常来讲这一切都是透明的。只有一种可能会丢失watch:当一个znode在断开和服务器链接时被建立或删除,那么判断这个znode存在的 watch因未建立而找不到。
ZooKeeper如何保证watch可靠性
zookeeper有以下方式:
watch与其余事件、watch、异步回复保持有序,Zookeeper客户端库确保任何分发都是有序的。
客户端会在某个监视的znode数据更新以前看到这个znode的watch事件。
watch事件的顺序由Zookeeper服务端观察到的更新顺序决定。
watch注意事项
watch是一次性触发的;若是你收到watch事件后还想继续获得后续更改的通知,你须要再生成(设置)一个watch。
因为watch是一次性触发,你在获取某事件和发送新的请求来获得watch这个操做之间,没法确保观察到Zookeeper中那个节点在这期间 的全部修改。你要准备好应付这种状况出现:znode会在收到事件和再次设置新事件(译者注:对节点的操做)之间发生了屡次修改。(你可能并不关心,可是 必须了解这可能发生)
watch对象,或是function/context对,只会在获得通知时触发一次。例如,若是一个watch对象同时用来监控某个目标文件是否存在和监听getData(),以后那个文件被删除了。那么这个watch对象只会触发一次文件删除事件通知。
若是你断开了同服务器的链接(例如服务器挂了),你在从新连上以前得不到任何watch。出于这种缘由,session event会被发送给全部重要的watch handler。可使用session事件进入安全模式:当断开链接时你收不到任何事件,这样你的进程能够在那种模式下稳健地执行。(译者注:能够经过 发送session event使客户端进入安全模式(伪断开链接状态),在安全模式你能够修改代码而不用担忧程序收到事件通知)
zookeeper使用ACL来控制对znode(zookeeper的数据节点)的访问权限。ACL的实现方式和unix的文件权限相似:用不一样 位来表明不一样的操做限制和组限制。与标准unix权限不一样的是,zookeeper的节点没有三种域——用户,组,其余。zookeeper里没有节点的 全部者的概念。取而代之的是,一个由ACL指定的id集合和其相关联的权限。 注意,一个ACL只从属于一个特定的znode。对这个znode子节点也是无效的。例如,若是/app
只有被ip172.16.16.1的读权限,/app/status
有被全部人读的权限,那么/app/status
能够被全部人读,ACL权限不具备递归性。 zookeeper支持插件式认证方式,id使用scheme:id
的形式。scheme
是id对应的类型方式,例如ip:172.16.16.1
就是一个地址为172.16.16.1的主机id。 当客户端链接zookeeper而且认证本身,zookeeper就在这个与客户端的链接中关联全部与客户端一致的id。当客户端访问某个znode时,znode的ACL会从新检查这些id。ACL的表达式为(scheme:expression,perms)
。expression
就是特殊的scheme,例如,(ip:19.22.0.0/16, READ)
就是把任何以19.22开头的ip地址的客户端赋予读权限。
ZooKeeper支持下列权限:
CREATE:容许建立子节点
READ:容许得到节点数据并列出全部子节点
WRITE:容许设置节点上的数据
DELETE:容许删除子节点
ADMIN:容许设置权限
CREATE和DELETE操做是更细的粒度上的WRITE操做。有一种特殊的状况:
你想要A得到操做zookeeper上某个znode的权限,可是不能够对其子节点进行CREATE和DELETE。
只CREATE不DELETE:某个客户端在上一级目录上经过发送建立请求建立了一个zookeeper节点。你但愿全部客户端均可以在这个节点上添加,可是只有建立者能够删除。(这就相似于文件的APPEND权限)
zookeeper没有文件全部者的概念,但有ADMIN权限。在某种意义上说,ADMIN权限指定了所谓的全部者。zookeeper虽然不支持 查找权限(在目录上的执行权限虽然不能列出目录内容,却能够查找),但每一个客户端都隐含着拥有查找权限。这样你能够查看节点状态,但仅此而已。(这有个问 题,若是你在不存在的节点上调用了zoo_exists(),你将无权查看)
ZooKeeper有下列内建模式:
world 有独立id,anyone,表明任何用户。
auth 不使用任何id,表明任何已经认证过的用户
digest 以前使用了格式为username:pathasowrd
的字符串来生成一个MD5哈希表做为ACL ID标识。在空文档中发送username:password
来完成认证。如今的ACL表达式格式为username:base64
, 用SHA1编码密码。
ip 用客户端的ip做为ACL ID标识。ACL表达式的格式为addr/bits
,addr中最有效的位匹配上主机ip最有效的位。
zookeeper运行于复杂的环境下,有各类不一样的认证方式。所以zookeeper拥有一套插件式的认证框架。内建认证scheme也是使用这 套框架。 为了便于理解认证框架的工做方式,你首先要了解两种主要的认证操做。框架首先必须认证客户端。这步操做一般在客户端链接服务器的同时完成而且将从客户端发 过来的(或从客户端收集来的)认证信息关联这次链接。认证框架的第二步操做是在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适用于该scheme。 当客户端发送与链接关联的认证信息时,handleAuthentication被调用。客户端指定和认证信息相应的模式。zookeeper把信息传给认证插件,认证插件的getScheme
匹配scheme。实现handleAuthentication
的方法一般在判断信息错误后返回一个error,或者在确认链接后使用cnxn.getAuthInfo().add(new Id(getScheme(), data))
认证插件在设置和ACL中都有涉及。当对某个节点设置ACL时,zookeeper服务器会传那个条目的id给isValid(String id)
方法。插件须要判断id的链接来源。例如,ip:172.16.0.0/16
是有效id,ip:host.com是无效id。若是新的ACL包括一个"auth"条目,就用isAuthenticated
判断该scheme的认证信息是否关联了链接,是否能够被添加到ACL中。一些scheme不会被包含到auth中。例如,若是auth已经指定,客户端的ip地址就不做为id添加到ACL中。 在检查ACL时zookeeper有一个matches(String id, String aclExpr)
方法。ACL的条目须要和认证信息相匹配。为了找到和客户端对应的条目,zookeeper服务器寻找每一个条目的scheme,若是对某个scheme有那个客户端的认证信息,matches(String id, String aclExpr)
会被调用并传入两个值,分别是事先由handleAuthentication
加入链接信息中认证信息的id,和设置到ACL条目id的aclExpr
。认证插件用本身的逻辑匹配scheme来判断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
,只有一个会被使用。一样,全部服务器都必须统一插件定义,不然客户端用插件提供的认证schemes链接服务器时会出错。
ZooKeeper是一个高性能,可扩展的服务。读和写操做都很是快速。之因此如此,全由于zookeeper有数据一致性的保证:
顺序一致性 客户端的更新会按照它们发送的次序排序。
原子性 更新的失败或成功,都不会出现半个结果。
单独系统镜像 无论客户端连哪一个服务器,它看来都是同一个。
可靠性 一旦更新生效,它就会一直保存到下一次客户端更新。这就有两个推论:
若是客户端获得成功的返回值,说明更新生效了。在一些错误状况下(链接错误,超时等)客户端不会知道更新是否生效。虽然咱们使失败的概率最小化,可是也只能保证成功的返回值状况。(这就叫Paxos算法的单调性条件)
客户端能看到的更新,即便是渡请求或成功更新,在服务器失败时也不会回滚。
时效性 客户端看到的系统状态在某个时间范围内是最新的(几十秒内),任何系统更改都会在该时间范围内被客户端发现。不然客户端会检测到断开服务。
用这些一致性保证能够在客户端中构造出更高级的程序如 leader election, barriers, queues, read/write revocable locks(无须在zookeeper中附加任何东西)。更多信息Recipes and Solutions
zookeeper不存在的一致性保证: 多客户端同一时刻看到的内容相同 zookeeper不可能保证两台客户端在同一时间看到的内容老是同样,因为网络延迟等缘由。假设这样一个场景,A和B是两个客户端,A设置节点/a下的 值从0变为1,而后让B读/a,B可能读到旧的数据0。若是想让A和B读到一样的内容,B必须在读以前调用zookeeper接口中的sync()方法。
下面是一些常见的陷阱:
若是你使用watch,你必须监控好已经链接的watch事件。当ZooKeeper客户端断开和服务器的链接,直到从新链接上这段时间你都收不到任何通知。若是你正在监视znode是否存在,那么你在断开链接期间收不到它建立和销毁的通知。
你必须测试ZooKeeper故障的状况。在大多数服务器均可用的状况下,ZooKeeper是能够维持工做的。关键问题是你的客户端程序是否能 察觉到。在实际状况下,客户端与ZooKeeper的链接有可能中断(多数时候是由于Zookeeper故障或网络中断)。Zookeeper的客户端库 关注于如何让你从新链接而且知道发生了什么。可是同时你也必须确保可以恢复你的状态和发送失败的请求。努力在测试库里测出这些问题,而不是在产品里——用 几台服务器组成的zookeeper集群测试这个问题,尝试让它们重启。
客户端维护的服务器列表必须和现有的服务器列表一致。若是客户端的列表是现有服务器列表的子集,还能够在非最佳状态工做,可是若是客户端列表里的服务器不在现有集群里你就悲剧了。
注意存放事务日志的位置。性能评测最重要的部分就是日志,ZooKeeper会在回复响应以前先把日志同步到磁盘上。为了达到最佳性能,首选专用 的磁盘来存日志。把日志放在繁忙的磁盘上会下降效率。若是你只有一个磁盘,就把记录文件放在NFS上而后增长SnapshotCount。这样虽然没法完 全解决问题,但能缓解一些。
正确地设置你java的堆空间大小。这是避免频繁交换的有效措施。无用的访问磁盘会让你的效率大打折扣。记住,在ZooKeeper中,一切都是有序的,若是一个服务器访问了磁盘,全部的服务器都会同步这个操做。
其余资料连接请自行官网查看。