简介
对于想要利用ZooKeeper的协调服务来创建一个分布式应用的开发人员来说,这篇文章提供了指导。包含了一些概念和实际性操作的信息。
这篇文章的前四个章节介绍了各种ZooKeeper的概念,这对理解ZooKeeper是怎么工作的是必须的。没有包含源代码,但是它假设你对分布式处理有关的问题比较熟悉。这四个章节是:
ZooKeeper数据模型
ZooKeeper 会话
ZooKeeper Watches
一致性保证
随后的四个章节提供了实际的编程信息,他们是:
构建块:ZooKeeper操作指南
绑定
程序结构,简单的例子
陷阱:常见问题和故障排查
ZooKeeper数据模型
ZooKeeper有一个层级命名空间,和一个分布式文件系统非常相似。唯一的不同是每个节点可以有关联的数据,子节点也是。就像有一个文件系统并且允许文件是一个目录。一个规范的、绝对的、斜杠分隔的路径来表示一个节点路径,没有相对路径。任何符合下列约束的的unicode字符可以被使用:
null字符串(\u0000)不能是一个路径名称。
下列字符不能被使用,因为不能很好的被展示:\u0001 - \u001F 和 \u007F - \u009F。
下列字符是不允许的:\ud800 - uF8FF, \uFFF0 - uFFFF。
“.”字符可以作为另一个名字被使用,但是“.”和“..”不能单独使用来表示一个节点路径,因为ZooKeeper不使用相对路径,下列是无效的:”/a/b/./c”或者 “/a/b/../c”。
“zookeeper”标记被保留。
ZNodes
在ZooKeeper树中的每个节点被称为一个znode。Znodes包含了一个stat数据结构,这个数据结构包括了数据变更的版本号、acl变更。stat数据结构也有时间戳,版本号和时间戳一起来允许ZooKeeper校验缓存和协调更新。每当一个znode的数据改变,版本号就会增加。例如:当一个客户端取得数据,它同样也接受数据的版本。并且,当一个客户端执行一个更新或删除操作,它必须提供数据的版本号。如果客户端提供的的版本号和实际的版本号不匹配,更新操作将会失败。
注意 在分布式应用应用中,node一词可以用来表示一台主机、一个服务器、集中中的一个、一个客户端进程等等。早ZooKeeper这边文档中,znodes 表示一个数据节点,Servers表示组成ZooKeeper服务中的机器,quorum peers 表示组成集合的机器,客户端表示使用一个ZooKeeper服务的主机或进程。
Znodes是一个程序员访问的主要实体,有许多在这里值得提到特性:
Watches
客户端可以在znodes上设置监听器,znode的改变触发这个监听器然后清空这个监听器。当一个监听器被触发,ZooKeeper发送给客户端一个通知。更多信息可以查看ZooKeeper Watches章节。
数据访问
每个znode的上存储的数据读写都是原子的,读操作取出所有的和这个znode有关的所有数据,写操作替换所有的数据。每个节点有一个访问权限列表(ACL)来限制谁可以做这些事情。
ZooKeeper没有被设计成一个一般的数据库或一个大型对象存储。它管理协调数据,数据可以是配置、状态信息、集合点等的形式。各种各样的数据有一个共同的属性就是他们都很小:以千字节为标准。ZooKeeper客户端和服务器有一个健康检查来确保znodes的数据少于1M,但是数据平均应该更小。操作较大的数据将导致一些操作花费更多的时间,并且会影响一些操作的延迟,因为在网络和存储媒介中移动更多的数据将需要额外的时间。如果需要存储大数据,通常的处理是把数据存储在一个大容量存储系统中,并把存储位置的指针存储到ZooKeeper上。
临时节点
ZooKeeper也临时节点的概念。这些znodes存活的时间和创建这个节点的会话有效期是一样的。当会话结束,节点被删除。因为这种临时节点的特性,临时节点不允许有子节点。
顺序节点——唯一名称
当创建一个节点的时候,也可以请求ZooKeeper在路径后面增加一个自增的计数器。对父节点来说,这个计数器是 唯一的。计数器是%010d的格式——是一个十位数,比如:<path>0000000001。
查看Queue Recipe使用这个特性的示例,注意:这个计数器用来存储下一个序列号是一个4字节的数,当增加到2147483647 之后,计数器会溢出。
ZooKeeper中的时间
ZooKeeper以多种方式跟踪时间:
Zxid:ZooKeeper状态的每次变化都接收一个zxid(ZooKeeper事务id)形式的标记。这个展示了所有的ZooKeeper的变更顺序。每次变更会有一个唯一的zxid,如果zxid1小于zxid2说明zxid1在zxid2之前发生。
Version numbers:节点的每次变化都会引起这个节点版本号之一的一次增加。这三个版本号是:version(一个节点的数据变化次数),cversion(一个节点的子节点变化次数),aversion(一个节点的ACL 变化次数)。
Tricks:当使用多个ZooKeeper服务,服务器使用ticks来确定事件的时间,比如说状态上传、会话超时、连接超时等。这个tick时间仅仅通过最小会话超时时间间接的暴露出来;如果一个客户端请求会话的超时时间小于最小超时时间,服务器将会告诉客户端实际的会话超时时间是最小超时时间。
Real Time:ZooKeeper不使用实时、时钟时间。除了把时间戳放在stat结构中。
ZooKeeper Stat 结构
每个节点的Stat结构由下列字段组成:
czxid:该数据节点被创建时的事务id。
mzxid:该节点最后一次被更新时的事务id。
ctime:节点被创建时的时间。
mtime:节点最后一次被更新时的时间。
version:这个节点的数据变化的次数。
cversion:这个节点的子节点 变化次数。
aversion:这个节点的ACL变化次数。
ephemeralOwner:如果这个节点是临时节点,表示创建者的会话id。如果不是临时节点,这个值是0。
dataLength:这个节点的数据长度。
numChildren:这个节点的子节点个数。
ZooKeeper会话
通过使用一种语言绑定来创建服务端的句柄,一个ZooKeeper客户端可以和ZooKeeper服务创建会话。一旦创建,句柄开始在CONNECTING 状态,客户端库尝试连接组成ZooKeeper服务中的其中一个服务器并且切换到CONNECTED状态。在正常的操作期间将会是这两种状态之一。如果一个不可恢复的错误发生了,比如说会话过期或授权失败,或者如果应用显示地关闭了句柄,句柄将会到CLOSED状态。下面的图展示了一个ZooKeeper客户端可能的状态转变。
为了创建一个客户端会话,应用程序代码必须提供一个连接字符串列表以逗号分隔开,主机:端口号成对出现,每个都相当于一个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”后缀可以被加在连接字符串后面,这会运行客户端命令导致所有的路径都和这个跟路径相关。如果使用像下面的示例:”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”作为跟路径,并且所有的路径都与这个根路径相关,比如getting、setting等。”/foo/bar” 将导致操作在”/app/a/foo/bar”(从服务端的观点来看)。这个特性在多租户下面是非常也有用的,ZooKeeper服务的每个用户可以有不同的根路径。这让再使用变得非常简单,因为每个用户都可以编写代码让他的应用好像在”/”根路径下,但实际的位置能在部署时决定。
当一个客户端从ZooKeeper服务得到一个句柄,ZooKeeper创建了一个会话,表现为一个64位的数字,并把 它分配给客户端。如果客户端连接到一个不同的服务端,在连接握手的时候它将发送这个会话id。作为一个安全措施,服务端给会话id创建了一个密码,让服务端能够校验。当客户端建立会话的时候,这个密码随着会话id一起发送给客户端。每当客户端与一个新的服务端恢复会话的时候,密码会随着会话id一起发送过去。
客户端调用创建会话的时候有一个参数是会话超时时间(毫秒),客户端发送一个要求的超时时间,服务端回复一个他能给客户端的超时时间。当前实现要求超时时间至少是2倍的tickTime,最大是20倍的tickTime。ZooKeeper客户端API允许使用一个协商的超时时间。
当一个客户端从ZK服务集群成为分区,它将开始寻找在会话创建时期指定的服务端列表。最终,当客户端和至少一个服务端联通重新建立的时候,会话要么转变成“connected”状态(如果在会话超时时间内恢复连接),要么转变成“expired”状态(如果在超时时间之外恢复连接)。在断开时创建一个新的会话是不可取的。ZK客户端库将处理连接。尤其是客户端内部有方法来处理像“羊群效应”之类的事情。仅仅在你被通知会话过期的时候去创建一个新的会话。
ZooKeeper集群自己管理会话过期,而不是由客户端管理。当ZK客户端和一个集群建立会话,它提供一个“超时时间”。这个值被集群使用来决定客户端的会话是否过期。当集群不能在指定的会话超时时间内从客户端收到信息,过期发生。在会话过期期间,集群将删除由这个会话创建的所有的临时节点,并且立即通知连接的客户端这个改变。此时,会话过期的客户端依然和集群式断开的,它不会收到通知直到它能和集群重新建立连接。这个客户端将保持断开状态直到和集群的TCP连接重新建立,并且在这个时候,过期会话的监听将会收到“会话过期”通知。
对于一个过期的会话,监听器所看到的状态转变:
“connected”:会话被建立,并且客户端能和集群交流
……客户端从集群被分割
“disconnected”:客户端与集群丢失了联系
……时间流逝,在超时时间之后,集群已经让这个会话过期,而客户端没看到什么,因为它已经从集群断开连接了
……时间流逝,客户端恢复网络和集群联通
“expired”:最后客户端与集群重新连接,然后收到过期的通知
ZooKeeper会话建立的另一个参数是默认监听器。当客户端的一些状态改变发生,监听器会收到通知。比如如果客户端丢失与服务端的连接,客户端将会收到通知,或客户端的会话到期等。这个监听器应该考虑初始状态到断开连接。对于一个新的连接,第一给发给监听器的事件就是会话连接事件。
客户端通过发送请求保持会话存活。如果会话在一段时间内空闲将会导致会话超时,客户端将会发送PING请求保持会话存活。这个PING请求不仅仅让ZooKeeper服务端知道客户端是存活的,而且让客户端检查它的和ZooKeeper 服务端的连接也是存活的。PING的时间是足够保守的合理时间,来发现死掉的连接和一个新的服务端重新连接。
一旦成功建立一个到服务端的连接,当客户端发生connectionloss异常 时有两种基本的情况,在执行一个同步或者非同步的操作时:
应用调用一个操作,但是会话不再存活。
当等待一个操作的时候ZooKeeper客户端从服务端断开连接,比如说:等待一个异步调用。
3.2.0新增——SessionMovedException。有一个内部的异常,通常不会被客户端发现,被称为SessionMovedException。一个已经连接的会话但是重新连接到了一个不同的服务器上接收了一个请求,这个异常就会发生。这个错误的正常原因是一个客户端发送了一个请求到一个服务端,但是网络数据包延迟了,所以客户端超时并连接到了一个新的服务器。当延迟的数据包到达了第一个服务器,这个服务端发现这个会话已经被移除了并且关闭了这个客户端连接。客户端一般不会发现这个错误因为它们不在从老的连接读取数据(老的连接一般被关闭了)。这种事情发生的另一种情况是当两个客户端使用一个保存的会话id和密码来尝试恢复相同的连接时,只有一个客户端能够恢复连接,另一个客户端将会断开。
更新服务器列表。我们允许一个客户端更新连接字符串通过提供一个新的逗号分隔的主机:端口号列表,每个都是一个服务器。函数调用一个概率负载均衡算法会引起客户端断开与当前主机的连接,来使在新列表中的每个服务器达到与预期一致的数量。万一客户端连接的当前主机不在新的列表中,这个调用会引起连接被删除。另外,这个决定基于是否服务器的数量增加或减少了多少。
比如说,如果之前的连接包含三个主机,现在的连接多了两个主机,连接到每个主机的客户端的40%为了负载均衡将会移动到新的主机上去。这个算法会引起客户端断掉它当前与服务器的连接,这个概览是0.4,并且客户端随机选择两个新主机中的一个连接。
另一个例子,假设我们有5个主机,然后现在更新列表移除两个主机,连接到剩余三台主机的客户端依然保持连接,然而所有连接到已被移除主机的客户端都需要移到剩下三台主机的一台上,并且这种选择是随机的。如果连接断开,客户端进入一个特殊的模式并使用概率算法选择一个新的服务器,而不仅仅只是循环。
在第一个例子中,每个客户端决定断开连接的概览为0.4,但是一旦做了决定,它将会随机的连接到一个新的服务器,仅仅当它不能连接到任何一台新的服务器上时,它将尝试连接旧的服务器。当找到一个服务器或者新列表中所有的服务器都连接失败的时候,客户端回到操作正常模式,选择一个任意的服务器并尝试连接它,如果连接失败,它会继续尝试不同的随机的服务器,并一直循环下去。
ZooKeeper Watches
ZooKeeper中所有的读操作——getData(), getChildren()和 exists()—可以选择设置 一个监听器。这是ZooKeeper’s一个监听器的定义:一个监听事件是一次性触发,当一个被设置监听的数据改变时,发送给设置这个监听器的客户端。在这个监听器的定义中,有三个要点:
一次性触发:当数据改变的时候一个监听事件会被发送给客户端。比如说,如果一个客户端做了getData(“/znode1″, true)操作,然后 /znode1下的数据被改变或者删除了,客户端将得到/znode1的一个监听事件。如果/znode1节点再次发生改变,没有监听事件会被发送除非客户端做了别的设置了一个新的监听器。
发送到客户端:这意味着事件正在发送给客户端的途中,但是在操作成功的返回码到达发起这个变更操作的客户端之前,事件可能还没到达监听的客户端。ZooKeeper提供了一个有序保证:在它第一次看到监听事件之前,它永远不会看到它设置的监听改变。网络延迟或别的因素可能会引起不同的客户端看见监听器和更新操作的返回码,在不同的时间。关键得一点是不同的客户端看见的每件事有一个一致的顺序。
被设置监听的数据:这是指一个节点能变化的不同方式。可以认为ZooKeeper有两个监听器列表:数据监听和子节点监听。getData()和exists()设置数据监听器。 getChildren()设置子节点监听器。二选一,根据返回数据的类型来设置监听器。getData()和exists()返回节点的数据信息,然而getChildren()返回一个子节点列表。因此,setData()会触发数据监听器。一个成功的 create()会触发一个数据监听器。一个delete()会触发数据监听器和子节点监听器。
在ZooKeeper服务器中,当客户端连接的时候,监听器被保存在本地。这使得监听器轻量级的被设置、保存、分发。当一个客户端连接一个新的服务器,监听器会触发一些会话事件。当从服务器断开连接的时候,不会受到监听器。当一个客户端重新连接,如果需要的话,之前注册的监听器会被注册和触发。有一个监听器可能丢失的情况:如果在断开连接期间,一个节点被创建和删除,一个已存在的节点的监听器还没有创建,将丢失。
监听器的意思
我们能在三种调用读取ZooKeeper状态的情况下设置监听器:exists,getData和getChildren,下面的列表是一个监听器触发的事件的详细情况:
创建事件:exists的调用
删除事件:exists,getData和getChildren的调用
改变事件:exists,getData的调用
子节点事件:getChildren的调用
移除监听器
我们可以调用removeWatches来移除一个注册在节点上的监听器。同样的,一个ZooKeeper客户端在没有服务器连接的情况下能移除本地的监听器,通过设置本地的标记为true。下面是事件的详细列表监听器成功的被移除后触发:
子节点移除事件:调用getChildren增加的监听器。
数据移除事件:调用exists或getData增加的监听器。
ZooKeeper对监听器的保证
对于监听器,ZooKeeper有下列的保障:
监听器和另外的事件,另外的监听器和异步的回复是有序的。ZooKeeper 客户端库确保每件事都有序分发。
一个客户端看到这个节点的新的数据之前,会先看到他监听的节点的一个监听事件。
从ZooKeeper 来的监听事件的顺序对应于ZooKeeper 服务看到的更新的顺序。
关于监听器要记住的事情
监听器是一次触发的,如果你得到了一个监听事件并且想继续得到未来的事件通知,你必须设置一个另外的监听器。
因为监听器是一次触发的,就会在得到事件和发送请求设置新的监听器之间有一个延迟,你不能看到ZooKeeper的节点上每次 改变。准备好处理在得到事件和设置监听器之间节点多次改变的情况(你或许不太关心,但至少要意识这会发生)。
一个监听器对象或一个函数/上下文对,为一个事件只会被触发一次。比如说,如果相同的监听器在一次exists或getData调用中被注册到了相同的文件,并且文件被删除,对于该文件删除的通知,监听器对象只会被调用一次。
当你从服务器断开连接,在恢复连接之前,你不会得到任何监听器。由于这个原因,会话事件会被发送给所有的未处理的监听器。使用会话事件进入一个安全模式:在断开期间,你不会收到事件,所以你的进程在这种模式下应该小心行事。
ZooKeeper使用ACLs控制访问
ZooKeeper使用ACLs来控制访问它的节点(ZooKeeper数据树上的数据节点)。ACL的实现和UNIX文件访问权限非常相似:它使用权限位来允许/拒绝对节点和位适用范围的各种操作。不像标准的UNIX权限,一个ZooKeeper节点没有限制在这三个标准的范围:user (文件拥有者)、group、world 。ZooKeeper没有节点拥有者的概念,取而代之的是,一个ACL指定ids和id相关的权限的集合。
还请注意一个ACL只适用于一个指定的节点,它也不适用于子节点。比如说,如果 /app节点只能被ip:172.16.16.1读取, /app/status是全部可读的,任何人都 可以读取/app/status。ACLs不是递归的。
ZooKeeper支持可插拔式的认证方案。Ids指定使用这个形式scheme:id,scheme是id对应的授权方案,比如说,ip:172.16.16.1是一个主机地址为172.16.16.1的id。
当一个客户端连接ZooKeeper并进行认证,ZooKeeper把符合这个客户端的所有ids联系起来。当客户端尝试存取一个节点的时候,这些ids用来检查一个节点的ACLs。ACLs由成对(scheme:expression, perms)的组成。expression的格式指定了权限,比如说,(ip:19.22.0.0/16, READ)给所有的以19.22开头的IP地址的客户端读的权限。
ZooKeeper支持下列权限:
CREATE:可以创建一个子节点
READ:可以从一个节点读取数据并展示子节点
WRITE:可以设置一个节点的数据
DELETE:可以删除一个子节点
ADMIN:可以设置权限