码迷,mamicode.com
首页 > 其他好文 > 详细

zookeeper学习

时间:2016-07-07 14:25:27      阅读:175      评论:0      收藏:0      [点我收藏+]

标签:

Zookeeper是参考Google Chubby实现原理设计实现的一个分布式应用协调系统。Zookeeper的原型系统由Yahoo!开发,目前,由Apache基金会维护,为Hadoop项目的子项目。本文主要通过分析Chubby,Zookeeper项目的相关文档,总结和分析了Zookeeper的特点,能使用Zookeeper实现的高级分布式应用场景,以及用Zookeeper实现的分布式协调功能帮助社区产品线解决的一些分布式问题。本文还讨论了社区Space组的UDAM资源定位系统与Zookeeper比较的一些不同之处。

1 Google Chubby

分布式锁服务是现代分布式系统的重要基础之一。简单的说,分布式锁服务实现了在一个分布式的环境下实现了单机版锁服务的功能。在分布式环境下,锁服务锁定的资源对象通常不是单个线程,或者进程,而是单个或者多个客户端。Google对分布式锁服务的定义是:分布式锁服务的目标是让客户端的行为得到同步,并使他们关于环境的一些基础信息达到统一。由于是分布式系统的组成部分,分布式锁的首要设计目标包括:可靠性、对于大规模客户端的可用性、以及简单,且易于理解的语义。锁服务的吞吐率和存储能力则是其他需要重点考虑的问题。值得注意的一点是,与这几个重要的目标相比,分布式系统的性能并不是一个需要优先考虑的问题。

Chubby是Google开发的分布式锁系统,目前,在Bigtable(Google的分布式存储系统)和MapReduce(Google的分布式计算平台)中都应用了Chubby提供的分布式锁服务,以保证分布式的同步和并发。在参考文献[1]中提到,Chubby被用于解决以下问题: 
a) 帮助工程师实现服务的高可用性。
b) 为需要主从选择,数据划分的服务提供选择机制和通告方式(advertising the results)。
c) 基于锁的使用接口(lock-based interface)容易理解
d) 分布式一致性算法利用投票的方式来做出决定,并利用镜像冗余获得高可用性

Chubby的设计实现基于以下的考虑: 
a) 一个服务通过一个Chubby文件发布服务的基础信息,而这个服务可能被成千上万的客户端所依赖。这就要求在服务器尽可能少的条件下,有效支持这些客户端对该文件的访
b) 多服务器冗余环境下,从属服务器需要知道主服务器的更新状况,这就需要通知机制来避免轮询,以提高效率
c) 需要有cache机制来缓存查询结果,以防止多余重复访问
d) 需要有权限控制来保证数据访问的安全

1.1 系统构架

Chubby主要由两个通过PRC通讯的部分组成:服务器,与应用相连的客户端库。

每个Chubby组群(Cell)都包含一组保持相同数据的Chubby服务器,被称为replicas。每个组群利用分布式一致性协议(distributed consensus protocol, 如:Paxos[2])来选择一个主服务器(master),并且在一个主服务器租期(master lease)内,组群不会再选择新的主服务器。组群中的所有服务器都有一个数据库的备份,但只有主服务器能发起读和写的响应,其他服务器只利用分布式一致性协议从主服务器获得更新。

客户端通过DNS服务器获得组群中主服务器的地址。当客户端确定了主服务器后,就将所有请求发送到主服务器,直到主服务器不再响应,或者该服务器返回它已经不再是主服务器为止。主服务器将客户端写请求转发给其他服务器,只有当组群中大多数服务器更新成功后,该写请求才被确认。读请求完全由主服务器响应。当主服务器死机,或者主服务器租期结束时,组群能在1分钟之内选出新的主服务器(一般来说,这个过程只花费几秒钟时间)。当服务器失效后,若数小时不能恢复服务,则系统自动从备机池中启用备机保证组群机器数目。

技术分享

图一、Chubby系统构架

1.2 系统细节

Chubby采用类似于UNIX文件系统的方式管理系统中的相关文件,如一个典型的文件路径: /ls/foo/wombat/pouch。其中ls是Chubby系统常用的前缀,代表Lock Service;foo是Chubby集群的名字。/wombat/pouch则标识了Chubby的具体服务。Chubby如此的树状层次结构安排主要是为了方便文件系统(如Google File System)的访问。但与UNIX文件系统不同的是,Chubby中文件的访问权限完全取决于文件本身定义的访问权限,与它所在上层目录的访问权限无关。Chubby中的文件和文件夹,也被称为节点(node),有“永久节点”和“暂时性节点”之分。任何类型的节点都能被删除,“暂时性节点”还可以在无客户端连接时自动删除。每个节点都能作为一个读写锁被使用。节点的元数据信息中,包含ACL(Access Control Links)信息,当节点被创建时,若不指定其访问权限,则节点自动继承上级目录的访问权限。每次RPC访问时,系统会自动根据ACL信息确定客户端的访问合法性。

