翻译- O'Reilly ZooKeeper-第二章 开始掌握ZooKeeper

来源:互联网 发布:大秦铁路知乎 编辑:程序博客网 时间:2024/05/01 14:25

前一章从较高层次讨论了分布式应用在协同方面的需求。我们以广泛应用的Master-Workers架构为例,提取出了一些常用的协同原语。在本章中,我们介绍使用ZooKeeper实现这些协同原语。

ZooKeeper基础

         一些协同原语被广泛应用于各种分布式应用。一种设计协同服务的方式是根据不同原语,对外暴露方法进行实例化,然后直接操作这些实例。比如,分布式锁就是其中之一,用户可以调用方法创建、获取和释放锁。

         这种设计有一些缺点。首先,需要预先实现一个原语操作的详细清单,通过扩展API来引入新的原语。其次,缺乏灵活性,应用不能根据自己的最佳需求来实现原语。

         在设计ZooKeeper时,我们采取了截然不同的方式。ZooKeeper不直接暴露原语,而是通过一个类文件系统的API让应用来选择实现自己的原语。我们使用方法(recipes)来表示这些原语的实现。方法中包含ZooKeeper操作,被操作的数据节点称之为znode节点,节点被组织成类似文件系统的树形层级结构。图2-1展示了一棵节点树。根节点包含四个子节点,其中三个子节点拥有子节点,叶子节点是数据。

Figure 2-1. ZooKeeper data tree example

         节点数据缺失往往传递重要信息。比如在Master-Worker的例子中,缺少Master节点表示没有Master被选举。图2-1包含了一些在Master-Worker结构中有用的节点信息:

  • /workers节点是是所有可用Worker的父节点。图2-1中Worker(foo.com:2181)是可用的。如果Worker变得不可用,它对应的节点将从/workers中被移除。
  • /tasks节点是所有等待执行任务的父节点。Master-Worker应用的客户端在/tasks下创建子节点,代表新提交的任务。
  • /assign节点是所有任务分配的父节点。当Master分配任务给某个Worker,会在/assign下增加相应子节点。

API概述

         节点可能包含,也可能不包含数据。节点数据以字节数组形式存储。每个应用都可以自定义存储格式,ZooKeeper不会进行解析。ProtocolBuffers、Thrift、Avro和MessagePack等序列化包都可以用来处理节点数据存储格式,有些情况下,使用UTF-8或者ASCII编码的字符串就可以满足需求。

ZooKeeper API包含以下操作:

create /path data

         创建名称为/path节点,包含数据data。

delete /path

         删除/path节点

exists /path

         检查/path是否存在

setData /path data

         将/path节点的数据设置为data

getData /path

         返回/path的数据

getChildren /path

         返回/path的子节点列表

         重要提示:ZooKeeper不允许部分读写节点数据。当读写节点的数据时,数据内容作为整体返回或被替换。

         ZooKeeper客户端连接服务,建立会话,调用API方法。如果你急于使用ZooKeeper,请跳到“会话”章节,该节解释了从命令行运行ZooKeeper命令的方法。

节点的不同类型

         当创建新节点时,必须指定节点类型。不同类型的节点有着不同的行为表现。

持久化和临时节点

         持久化节点只能被delete操作删除,而临时节点在创建它的客户端崩溃或者连接关闭时会被删除掉。

         持久化节点被用于存储应用数据,比如在Master-Worker例子中,即使进行分配操作的Master已经崩溃,依然需要将任务的分配状态保存下去。

         临时节点用于表示创建者会话周期内有效的应用数据。比如在Master-Worker例子中,Master节点是临时节点。它存在意味着Master服务器正在运行。如果Master下线而节点继续存在,那么系统就不能检测到Master已经崩溃。Workers节点也是临时节点,当Worker不可用,会话过期,节点也会自动消失。

         临时节点在两种情况下被删除:

  1. 会话结束:过期或者显式关闭。
  2. 客户端(不需要是创建者)调用delete删除

因为临时节点在创建者会话过期时被删除,所以现在不允许临时节点拥有子节点。社区里讨论过允许临时节点拥有临时子节点,这个新特性可能会出现在将来的版本中。

顺序节点

         顺序节点被分配唯一、单调递增的序列号。序列号附加在创建时指定的路径之后。比如,如果客户端使用/tasks/task-创建顺序节点,假设ZooKeeper分配的序列号为1,那么节点路径为/tasks/task-1。顺序节点用于创建名称唯一的节点,也能够用于表示节点的创建顺序。

         总的来讲,如果组合使用,一共有四种节点类型:持久化、临时、持久化顺序和临时顺序。

观察(Watches)和通知(Notification)

         由于ZooKeeper通常作为远程服务被访问,如果客户端需要知道节点内容时每次都进行远程访问,代价非常昂贵:可能会导致高延迟和对ZooKeeper集群的过多操作。以图2-2为例,第二次对/task的getChildren调用是不必要的,因为返回同样的空集合。


Figure 2-2. Multiple reads to the same znode

这是轮询的一种常见问题。为了避免轮询,我们选择了一种基于通知的机制:客户端注册到ZooKeeper,给指定节点设置watch,接收它发生变化的通知。Watch是一次性事件,它只触发一次通知。为了收到多次通知,客户端每次收到通知之后,必须设置新的watch。如图2-3描述,客户端只有在收到表示/tasks发生变化的通知之后,才会从ZooKeeper读取新值。


         Figure 2-3. Using notifications to be informed of changesto a znode

         使用通知机制,有一些注意事项。因为通知是一次性事件,当客户端收到通知后,可能在设置新的watch之前,新的数据更新就已经发生(不要担心,你不会错过状态变化)。假设以下事件顺序发生:

  1. 客户端c1在/tasks上设置watch
  2. 客户端c2在/tasks下增加新任务
  3. 客户端c1接收通知
  4. 客户端c1设置新的watch,但是在设置之前,第三个客户端c3在/tasks下增加新任务

虽然最终客户端c1设置了新的watch,但是客户端c3的操作不会触发通知。尽管如此,因为在设置watch的时候,c1读取了/tasks的状态,所以c1没有错过任何状态变化。

ZooKeeper能够保证通知在同一节点的后续更新发生之前就被传递到客户端。如果客户端在节点上设置了watch,然后节点发生两次连续更新,客户端将在第一次更新之后,第二次更新发生之前接收到通知。这里的关键点是,通知没有破坏客户端观察到的更新次序。虽然这会导致ZooKeeper传播状态变化给客户端的速度变慢,但这保证了所有客户端观察到的ZooKeeper状态更新都是全局有序的。

         ZooKeeper会根据设置watch的不同方式,产生不同类型的通知。客户端可以针对节点数据变化,子节点变化,或者节点被创建/删除设置watch。我们通过调用API中读取ZooKeeper状态的方法来设置watch。这些API调用支持传递watcher对象作为参数,或者使用默认的watcher。在后面章节,我们将结合Master-Worker例子详细介绍怎么使用watch。

版本

         每个节点都有版本号,在数据更新时版本号会递增。API中的一些操作支持带条件执行:setData和delete操作。这两个方法都将版本号作为输入参数,只有客户端传入的版本号和服务器端的当前版本号匹配时操作才能成功。当多个ZooKeeper客户端试图在同一节点上进行操作时,版本号非常重要。以图2-4为例,如果客户端c1对/config进行写入后,客户端c2也更新了这个节点,客户端c1所知道的版本号已经过时,它的第二次setData操作会失败。


Figure 2-4. Using versions to prevent inconsistencies dueto concurrent updates

ZooKeeper架构

         既然已经从高层级讨论了ZooKeeper暴露给应用程序的操作,我们还需要更多地理解服务是如何工作的。应用程序通过客户端库调用方法,客户端库负责和ZooKeeper服务器进行交互。

         图2-5展示了客户端和服务器端之间的关系。每个客户端导入客户端库,然后就能和ZooKeeper的任意服务器节点通信。


Figure 2-5. ZooKeeper architecture overview

ZooKeeper服务器有两种运行模式:独立和集群(法定人数quorum)。独立模式顾名思义使用单机,ZooKeeper状态不进行复制。集群模式下,一组ZooKeeper服务器,我们称之为ZooKeeper集合(ZooKeeper ensemble),它们复制状态,一起服务于客户端请求。从现在开始,我们使用术语ZooKeeper集合表示一组(台)安装的服务器,可能是一台运行在独立模式下的单机,也可能是运行在集群模式下的一组服务器。