节点的元数据中,还包含4个严格增长的64位整数:

名称 描述
实体编号 比上一个与此节点同名的节点的该编号大
内容编号 当文件内容发生改变时增长
锁编号 当锁状态从释放变为持有时增长
ACL编号 当节点的ACL信息被修改时增长

Chubby中的任何文件都可以作为锁被客户端持有(以互斥锁,或者读写锁的模式),但无论以何种方式持有锁都要求客户端对该节点有写权限,如果客户端只对某个节点有读权限,它无法阻止有权限的客户端对节点进行写操作。分布式环境中的锁是非常复杂的,因为各个实体间的网络交互情况是不确定的。可能出现的情况是客户端A获得了锁L,当它发送请求R时,网络发生故障。此时客户端B又获得了锁L,并进行了某些操作,这时,如果R请求到达的话,它并不是一个受到锁L保护的操作。解决这个问题的关键在于,服务器在处理请求时必须严格按照事件发生的顺序。由于对于每次交互都加序列号的实现方式代价太大,Chubby只对需要锁的操作记录序列号:客户端在取得了锁之后,会马上向服务器请求一个序列号和锁状态。当客户发起操作请求时,服务器会验证客户端带的序列号和锁状态是否有效,如果已经失效,则操作被拒绝。

即便如此,也不是所有协议都可以加上序列号,所以Chubby还保存了一种不完美,但比较有效的方法:锁延时(lock-delay)。当客户端主动释放锁时,其他客户端可以马上获得该锁。但如果是因为客户端失效或者通信失败的话。其他客户端将在一定时间内(即,锁延时时间内)不能获得该锁。

Chubby具有依据事件的异步唤醒机制,支持的事件包括: 
a) 文件内容被修改。该事件通常被用来监视服务描述(即,资源定位)文件
b) 子节点的增加、删除、或者修改操作
c) Chubby主服务器失效。接收到该事件表明,某些交互信息可能已经丢失
d) 锁被获得。这个事件可以被用于判断某服务器是否被选为主服务器
e) 其他客户端发来的锁冲突请求

Chubby实现了以下的API:

接口名 描述
Open() 根据文件名获得文件句柄,与UNIX类似。客户端需要传递如下信息:
a)这个句柄的使用方式(读、写、锁定,或者修改访问权限),只有在使用方式符合客户端权限的时候,打开句柄才能成功。
b)客户端接收的事件类型
c)锁延时(lock-delay)长度
d)是否需要创建新文件或者新目录,如果需要则需要传递初始化信息
Close() 关闭文件句柄
Poison() 不关闭文件句柄,但任何关于该句柄的操作都会失败
GetContentsAndStat() 获得文件(节点)数据和元数据
GetStat() 获得文件(节点)元数据
ReadDir() 获得文件夹中子节点名和他们的元数据
SetContents() 写文件内容。客户端需要提供当前数据编号,如果编号错误,则失败
SetACL() 设置文件(节点)ACL信息
Delete() 删除一个无子节点的节点
Acquire() 请求锁
TryAcquire() 尝试请求锁
Release() 释放锁
GetSequencer() 返回一个与该句柄持有锁相关的序列号
SetSequencer() 将一个序列号与句柄相关联

客户端利用Chubby选主的过程即可利用以上的API实现:所有候选客户端同时打开同一个文件句柄,并竞争它的锁,获得锁的客户端将自身信息写入该文件,以便让其他客户端知道自己已经成为主客户端。

另一方面,为了减轻网络交互的压力,Chubby的客户端库还实现了cache。客户端库接收服务器发送的失效通知来使客户端内存中的数据失效。当Chubby服务器上的数据和元数据被修改的时候,主服务器会发失效通知给可能cache了这些数据的客户端,这一机制是建立在KeepAlives机制之上,并依赖租约机制。它保证了客户端看到的服务器是一致的,或者是失效的。注意,此处Chubby服务器仅仅是让客户端的cache失效,而没有不是更新客户端的cache,这是由于在分布式情况下让cache失效远比直接更新cache要有效率。

Chubby中定义的会话(Session)指的是Chubby组群和客户端之间的一种连接关系,这种关系存在于一定时间段内,并由一种周期性进行的handshake所维护,这种握手被称为KeepAlives。除非客户端通知Chubby组群中止会话,客户端关于Chubby句柄的任何操作都会使会话保持有效。客户端在第一次连接Chubby主服务器的时候建立会话,当会话被外部中止,或者空闲的时候,会话结束。每个会话都由一个租期(lease)相关联,Chubby服务器保证在租期时间内,服务器端不会关闭会话。服务器有权限延长与客户端的会话租期时间,但不能将其缩短。

服务器通过KeepAlives的回复可以延长客户端租期的时间。此外,KeepAlives回复还被用于传递Chubby服务器事件和cache失效信息给客户端。当服务器有信息传递给客户端时,KeepAlives回复将会早一些返回。而KeepAlives回复中的Piggybacking事件保证了客户端不能在没有做cache失效确认的情况下维持会话有效。这样的机制简化了客户端,并且方便穿透只能单向发起请求的防火墙。当一个客户端租期到期的时候,客户端无法判断是否是服务器中止了会话,此时客户端会清空本地cache,进入Jeopardy状态,并等待一段时间(the grace period)。如果在这段时间中,Chubby服务器能成功的完成KeepAlives交互,则客户端重新使其cache有效。否则客户端认为会话已经失效。这保证了客户端的调用不会在Chubby组群失效时永久阻塞。当grace period开始的时候,Chubby客户端库保证客户端会收到Jeopardy事件,如果,KeepAlives能成功完成,则客户端会收到safe事件,否则,则收到expired事件。

当Chubby主服务器发生变更时,新的Chubby主服务器会做以下一些工作: 
a) 首先选择一个新的时期编号(epoch number),客户端的每个请求都需要带这个编号。这样可以保证当前的Chubby主服务器不会处理以及过时的(发给以前主服务器的)请求。
b) 新的主服务器也可以响应定位主服务器的请求
c) 新的主服务器在内存中创建被用于会话和锁等信息的结构体,并保存在数据库中。并且将会话租期调整到前一主服务器允许的最大租期长度
d) 此时,新的主服务器已经可以接受KeepAlives请求,但不能处理会话相关的操作。
e) 新的主服务器发送服务器失效事件给每个连接客户端,并通知它们清除cache,因为某些事件可能已经被丢失。
f) 新的主服务器等待所有连接客户端确认服务器失效事件,并让会话过期
g) 如果客户端使用过期的句柄连接服务器,则新的主服务器重新在内存中建立该句柄的标识方式,并相应请求。但如果该句柄被关闭,则新的主服务器保证,在其服务期间,该句柄不能再次被使用,这保证了网络延时不会造成已关闭的句柄重新打开。
h) 新的主服务器在启动一段时间后会关闭没有客户端链接的暂时性节点,所以客户端在收到主机主机失效的事件后,应该刷新暂时性节点。

关于数据存储和备份方面,Chubby一开始使用Berkeley DB的key-value存储方式储存节点数据和元数据,并每隔几个小时就将数据库快照备份到异地的GFS文件服务器上。这样的备份机制完全可以保证数据恢复,或者重建新备机数据的要求。镜像功能方面,由于Chubby文件的实现方式,简单的文件拷贝就能实现镜像功能。

2 Zookeeper

正如前文所说,Zookeeper的设计和实现参考了Chubby的设计原理。如,Zookeeper也使用了类似于文件系统的树状名字空间,也用ACL控制访问权限,也有事件触发机制等。但Zookeeper实现的功能可以认为是Chubby实现功能的一个子集。以下将总结和分析Zookeeper实现的功能,并与Chubby以及Space组的UDAM系统的功能做一个比较。

2.1 Zookeeper数据模型

Zookeeper使用类似于分布式文件系统的树状命名空间。不同的是,在Zookeeper中只使用绝对路径,没有相对路径。对于命名空间中的每一个节点,都有与之相关的数据和子节点。(分析:此处Zookeeper的资源表示方式与Chubby是一致,以路径名的方式来表示资源的逻辑关系。UDAM也采用树状的方式标识资源)。

2.1.1 数据节点(ZNodes)

Zookeeper树状结构上的节点被称为znode。每个znode维护一个stat数据结构,它包括了数据的版本号,访问控制链的版本号,时间戳等。每次znode的数据发生变化时,数据版本号都会增长。当客户端发起一个节点的删除或者更新操作时,必须携带该数据节点当前的版本号,若版本号不正确,则操作将会失败。(分析:与Chubby不同的一点是,Zookeeper的节点并没有天然的锁属性,不能对节点进行直接的加锁和解锁操作。但锁的实现通过Zookeeper实现的另一个特性得到弥补,即,顺序节点。本文后面会解释如何使用序列节点实现分布式锁功能。另一方面,与Chubby一致的一点是,节点有与之关联的数据和元数据。数据的格式自由,有很好的扩展性。在这方面,UDAM目前实现的是子系统级别的资源实体定位,数据可扩展性方面较弱)

2.1.1 监视机制(Watch)

Zookeeper客户端在数据节点上设置监视,则当数据节点发生变化时,客户端会收到提醒。详细分析见本文下面的内容。

2.1.2 数据访问(Data Access)

对于Zookeeper数据节点的数据读写是Zookeeper自动完成的,并以单个节点为粒度,即,读操作读出与数据节点相关联的所有数据,写则替换该节点上的所有数据。每个节点上的“访问控制链”(ACL, Access Control List)保存了各客户端对于该节点的访问权限。(分析:ACL的访问权限控制思想与Chubby一致。Zookeeper对与用户类别的区分,不只局限于所有者、组、以及所有人三个级别。Zookeeper支持对访问的区分有更加细的粒度。UDAM使用ip名单的方式控制,防止线下的模块使用线上的资源。但没有粒度更小的权限控制。对于目前NS应用来说,UDAM的实现已经够用,但Zookeeper这方面功能更加完善)