ZooKeeper Quorums

         在集群模式下,ZooKeeper集合中的所有服务器都复制数据。如果客户端必须等待所有服务器存储好数据之后才能继续操作,延迟可能会难以接受。在公共事务中,quorum是举行投票的法定最低人数。在ZooKeeper中,quorum指支持ZooKeeper工作的最低正常运行服务器数量,同时也是数据安全存储的最少服务器数量。如果我们一共有5台ZooKeeper服务器,那么quorum是3。只要有3台服务器已经存储好数据,客户端就能够继续进行下一步操作,而其他两条服务器最终会同步存储状态。

         选择足够的quorum大小至关重要。Quorum必须保证,不管系统是否发生延迟和崩溃,任何已得到确认的更新请求都会被持久化。

         为了便于理解,我们举一个quorum设置太小导致错误的例子。假设我们有5台服务器,quorum被设置成2。服务器s1和s2确认了创建/z节点的操作,然后服务返回给用户告知znode已经被创建。这时候,如果s1和s2在将数据复制到其他服务器之前,与其他服务器以及客户端的连接都中断了。服务继续正常运行,因为有3台服务器可用,但是这3台机器都没能看到新的/z节点,于是,创建/z的请求没有被持久化。

         这是第一章中提到的“脑分裂”现象。为了避免这个问题,前面例子中的quorum大小至少得为3,这才是5台服务器集合中的大多数。ZooKeeper集合需要至少3台服务器可用才能继续正常运行。同样,确认更新请求完成,ZooKeeper集合也需要至少3台服务器进行确认。由于可能的quorum(3)之间至少会相交1台服务器,那么每次成功完成的更新操作,至少有1台可用服务器上保存了这次更新。

         使用这种大多数机制,可以在f台服务器崩溃的情况下容错(f小于集合服务器数量的一半)。比如,如果5台服务器,可以对2台服务器崩溃容错。服务器集合数量不一定非得为奇数,但实际上偶数会削弱系统的容错能力。比如,如果集合使用4台服务器,大多数得是3,系统只能容错1台服务器崩溃。底线是我们应该总是选择奇数台服务器。

         我们也可以不设置quorum为大多数,这个高级特性在第十章讨论。

会话

         在执行请求之前,客户端必须和服务端建立会话。会话的概念对ZooKeeper操作至关重要。客户端提交的所有操作都和某个会话绑定在一起。当一次会话结束,会话期间创建的临时节点都会消失。

         当客户端创建一个ZooKeeper句柄时,就建立了会话。客户端开始连接到集合中的任意一台服务器,通过TCP和服务器进行交互。但是一段时间后,如果客户端从当前服务器接收不到消息,会话可以转移到其他服务器上。会话转移由ZooKeeper客户端库自动处理,对用户是透明的。

         会话提供有序性保证,一次会话内的请求按照FIFO的顺序执行。一般一个客户端只开启单个会话,所以它的请求都按照FIFO顺序执行。如果一个客户端开启了多个会话,会话之间的请求顺序是不确定的。以下场景可能发生:

  • 客户端建立会话,连续调用两次异步操作,create /tasks和/workers。
  • 会话过期
  • 客户端再次建立会话,异步调用create /assign。

在这一系列调用中,可能只有/tasks和/assign被成功创建,第一个会话内部是FIFO有序的,而会话之间的有序性被破坏。

ZooKeeper入门指南