2.1.3 暂时性节点(Ephemeral Nodes)

Zookeeper有个暂时性节点机制,即,暂时性节点仅在单个会话(session)中,当会话结束时,节点便销毁了。这个特性决定了这类节点不能有子节点。(分析:从Zookeeper文档描述分析,暂时性节点是对应于模块级资源的合适选择。当模块失效时,节点销毁。)

2.1.4 顺序节点(Sequence Nodes)

顺序节点用于保证新创建的节点中所带的编号一定大于某个已有节点的编号。(分析:Chubby中没有顺序节点的概念,Zookeeper使用顺序节点来实现分布式锁的功能)

2.2 Zookeeper 中的时间

Zxid
Zookeeper 事务ID,由zxid来标识Zookeeper状态的每一个变化,zxid按照产生的时间顺序递增。

节点版本号:数据节点的每一个变化将改变节点的一个版本号。三个节点版本号包括:版本(数据节点变化的次数)、子节点版本(子节点变化的次数)、访问控制链版本(访问控制链变化的次数)

Ticks:在多服务器的Zookeeper服务中,时间的时间用ticks来描述。当客户端要求一个比最小会话失效时间更小的超时时间时,最小会话失效时间将会被使用。

2.3 stat 结构体

字段名 含义
Czxid 该znode创建时的zxid
Mzxid 该znode最后一次修改时的zxid
Ctime 该znode创建时的时间
Mtime 该znode最后一次修改的时间
Version 该znode的变化次数
Cversion 该znode的子节点变化次数
Aversion 该znode的acl变化次数
EphemeralOwner 如果是个暂时性节点,则记录拥有该znode的会话id,否则为0
DataLength 该znode数据长度
NumChildren 该znode子节点个数

2.4 Zookeeper会话

客户端保存了所有Zookeeper服务器列表,并在发起服务请求时随机选择服务器。当客户端向Zookeeper发起服务请求时,Zookeeper会创建一个Zookeeper会话,并产生一个64位数值的会话ID。如果客户端连接其他Zookeeper服务器,则会把这个会话ID作为握手的一个参数。为了保证数据传输安全,与会话ID相关联的一个密码会发送给客户端。会话ID与密码的关联关系,集群中的任何一台Zookeeper服务器都可以验证。(分析:该机制不仅保证了数据传输的安全性,也保证了,当某台Zookeeper服务器停机时,客户端能够毫无障碍地连接新的Zookeeper服务器以获得持续的会话服务)

客户端与服务器建立连接时的参数包括: 
a)超时设置(以ms为单位),目前要求这个超时时间至少是tickTime的两倍
b)默认监视器,当客户端的数据发生变化的时候,该监视器将被通知。监视器的初始状态是“连接断开”,所以发起连接请求时,监视器收到的第一的事件是连接建立。
(分析:UDAM利用α报文建立客户端与资源中心服务器的连接,交互过程如下:
a)客户端发送α报文给UDAM资源服务器
b)UDAM资源服务器与客户端建立TCP连接,并将所有资源服务器信息发送给客户端,注意,此处UDAM会将最新的资源中心列表发送给客户端。但Zookeeper的资源服务器列表依赖于自身配置。
c)客户端发送自己拥有的资源和依赖的资源信息
d)资源服务器发送客户端所依赖的资源信息,连接建立

Zookeeper的会话保持机制通过客户端发送的请求来维持,如果客户端一段时间没有发送请求,则客户端会发送PING请求来保持会话。这个心跳机制也保证了服务器端了解客户端的生存状态。(分析:UDAM没有使用Session id作为连接标识,它使用客户端发送的β报文来行使心跳的功能。由于β报文是以UDP的方式发送的,在服务器端超时未收到β报文的时候,资源服务器会发起TCP连接,以保证客户端的报文不是由于UDP丢包而导致。此处Chubby的实现方式不一样,Chubby利用KeepAlives来保持会话,但它利用KeepAlives返回包传递服务器事件等信息。Zookeeper没有使用ping返回包携带数据信息,但也是靠客户端发起获取资源的请求。这两种方式对于穿透单向防火墙有帮助。UDAM客户端需要监听特定端口来捕获资源服务器发起的连接。)

当连接成功建立以后,有两种情况可以导致客户端产生失去连接错误。 
a) 对于一个已失效(不合法)的会话的操作
b)客户端与Zookeeper服务器断开连接,但仍有未完成的异步请求。

2.5 Zookeeper监视机制

Zookeeper中的各种读请求,如getDate(),getChildren(),和exists(),都可以选择加“监视点”(watch)。“监视点”指的是一种一次性的触发器(trigger),当受监视的数据发生变化时,该触发器会通知客户端。

监视机制有三个关键点: 
a) “监视点”是一次性的,当触发过一次之后,除非重新设置,新的数据变化不会提醒客户端。
b)“监视点”将数据改变的通知客户端。如果数据改变是客户端A引起的,不能保证“监视点”通知事件会在引发数据修改的函数返回前到达客户端A。对于“监视点”,Zookeeper有如下保证:客户端一定是在接收到“监视”事件(watch event)之后才接收到数据的改变信息。
c)getData() 和exists()设置关于节点数据的“监视点”,并返回节点数据信息;getChildren()设置关于子节点的“监视点”,并返回子节点信息。setData()会触发关于节点数据的“监视点”。成功的create()会触发所建立节点的数据“监视点”,和父节点的子节点“监视点”。成功的delete()会触发所删除节点的数据“监视点”,和父节点的子节点“监视点”。

“监视点”保留在Zookeeper服务器上,则当客户端连接到新的Zookeeper服务器上时,所有需要被触发的相关“监视点”都会被触发。当客户端断线后重连,与它的相关的“监视点”都会自动重新注册,这对客户端来说是透明的。在以下情况,“监视点”会被错过:客户端B设置了关于节点A存在性的“监视点”,但B断线了,在B断线过程中节点A被创建又被删除。此时,B再连线后不知道A节点曾经被创建过。

(分析:UDAM的资源修改提醒机制实现逻辑如下: 
a) 模块A在修改后重启时利用α报文通知UDAM资源中心
b)资源中心确认并与模块A进行数据交互
c)资源中心将所有依赖于模块A的资源标记为不可用,有一个单独的定时线程扫描所有资源,并与这些不可用资源通信,以达到更新数据的目的。)

Zookeeper的“监视”机制保证以下几点: 
a) “监视”事件的触发顺序和分发顺序与事件触发的顺序一致。
b) 客户端将先接收到“监视”事件,然后才收到新的数据
c) “监视”事件触发的顺序与Zookeeper服务器上数据变化的顺序一致

关于Zookeeper“监视”机制的注意点: 
a) “监视点”是一次性的
b) 由于“监视点”是一次性的,而且,从接收到“监视”事件到设置新“监视点”是有延时的,所以客户端可能监控不到数据的所有变化
c) 一个监控对象,只会被相关的通知触发一次。如,一个客户端设置了关于某个数据点exists和getData的监控,则当该数据被删除的时候,只会触发“文件被删除”的通知。
d) 当客户端断开与服务器的连接时,客户端不再能收到“监视”事件,直到重新获得连接。所以关于Session的信息将被发送给所有Zookeeper服务器。由于当连接断开时收不到“监视”时间,所以在这种情况下,模块行为需要容错方面的设计。

2.6 Zookeeper基于ACL的访问控制机制

Zookeeper利用访问控制链机制(Access Control List)控制客户端访问数据节点的权限,类似于UNIX文件的访问控制。但Zookeeper对于用户类别的区分,不止局限于所有者(owner)、组(group)、所有人(world)三个级别。Zookeeper中,数据节点没有“所有者”的概念。访问者利用id标识自己的身份,并获得与之相应的不同的访问权限。

Zookeeper支持可配置的认证机制。它利用一个三元组来定义客户端的访问权限:(scheme:expression, perms)

其中,scheme定义了expression的含义,如:host:host1.corp.com标识了一个名为host1.corp.com的主机。Perms标识了操作权限,如:(ip:19.22.0.0/16, READ)表示IP地址以19.22开头的主机有该数据节点的读权限。

Zookeeper权限定义:

权限 描述 备注
CREATE 有创建子节点的权限
READ 有读取节点数据和子节点列表的权限
WRITE 有修改节点数据的权限 无创建和删除子节点的权限
DELETE 有删除子节点的权限
ADMIN 有设置节点权限的权限

Zookeeper内置的ACL模式:

模式 描述
world 所有人
auth 已经被认证的用户
digest 通过username:password字符串的md5编码认证用户
host 匹配主机名后缀,如,host:corp.com匹配host:host1.corp.com, host:host2.corp.com,但不能匹配host:host1.store.com
ip 通过IP识别用户,表达式格式为 addr/bits

2.7 Zookeeper一致性保证


a) 序列一致性:客户端发送的更新将按序在Zookeeper进行更新
b)原子一致性:更新只能成功或者失败,没有中间状态
c)单系统镜像:无论连接哪台Zookeeper服务器,客户端看到的服务器数据一致
d)可靠性:任何一个更新成功后都会持续生效,直到另一个更新将它覆盖。可靠性有两个关键点: 第一,当客户端的更新得到成功的返回值时,可以保证更新已经生效,但在某些异常情况下(超时,连接失败),客户端无法知道更新是否成功;第二,当更新成功后,不会回滚到以前的状态,即使是在服务器失效重启之后。
e)实时性:Zookeeper保证客户端将在一个时间间隔范围内获得服务器的更新信息,或者服务器失效的信息。但由于网络延时等原因,Zookeeper不能保证两个客户端能同时得到刚更新的数据,如果需要最新数据,应该在读数据之前调用sync()接口。