首先下载ZooKeeper分发包。ZooKeeper是Apache下项目(http://zookeeper.apache.org)。在项目主页的下载链接里,可以下载到压缩的TAR文件,文件名类似zookeeper-3.4.5.tar.gz。在Linux,Mac OSX或者其他类UNIX系统上,使用命令解压:

# tar-xvzf zookeeper-3.4.5.tar.gz

如果你使用Windows,需要用WinZip等工具进行解压缩。

你还要安装Java,运行ZooKeeper需要Java6以上版本

在分发包目录下,bin目录下有ZooKeeper启动脚本。以.sh结尾的脚本适用于UNIX平台(Linux,Mac OSX等),以.cmd结尾的脚本适用于Windows。Conf目录下是配置文件。Lib目录包含运行ZooKeeper所需要的第三方jar包。后面我们用{PATH_TOZ_ZK}来引用分发包目录。

首次ZooKeeper会话

         我们使用分发包中/bin目录下的zkServer和zkCli工具,在本地以独立模式运行ZooKeeper并创建会话。有经验的管理员一般使用这些工具进行调试和管理,它们对新手熟悉ZooKeeper也非常有帮助。

         切换到ZooKeeper根目录,重命名样例配置文件:  

 #mv conf/zoo_sample.cfg conf/zoo.cfg
         虽然是可选地,但最好将数据目录移出/tmp以免ZooKeeper塞满你的根分区。修改zoo.cfg中的配置项:        

dataDir=/users/me/zookeeper

最后启动服务器:

#bin/zkServer.sh startJMX enabled by defaultUsing config: ../conf/zoo.cfgStarting zookeeper ... STARTED#

上面的启动命令让ZooKeeper在后台运行。如果你想让服务在前台运行,以便观察输出日志,执行:        

 #bin/zkServer.shstart-foreground

start-foreground选项会输出服务器操作的详细日志。

打开新的shell,启动客户端:        

 # bin/zkCli.sh...<some omitted output>...2012-12-06 12:07:23,545 [myid:] - INFO [main:ZooKeeper@438] -1Initiating client connection, connectString=localhost:2181sessionTimeout=30000 watcher=org.apache.zookeeper.ZooKeeperMain$MyWatcher@2c641e9aWelcome to ZooKeeper!2012-12-06 12:07:23,702 [myid:] - INFO [main-SendThread  2(localhost:2181):ClientCnxn$SendThread@966]- Openingsocket connection to serverlocalhost/127.0.0.1:2181.Will not attempt to authenticate usingSASL (Unable tolocate a login configuration)JLine support is enabled2012-12-06 12:07:23,717 [myid:] - INFO[main-SendThread  3(localhost:2181):ClientCnxn$SendThread@849]- Socketconnection established tolocalhost/127.0.0.1:2181, initiatingsession [zk:localhost:2181(CONNECTING) 0]2012-12-06 12:07:23,987 [myid:] - INFO[main-SendThread  4(localhost:2181):ClientCnxn$SendThread@1207]- Sessionestablishment complete on serverlocalhost/127.0.0.1:2181,sessionid = 0x13b6fe376cd0000, negotiatedtimeout = 30000WATCHER::WatchedEvent state:SyncConnectedtype:None path:null 5

  1. 客户端开始建立会话。
  2. 客户端试图连接到localhost/127.0.0.1:2181。
  3. 客户端连接成功,服务器开始初始化新的会话。
  4.  会话初始化完成。
  5. 服务器向客户端发送SyncConnected事件。

观察输出日志,前面的几行告知我们各种环境变量设置和客户端使用的Jar包。先忽略它们,集中关注会话建立部分。

输出日志后半段是会话建立相关消息。首先“Initiating client connection”表示正在初始化连接,连接字符串“localhost/127.0.0.1:2181”表示客户端正在试图连接到其中一台服务器。因为在独立模式下,所以连接到localhost本地服务。接下来是SASL相关日志,先忽略掉,紧接其后是客户端和本地服务器成功建立TCP连接的确认消息,然后会话建立,生成会话ID0x13b6fe376cd0000。最后,客户端库通知应用程序发生了SyncConnected事件,应用端应当实现Watcher接口处理事件。我们在后面章节讨论事件。

为了逐步熟悉ZooKeeper,我们列出根节点下子节点,并创建一个节点。首先要确认的是,除了用于保存ZooKeeper服务元数据的/zookeeper节点外,节点树为空:

WATCHER::WatchedEvent state:SyncConnectedtype:None path:null[zk: localhost:2181(CONNECTED) 0] ls /[zookeeper]
我们执行ls /命令,输出显示只存在/zookeeper节点,然后创建/wokers节点并查看:         

WATCHER::WatchedEvent state:SyncConnectedtype:None path:null[zk: localhost:2181(CONNECTED) 0][zk: localhost:2181(CONNECTED) 0] ls /[zookeeper][zk: localhost:2181(CONNECTED) 1]create /workers ""Created /workers[zk: localhost:2181(CONNECTED) 2] ls /[workers, zookeeper][zk: localhost:2181(CONNECTED) 3]                
删除节点并退出,圆满完成练习:       

[zk: localhost:2181(CONNECTED) 3] delete /workers[zk: localhost:2181(CONNECTED) 4] ls /[zookeeper][zk: localhost:2181(CONNECTED) 5] quitQuitting...2012-12-06 12:28:18,200 [myid:] - INFO[main-EventThread:ClientCnxn$EventThread@509] - EventThread shutdown2012-12-06 12:28:18,200 [myid:] - INFO[main:ZooKeeper@684] - Session:0x13b6fe376cd0000 closed观察到/workers节点被删除,会话关闭。最后停止ZooKeeper服务:#bin/zkServer.sh stopJMX enabled by defaultUsing config: ../conf/zoo.cfgStopping zookeeper ... STOPPED#

会话状态和生命周期

         会话的生命周期从创建开始,持续到正常关闭或者因超时而过期。我们介绍会话周期内可能的状态和能够使改变状态的事件。

         会话状态是自解释的:CONNECTING,CONNECTED,CLOSED和NOT_CONNECTED。状态转化取决于客户端和服务端之间的不同事件。

 

Figure 2-6. Session states and transitions

会话最初状态是NOT_CONNECTED,客户端完成初始化之后,会话状态转变为CONNECTING(1)。正常情况下,客户端连接上服务器,会话状态转变为CONNECTED(2)。当客户端连接丢失,或者接收不到服务器的心跳消息,状态转换为CONNECTING(3),并试图连接其他服务器,如果能够连接上新的服务器,或者重新连接到原服务器并且服务器确认会话依然有效,那么状态又回到CONNECTED,否则会话过期,状态变为CLOSED(4)。应用程序也可以显式关闭会话(4和5)。

创建会话时,设置会话超时时间是非常重要的。会话超时时间t代表ZooKeeper服务端宣告会话过期的最长时间。如果服务器在时间t内都没有接收到会话发送的消息,就认为会话过期。客户端如果在t/3时间内没有收到服务器消息,它会向服务器发送心跳消息。在2t/3时,客户端开始试图连接新的服务器,在会话过期之前,它还有t/3的时间自救。

在客户端试图连接到新的服务器时,新服务器的数据状态不能落后于老服务器。当服务器还没有同步客户端已经观察到的数据更新操作时,客户端不能选择它进行连接。ZooKeeper通过排序所有更新操作来决定同步状态。所有更新操作是全局有序的,所以如果客户端已经观察到序号为i的更新操作,它就不能连接只同步到i’<i更新操作的服务器。

图2-7演示了事务ID(zxids)在重新连接中的作用。当客户端超时,从服务器s1断开连接,这时候服务器s2状态滞后,而服务器s3的状态和服务器s1同步,所以最终客户端连接上服务器s3


Figure 2-7. Example of client reconnecting

ZooKeeper集群

         之前使用的配置文件仅适用于独立模式。如果单个服务挂了,整个ZooKeeper服务停止。这不太符合一个协同服务所作出的可靠性承诺。为了真正实现可靠性,我们需要在多台机器上运行ZooKeeper。

         幸运的是,只需要对配置进行一些调整,就可以在单机上模拟运行多个ZooKeeper服务。

         服务器之间必须能够互相通信。理论上,可以使用多播,但是必须考虑跨网段部署,以及在同一网段内部署多个ZooKeeper集群的情况,所以使用以下配置文件:

tickTime=2000initLimit=10syncLimit=5dataDir=./dataclientPort=2181server.1=127.0.0.1:2222:2223server.2=127.0.0.1:3333:3334server.3=127.0.0.1:4444:4445

我们先解释最后三行。其余配置项将在第十章进行讲解。

server.n配置项指定了ZooKeeper服务器n的地址和端口。配置项的值用冒号隔开分成三列。第一列是服务器n的主机名或者IP地址。第二列是服务器之间通信的TCP端口,第三列是Leader选举使用的端口。因为我们在同一台机器上启动了三个服务进程,所以需要使用不同的端口。正常情况下,如果在多台服务器上运行ZooKeeper,每台服务器都使用相同的端口号。

还需要设置data数据目录,执行以下命令:

mkdir z1mkdir z1/datamkdir z2mkdir z2/datamkdir z3mkdir z3/data
服务启动时,需要知道自己的服务器ID。它会从数据目录下的myid文件读取服务器ID,所以执行:

echo 1 > z1/data/myidecho 2 > z2/data/myidecho 3 > z3/data/myid

服务启动后会根据配置文件中dataDir项参数找到数据目录,从myid文件中加载服务器ID,然后使用相应的server.n配置项监听相关端口。当ZooKeeper运行于多台服务器时,可以使用相同的客户端连接端口,最好使用完全相同的配置文件。在我们的例子中,因为服务运行在单台服务器上,所以也需要对客户端连接端口作出定制化调整。

使用这一节最开始讨论的配置文件内容创建z1/z1.cfg,当创建z3/z3.cfg和z3/z3.cfg是,将clientPort修改成2182和2183.

现在可以启动服务了。先启动z1:

$cdz1$ {PATH_TO_ZK}/bin/zkServer.sh start ./z1.cfg

日志被记录在zookeeper.out文件中。因为只启动了三台ZooKeeper服务器中的server.1,所以服务还不正常,输出以下日志记录:

... [myid:1] - INFO[QuorumPeer[myid=1]/...:2181:QuorumPeer@670] - LOOKING... [myid:1] - INFO[QuorumPeer[myid=1]/...:2181:FastLeaderElection@740] -New election. My id = 1, proposedzxid=0x0... [myid:1] - INFO[WorkerReceiver[myid=1]:FastLeaderElection@542] -Notification: 1 ..., LOOKING (mystate)... [myid:1] - WARN[WorkerSender[myid=1]:QuorumCnxManager@368] - Cannotopen channel to 2 at election address/127.0.0.1:3334Java.net.ConnectException: Connectionrefusedatjava.net.PlainSocketImpl.socketConnect(Native Method)atjava.net.PlainSocketImpl.doConnect(PlainSocketImpl.java:351)

Server 1不停地试图连接到其他服务器。这时候再启动一台服务器,就能组成一个集群。  

$ cd z2$ {PATH_TO_ZK}/bin/zkServer.sh start ./z2.cfg

检查server 2的zookeeper.out日志输出文件,能看到以下内容:

... [myid:2] - INFO[QuorumPeer[myid=2]/...:2182:Leader@345] - LEADING- LEADER ELECTION TOOK - 279... [myid:2] - INFO[QuorumPeer[myid=2]/...:2182:FileTxnSnapLog@240] -Snapshotting: 0x0 to ./data/version-2/snapshot.0

这表示server 2被选举为Leader。这时候server 1的日志输出是:

... [myid:1] - INFO[QuorumPeer[myid=1]/...:2181:QuorumPeer@738] -FOLLOWING... [myid:1] - INFO[QuorumPeer[myid=1]/...:2181:ZooKeeperServer@162] -Created server ...... [myid:1] - INFO[QuorumPeer[myid=1]/...:2181:Follower@63] - FOLLOWING- LEADER ELECTION TOOK – 212

Server 1变成了server 2的活动Follower。现在三台服务器中的两台都被启动了,所以我们已经拥有一个多数可用的quorum(2/3)。

服务可用后,就可以配置客户端连接服务了。连接串包含组成服务的所有服务器的主机和端口号。我们使用“127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183”。(尽管server3还没有被启动,字符串中依然可以包含它,这用来演示ZooKeeper的一些有用特性。)

使用zkCli访问集群:

$ {PATH_TO_ZK}/bin/zkCli.sh -server 127.0.0.1:2181,127.0.0.1:2182,127.0.0.1:2183

         如果连接上了服务,会输出以下消息:

[myid:] - INFO [...] - Session establishmentcomplete on server localhost/127.0.0.1:2182 ...

注意看其中的端口号是2182。如果使用Ctrl-C结束客户端,再重新连接几次,端口号可能会在2182和2182之间变化。并且还能观察到试图连接到2183端口失败,紧接着成功连接上其他端口的消息。

上面的连接尝试展示了ZooKeeper通过运行多台服务器来实现可靠性(当然,在生产环境中,会使用多台不同的物理机器)。在本书大部分情况下,我们都在开发环境下使用单机模式,这样更方面启动和管理,并且例子更加直接。除了连接字符串的不同,对客户端来讲,多少台服务器组成服务都一样,这是ZooKeeper的优点之一。

用ZooKeeper实现原语:锁

         我们介绍一个使用ZooKeeper实现临界锁的简单例子。锁包括各种类型,比如读/写锁,全局锁等。通过ZooKeeper实现锁也有多种方法。先不考虑各种类型的锁,只使用一种最简单的方式来实现临界锁,用来演示应用程序中如何使用ZooKeeper。

         如果你的应用中,n个进程试图获取锁,因为ZooKeeper不直接暴露原语,所以要用ZooKeeper操作节点的方法来实现锁。每个进程都尝试创建/lock节点来获取锁,如果进程p成功创建了/lock节点,那么它就持有了锁,可以执行临界段代码。一个潜在的问题是,进程p可能崩溃,但是不释放锁。在这种情况下,其他进程再也不能获得锁,系统卡在死锁状态。为了避免这种情况,我们只需要在创建/lock节点时选择临时类型就可以了。

         只要/lock节点还存在,其他进程试图创建/lock的操作都会失败。它们可以监听/lock节点,当得知/locak节点被删除时,再次尝试获取锁。当进程p’收到/lock被删除的通知消息时,如果其他进程已经抢先创建了节点,而进程p’仍然还想获取锁,那么它必须重复试图创建——监听节点这一过程。

Master-Worker示例实现

         在本节中,我们使用zkCli实现Master-Worker例子中的一些基本功能。这个例子仅限于讲解目的,我们并不推荐使用zkCli构建系统。使用zkCli,并且忽略很多真实实现中需要考虑的细节,目的仅在于展示如何基于ZooKeeper实现各种协同方法。在下一章中,我们将会实现这些细节。

         Master-Worker模型中包含三种角色:

Master

         Master监视新的Worker和任务,分配任务给可用的Worker。

Worker

        Worker注册自身到系统中来,保证master能够给它分配任务,监听分配到的新任务

客户端

         客户端提交新任务,等待系统响应。

         我们逐个介绍这三个不同角色,介绍它们的执行步骤。

Master角色

         只有一个进程能充当Master,所以Master进程必须持有锁,这通过创建一个/master临时节点来实现:         

[zk: localhost:2181(CONNECTED) 0] create -e /master"master1.example.com:2223"1Created /master[zk: localhost:2181(CONNECTED) 1] ls / 2[master, zookeeper][zk: localhost:2181(CONNECTED) 2] get/master3"master1.example.com:2223"cZxid = 0x67ctime = Tue Dec 11 10:06:19 CET 2012mZxid = 0x67mtime = Tue Dec 11 10:06:19 CET 2012pZxid = 0x67cversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x13b891d4c9e0005dataLength = 26numChildren = 0[zk: localhost:2181(CONNECTED) 3]

  1. 创建/master节点获取控制权。使用-e参数表示创建的是临时节点。
  2. 列出ZooKeeper根节点下子节点。
  3. 获取/master节点的数据及元数据信息。

上面都发生了些什么?我们首先创建了/master临时节点。/master节点的数据内容是主机信息,主机信息不是必须的,添加主机信息是为了让其他和ZooKeeper交互的进程能够知悉Master的信息。使用-e参数创建临时节点,当会话关闭或者过期时,节点会被自动删除。

如果使用两个进程竞争Master角色,在同一时间内,它们之中最多只能有一个活动Master,另外一个进程成为备用master。如果另外这个进程,它不知道Master已经存在,也试图创建/master节点:

[zk: localhost:2181(CONNECTED) 0]create -e /master "master2.example.com:2223"Node already exists: /master[zk: localhost:2181(CONNECTED) 1]

ZooKeeper告诉我们/master节点已经存在。这样,第二个进程得知Master已经存在。尽管如此,当前活动Master可能崩溃,备用Master必须接管活动Master的角色,所以需要在/master节点上设置一个watch:

[zk: localhost:2181(CONNECTED) 0] create-e /master "master2.example.com:2223"Node already exists: /master[zk: localhost:2181(CONNECTED) 1] stat/master truecZxid = 0x67ctime = Tue Dec 11 10:06:19 CET 2012mZxid = 0x67mtime = Tue Dec 11 10:06:19 CET 2012pZxid = 0x67cversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x13b891d4c9e0005dataLength = 26numChildren = 0[zk: localhost:2181(CONNECTED) 2]
stat命令获取节点数据的同时,使用true参数设置了一个watch,监视节点是否存在。如果当前活动Master崩溃,会观察到以下消息:

[zk: localhost:2181(CONNECTED) 0]create -e /master "master2.example.com:2223"Node already exists: /master[zk: localhost:2181(CONNECTED) 1] stat/master truecZxid = 0x67ctime = Tue Dec 11 10:06:19 CET 2012mZxid = 0x67mtime = Tue Dec 11 10:06:19 CET 2012pZxid = 0x67cversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x13b891d4c9e0005dataLength = 26numChildren = 0[zk: localhost:2181(CONNECTED) 2]WATCHER::WatchedEvent state:SyncConnectedtype:NodeDeleted path:/master[zk: localhost:2181(CONNECTED) 2] ls /[zookeeper][zk: localhost:2181(CONNECTED) 3]

注意输出结束部分的NodeDeleted事件。事件表示活动Master的会话已经关闭或者过期,/master节点被删除。这时候,备用Master通过再次创建/master节点成为活动master。

[zk: localhost:2181(CONNECTED) 0]create -e /master "master2.example.com:2223"Node already exists: /master[zk: localhost:2181(CONNECTED) 1] stat/master truecZxid = 0x67ctime = Tue Dec 11 10:06:19 CET 2012mZxid = 0x67mtime = Tue Dec 11 10:06:19 CET 2012pZxid = 0x67cversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x13b891d4c9e0005dataLength = 26numChildren = 0[zk: localhost:2181(CONNECTED) 2]WATCHER::WatchedEvent state:SyncConnectedtype:NodeDeleted path:/master[zk: localhost:2181(CONNECTED) 2] ls /[zookeeper][zk: localhost:2181(CONNECTED) 3]create -e /master "master2.example.com:2223"Created /master[zk: localhost:2181(CONNECTED) 4]

当成功创建/master节点后,它成为活动Master。

Workers,任务和分配

在开始讨论Workers和客户端执行步骤之前,先创建三个重要的父节点:/workers,/tasks和/assign:

[zk: localhost:2181(CONNECTED) 0]create /workers ""Created /workers[zk: localhost:2181(CONNECTED) 1] create/tasks ""Created /tasks[zk: localhost:2181(CONNECTED) 2]create /assign ""Created /assign[zk: localhost:2181(CONNECTED) 3] ls /[assign, tasks, workers, master,zookeeper][zk: localhost:2181(CONNECTED) 4]

这是三个不包含数据的持久化节点。我们用它们来分别表示可用的workers、待分配的任务和任务分配情况。

在实际应用中,这三个节点要么由活动Master在进行任务分配之前创建,要么在启动过程中被创建。不管它们是如何被创建的,一旦这些节点存在,Master都必须监视/workers和/tasks子节点的变化情况。

[zk: localhost:2181(CONNECTED) 4] ls/workers true[][zk: localhost:2181(CONNECTED) 5] ls/tasks true[][zk: localhost:2181(CONNECTED) 6]

和之前stat/master节点一样,我们在ls后也使用了可选参数true。在这里,true参数表示在节点上创建了一个watch,监视子节点的变化情况。

Worker角色

一开始,Worker在/worker下创建一个临时子节点,通知Master它可用,等待执行任务。Worker使用主机名标识自身:

[zk: localhost:2181(CONNECTED) 0]create -e /workers/worker1.example.com"worker1.example.com:2224"Created /workers/worker1.example.com[zk: localhost:2181(CONNECTED) 1]

ZooKeeper确认节点创建成功。因为之前Master在/workers上设置了watch监视子节点变化,所以一旦worker在/workers下创建子节点成功,master会观察到如下通知:

WATCHER::WatchedEvent state:SyncConnected type:NodeChildrenChangedpath:/workers

然后,worker创建一个父节点,/assign/worker1.exampel.com,用来接收被分配的任务,并且通过执行ls true来监视新任务:

[zk: localhost:2181(CONNECTED) 0]create -e /workers/worker1.example.com"worker1.example.com:2224"Created /workers/worker1.example.com[zk: localhost:2181(CONNECTED) 1]create /assign/worker1.example.com ""Created /assign/worker1.example.com[zk: localhost:2181(CONNECTED) 2] ls/assign/worker1.example.com true[][zk: localhost:2181(CONNECTED) 3]

现在Worker已经准备好接收新任务分配。接下来,我们讨论客户端角色。

客户端角色

         客户端提交任务到系统。为了简化例子,我们不太关注任务具体内容。假设任务只是运行cmd命令,客户端提交任务:

[zk: localhost:2181(CONNECTED) 0] create -s /tasks/task-"cmd"Created /tasks/task-0000000000
我们使用顺序节点来添加任务,任务形成一个有序队列。客户端等待任务被执行。执行任务的Worker在任务完成后,会创建一个状态节点。客户端监视状态节点的创建,通过观察状态节点来获取任务的完成状态:

[zk: localhost:2181(CONNECTED) 1] ls/tasks/task-0000000000 true[][zk: localhost:2181(CONNECTED) 2]

Worker创建了/tasks/task-0000000000状态节点,所以它需要通过ls /tasks/task-0000000000 true监视其子节点的变化情况。

任务节点被创建后,Master会观察到以下事件:

[zk: localhost:2181(CONNECTED) 6]WATCHER::WatchedEvent state:SyncConnected type:NodeChildrenChangedpath:/tasks

Master检查新的任务,获取可用的Worker列表,然后将新任务分配给worker1.example.com:

[zk: 6] ls /tasks[task-0000000000][zk: 7] ls /workers[worker1.example.com][zk: 8] create /assign/worker1.example.com/task-0000000000""Created/assign/worker1.example.com/task-0000000000[zk: 9]

Worker收到被分配新任务的通知:

[zk: localhost:2181(CONNECTED) 3]WATCHER::WatchedEvent state:SyncConnectedtype:NodeChildrenChangedpath:/assign/worker1.example.com
Worker检查新任务,得知任务被分配给了自己:

WATCHER::WatchedEvent state:SyncConnectedtype:NodeChildrenChangedpath:/assign/worker1.example.com[zk: localhost:2181(CONNECTED) 3] ls/assign/worker1.example.com[task-0000000000][zk: localhost:2181(CONNECTED) 4]Worker完成任务后,添加一个状态子节点:[zk: localhost:2181(CONNECTED) 4]create /tasks/task-0000000000/status "done"Created /tasks/task-0000000000/status[zk: localhost:2181(CONNECTED) 5]
Worker再次接收到节点变化通知,检查结果:

[zk: localhost:2181(CONNECTED) 2] get/tasks/task-0000000000"cmd"cZxid = 0x7cctime = Tue Dec 11 10:30:18 CET 2012mZxid = 0x7cmtime = Tue Dec 11 10:30:18 CET 2012pZxid = 0x7ecversion = 1dataVersion = 0aclVersion = 0ephemeralOwner = 0x0dataLength = 5numChildren = 1[zk: localhost:2181(CONNECTED) 3] get/tasks/task-0000000000/status"done"cZxid = 0x7ectime = Tue Dec 11 10:42:41 CET 2012mZxid = 0x7emtime = Tue Dec 11 10:42:41 CET 2012pZxid = 0x7ecversion = 0dataVersion = 0aclVersion = 0ephemeralOwner = 0x0dataLength = 8numChildren = 0[zk: localhost:2181(CONNECTED) 4]
客户端检查状态节点的内容判断任务的执行情况。在上面的例子中,状态节点内容为“done”表示任务运行成功。当然,任务可以更复杂,甚至涉及到其他分布式系统,但是最根本的是,不管任务具体内容是什么,通过ZooKeeper执行任务、传输任务结果的机制本质上都是一致的。


0 0
原创粉丝点击