3 利用Zookeeper实现高级分布式应用

利用Zookeeper的会话,监视等机制,可以实现更高级的分布式应用。虽然Zookeeper的事件采用的是异步通知机制,但Zookeeper可以用于实现同步一致的原语操作,其原因是Zookeeper对于更新事件保证了严格的全局顺序。

3.1 障碍墙(Barriers)

分布式系统利用障碍墙来保证对于一组数据节点的处理被某个条件阻塞。直到条件被满足的时候,所有数据节点同时开始处理。在Zookeeper中,可以利用一个“障碍墙节点”(算法中使用b表示该节点)来实现这个功能: 
1、 客户端对于该“障碍墙”节点调用exists(b, true)函数,设置watch
2、如果exists()返回false,“障碍墙”节点不存在,客户端继续执行
3、如果exists()返回true,客户端等待“障碍墙”节点的watch事件
4、当watch事件被激发时,跳回1执行

3.2 双重障碍墙(Double Barriers)

双重障碍墙用于保证客户端同步开始和结束某个计算过程。当足够的客户端被障碍墙阻挡的时候,计算开始执行。当所有计算都结束的时候,所有客户端一起脱离障碍墙。利用Zookeeper实现双重障碍墙的同步机制,在计算启动之前,客户端通过在“障碍墙节点”(算法中使用b表示该节点)注册来加入同步过程。而在计算结束时,客户端从“障碍墙节点”注销。

在以下算法代码中,n是客户端的Zookeeper名字标识,p标识了一个客户端,pmax标识了编号最大的一个客户端,pmin标识了编号最小的一个客户端。

进入
1、创建名字 n = b + “/” +p
2、设置“监视点”:exists(b + “/ready”, true)
3、创建子节点:create(n, EPHEMERAL)
4、L = getChildren(b, false)
5、如果L中的子节点个数少于x(根据应用配置),客户端等待“监视”事件
否则 create(b + “/ready”, REGULAR)

如算法所示,在进入“障碍墙”的过程中,客户端在“障碍墙”节点下创建代表自己的临时节点。当最后一个客户端进入后,它能检测到“障碍墙”节点已经有x个子节点,此时,该客户端创建“ready”节点,通知等待“监视”事件的所有客户端同时开始计算过程。

退出
1、L = getChildren(b, false)
2、如果没有L中没有子节点,退出
3、如果p是L中唯一的子节点,delete(n),并退出
4、如果p是L中编号最小的节点,等待在pmax之上
否则,delete(n)(如果n还存在),等待在pmin之上
5、跳回1执行

在计算结束,所有客户端需要删除各自子节点,并同时离开“障碍墙节点”。注意,在上述退出协议中,最后一个被删除的子节点是序号最小的子节点,该子节点对应的客户端将“监视点”设置在当前序号最大子节点上。当该序号最大的子节点被删除后,继续选择当时序号最大的子节点等待。所有其他客户端都等待在序号最小的子节点上,当该节点被删除之后,所有其他的客户端同时离开。

3.3 队列

利用Zookeeper实现分布式队列结构,需要创建一个用于维护队列的数据节点,“队列节点”。当客户端需要往队列里添加成员的时候,调用create()函数,并设置“序列(sequence)”和“暂时性(ephemeral)”选项,并以“queue-”为节点名前缀,则由于序列属性的作用,创建的子节点都以“queue-X”为名,且X为一个严格增长的数字。处理队列的客户端,调用getChildren(),并设置“监视点”。按照序列号从小到大的顺序处理子节点。当第一次getChildren()获得的子节点都处理完了之后,继续等待“监视点”事件。这样就实现了分布式环境下的队列性质。

3.4 优先级队列

利用Zookeeper实现带优先级的队列,只需要简单地修改普通队列中节点的命名方式:以“queue-YY”来做队列元素的前缀,其中YY为节点的优先级,依据Linux的方式,数值越小,优先级越高。并且,处理队列的客户端,在处理完一个节点之后,需要调用sync()以保证有优先级高的节点插入时,能够先获得处理资源。

3.5 互斥锁

分布式互斥锁的定义是在任意时刻,分布式系统中的两个客户端不会同时认为自身持有同一个互斥锁。同样,首先我们需要定义一个锁节点,想要获得锁的客户端,按照以下过程操作:

获得锁
1、调用create()函数,设置路径名“_locknode_/lock-”,并且设置“序列(sequence)”和“暂时性(ephemeral)”选项
2、对锁节点调用getChildren()函数,并不设置“监视点”(注意,此处不能设置“监视点”)
3、如果1中创建的子节点序号是子节点中最小的序号,则该客户端获得了互斥锁,退出
4、对比1中创建的子节点序号小的最大的子节点调用exists()函数,并设置“监视点”
5、如果exists()返回true,等待“监视”事件通知,并跳回2
否则,跳回2
释放锁
1、已经获得锁的客户端要释放锁的话,只需要删除之前创建的子节点便可

需要注意的几点:
1、每次删除锁节点的子节点时,只会唤醒一个客户端。
2、在这个实现方案中不需要轮询和超时处理

3.6 读写锁

利用Zookeeper实现的读写锁,是在互斥锁的基础上实现的。
获得读锁
1、调用create()函数,设置路径名“_locknode_/read-”,并且设置“序列(sequence)”和“暂时性(ephemeral)”选项
2、对锁节点调用getChildren()函数,并不设置“监视点”(注意,此处不能设置“监视点”)
3、如果不存在其子节点序号比1中创建的子节点序号小的“write-”子节点,当前的客户端获得读锁,退出
4、对于序号小于1中创建的子节点序号的“write-”调用exists(),并设置“监视点”
5、如果exists()返回true,等待“监视事件”通知,并跳回2
否则,跳回2
释放读锁
1、删除客户端之前创建的子节点

获得写锁
1、调用create()函数,设置路径名“_locknode_/read-”,并且设置“序列(sequence)”和“暂时性(ephemeral)”选项
2、对锁节点调用getChildren()函数,并不设置“监视点”(注意,此处不能设置“监视点”)
3、如果1中创建的子节点序号是子节点中最小的序号,则该客户端获得了写锁,退出
4、对比1中创建的子节点序号小的最大的子节点调用exists()函数,并设置“监视点”
5、如果exists()返回true,等待“监视”事件通知,并跳回2
否则,跳回2
释放写锁
1、删除客户端之前创建的子节点

注意,此处实现之中有多个”read-”子节点同时等待在”write-”子节点上,但这样的设计不会有副作用。这是因为,”read-”子节点在被唤醒的时候,都能获得处理资源,而不是只有某个或者某些客户端能继续执行。

3.7 可恢复(recoverable)的读写锁

通过对读写锁做不大的修改,便可实现简单的可恢复读写锁(用于防止某些锁被客户端长期持有)。方法如下:

无论是在申请读锁还是申请写锁时,在调用create()之后,立即对于该节点调用getData()并设置“监视点”(注意,如果getData()接收到了create事件,则重新getData(),并设置“监视点”)。getData()用于监视该节点数据中是否出现“unlock”字符串。需要锁的其他客户端通过向该节点数据中写入”unlock”来提醒持有锁的客户端释放锁。

注意在这个可恢复读写锁的实现机制中,有一个关键点,即,持有锁的“客户端”必须同意释放锁。在很多情况下,持有锁的客户端需要保留锁来进行未完成的操作。

3.8 二阶段提交(Two-phased Commit)

二阶段提交机制用于保证分布式系统中的客户端都同意提交或者放弃某事务。

在Zookeeper中,可以在有协调客户端(coordinator)的基础上,实现二阶段提交机制。首先协调客户端创建一个事务节点(如:/app/Tx),为每个参与客户端建立一个子节点(如:/app/Tx/s_i),这些子节点的数据为空。当各个参与客户端接收到协调客户端发送的事务时,它们别的客户端对应的节点设置“监视”。并通过写自己对应的节点的数据来告诉别的客户端自己是否确认提交事务。需要注意的是,由于很多情况下,只要有一个客户端不能确认提交,事务就会被丢弃,所以整个处理时间可能很短。

3.9 主服务选择

利用Zookeeper实现的主服务选择,有最直观的一种方式。建立选择节点”ELECTION/ ”,参与竞选主服务功能的各个客户端在该节点下建立自己对应的节点,并且设置“序列(sequence)”和“暂时性(ephemeral)”选项。序列号最小的节点为主服务。其他非主服务客户端在主服务节点上设置“监视”,当其失效的时候,由于其“暂时性”,原来序号倒数第二小的节点变为主服务。这个实现有一个问题,就是,多个客户端同时监视在一个节点的变化,当这个节点发生变化是,所有客户端都被唤醒。如果客户端数量庞大的话,会造成Zookeeper服务器的瞬时大压力。

4 Zookeeper实际应用场景分析

本章初步分析了Zookeeper在NS各产品线中可能的应用场景,主要包括:资源定位、数据库主从自动调节、和异步作业管理等。

4.1 资源定位

资源定位是目前NS各产品线比较迫切的一个需求。目前,NS各产品线中各个模块的相互依赖关系完全是由配置文件指定。这种灵活性较差的资源配置方式在遇到以下异常状况时难于处理:
1、模块停止服务。当某模块A停止服务的时候,依赖于该模块的模块B仍然会向该模块请求服务。在最坏的情况下,模块B能与模块A建立TCP连接,但由于模块A实际已经不能处理请求。模块B会在读超时后返回。这样很有可能造成前端UI模块处理超时,造成稳定性下降。
2、机器更换和迁移。机器更换和迁移对于需要多服务器支持的服务来说是非常平凡的事情。但每次机器更换和迁移都给op带来大量运维工作。因为,依赖于这些机器上模块的服务需要修改配置并重启。
3、服务器死机。服务器死机的情况与模块停止服务情况类似,由于一台服务器上往往有多种服务提供,服务器死机对服务稳定性的影响更加大。

分析上述情况可以看出,服务稳定性的关键在于及时发现已经失效(或者变化)的模块,并以此更新程序的行为。利用Zookeeper实现资源定位,可以采用以下的结构方式:
/Zookeeper集群名/机房/服务名/资源节点

服务初始化
1、创建初始节点为常规节点,(/Zookeeper集群名/机房/服务名/)
资源注册
1、当服务启动的时候,在对应服务名下创建子节点,并且设置“序列(sequence)”和“暂时性(ephemeral)”选项。
2、将资源的位置和相关信息写入该节点数据
客户端使用
1、客户端启动时,对于”/Zookeeper集群名/机房/服务名/”调用getChildren(),并设置watch
2、将返回结果中各个子节点的信息保存,用于资源的连接
3、客户端收到watch事件时,重新调用getChildren(),并设置watch
4、用返回结果中的信息更新保存的资源数据

客户端收到watch事件,可能是以下两种原因引起:节点增加,即有冗余服务上线;节点减少,某服务失效。在这些情况下,客户端都可以通过getChildren()调用,获得最新的可用数据。

4.2 数据库主从自动调节

数据库单主问题也是NS部门常常遇到的一个单点情况。为了保证数据的一致性,各种写操作都是由主库完成,并同步到其他从库中去。这样的单点设计给运行稳定性造成了极大的隐患,如果主库失效的话,会造成相当一段时间内(OP切主库操作)用户的写库操作无法响应。解决该问题包括三个方面:

a) 如何迅速发现主库已经失效
b) 如何快速选择出新的主库,并修改各从库的同步目标
c) 如何让应用程序得到新的主库地址

技术分享

利用Zookeeper来实现数据库主从的自动管理,可以按照以下方式实现:

主库选择和通知
1、 建立MYSQL_ELECTION/ 节点, MARSTER_MYSQL/ 节点,和SLAVE_MYSQL
2、 为每个mysql服务关联一个Zookeeper proxy,向该节点注册自己对应的mysql服务
3、 选择序列号最小的mysql服务为主库,主库对应的proxy在MARSTER_MYSQL/下建立序列、暂时性节点,并写入主库地址信息
4、 从库proxy修改对应的同步对象为主库,并在SLAVE_MYSQL下注册自己对应节点
5、 当主库失效时,MARSTER_MYSQL/下节点消失,直到选择出新的主库,创建新的主库信息节点。该主库删除自己在SLAVE_MYSQL节点下的注册
6、 从库proxy修改对应同步对象为主库,并在SLAVE_MYSQL下注册(如果已经注册,则不需要重新注册)
应用程序行为
1、 对MARSTER_MYSQL/节点调用getChild(),并设置监视点,依据所获得的主库地址,发送更新请求
2、 对SLAVE_MYSQL/节点调用getChild(),并设置监视点,依据所获得的从库地址,发送查询请求
3、 当主库失效时,watch事件被触发,此时应用程序暂时停止写库操作,调用getChild(),并设置监视,知道MARSTER_MYSQL下出现新的节点为止
4、 当从库失效时,仅仅更新本地从库表,并重新设置监视点

4.3 异步作业管理

异步作业管理是Zookeeper系统能够发挥作用的另一个分布式应用场景。目前,NS部门中PM常提出较为耗时的统计需求,和挖掘需求(如Passport的连连看系统)。此时,异步作业的管理就成为一个问题。目前,NS流行的做法是在程序跑结束的时候,通过邮件将结果发送给发起者。但这样的做法需要人工进行数据归类等二次处理。比较理想的一种解决方案是,PM的统计需求能够直接通过MIS提交并执行,此时可以再Zookeeper的对应节点建立关于该事件的临时节点,当处理完成后,删除这个临时节点。则监视该节点的MIS系统可以很快的收到通知,并自动取回结果,或者进一步处理。一句话,利用Zookeeper来进行异步作业管理,将会使任务处理过程清晰和方便。

5 小结

本文主要从原理和功能方面分析了Zookeeper系统以及它与Google Chubby的一些差异,并穿插讨论了Zookeeper与UDAM系统在实现资源定位相关功能上的不同。基本结论如下:Zookeeper实现了与Chubby相近的功能,可以做较复杂的分布式协同工作。可以被用于诸如:资源定位,数据库主从自动调节,异步作业管理等工作。与UDAM相比,UDAM的优势在于,更贴近我们现实的资源定位需求,实现更符合Baidu习惯。而Zookeeper的实现更加通用,扩展性更好,并有比较完善的权限管理选择。

参考资料

[1] The Chubby lock service for loosely-coupled distributed systems

[2] http://en.wikipedia.org/wiki/Paxos_algorithm

[3] http://hadoop.apache.org/zookeeper/

[4] UDAM设计文档

zookeeper学习

标签:

原文地址:http://blog.csdn.net/mysee1989/article/details/51849952

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!