Chapter 9 ZooKeeper 内部原理

  • 群首(leader):作为中心点处理所有对ZooKeeper系统变更的请求,它就像一个定序器,建立了所有对ZooKeeper状态的更新的顺序。
  • 追随者(follower):接收群首所发出更新操作请求,并对这些请求进行处理,以此来保障状态更新操作不会发生碰撞。
  • 观察者(observer):不会参与决策哪些请求可被接受的过程,只是观察决策的结果,观察者的设计只是为了系统的可扩展性。

一、请求、事务和标识符

  • 读请求(exists、getData和getChildren):本地处理
  • 写请求(create、delete和setData):被转发给群首,群首执行相应的请求,并形成状态的更新,称为事务(transaction)

    • 一个事务为一个单位,也就是说所有的变更处理需要以原子方式执行
    • ZooKeeper集群以事务方式运行,并确保所有的变更操作以原子方式被执行,同时不会被其他事务所干扰
    • 同时一个事务还具有幂等性
    • 当群首产生了一个事务,就会为该事务分配一个标识符,称之为ZooKeeper会话ID(zxid),通过Zxid对事务进行标识,就可以按照群首所指定的顺序在各个服务器中按序执行。
    • 服务器之间在进行新的群首选举时也会交换zxid信息,这样就可以知道哪个无故障服务器接收了更多的事务,并可以同步他们之间的状态信息。
    • zxid为一个long型(64位)整数,分为两部分:时间戳(epoch)部分和计数器(counter)部分,每个部分为32位。
    • zab协议,通过该协议来广播各个服务器的状态变更信息

二、群首选举

设置群首的目的是为了对客户端所发起的ZooKeeper状态变更请求进行排序,包括:create、setData和delete操作。

群首将每一个请求转换为一个事务,将这些事务发送给追随者,确保集群按照群首确定的顺序接受并处理这些事务。

LOOKING --> LEADING --> FOLLOWING

当一个服务器收到一个投票信息,该服务器将会根据以下规则修改自己的投票信息:

  • 1、将接收的voteId和voteZxid作为一个标识符,并获取接收方当前的投票中的zxid,用myZxid和mySid表示接收方服务器自己的值。
  • 2、如果(voteZxid > myZxid)或者(voteZxid = myZxid且voteId > mySid),保留当前的投票信息。
  • 3、否则,修改自己的投票信息,将voteZxid赋值给myZxid,将voteId赋值给mySid。
简而言之,只有最新的服务器将赢得选举,因为其拥有最近一次的zxid。

【图9-1:群首选举过程的示例】
Chapter 9 ZooKeeper 内部原理

图9-1展示了三个服务器,这三个服务器分别以不同的初始投票值开始,其投票值取决于该服务器的标识符和其最新的zxid。每个服务器会收到另外两个服务器发送的投票信息,在第一轮之后,服务器s2和服务器s3将会改变其投票值为(1,6),之后服务器服务器s2和服务器s3在改变投票值之后会发送新的通知消息,在接收到这些新的通知消息后,每个服务器收到的仲裁数量的通知消息拥有一样的投票值,最后选举出服务器s1为群首。

【图9-2:消息交错导致一个服务器选择了另一个群首】
Chapter 9 ZooKeeper 内部原理

在图9-2中,展示了另一种情况的例子。服务器 s2 做出了错误判断,选举了另一个服务器 s3 而不是服务器 s1 ,虽然 s1 的zxid值更高,但在从服务器 s1 向服务器 s2 传送消息时发生了网络故障导致长时间延迟,与此同时,服务器 s2 选择了服务器s 3 作为群首,最终,服务器 s1 和服务器 s3 组成了仲裁数量(quorum),并将忽略服务器 s2 。

【图9-3:群首选举时的长延迟】
Chapter 9 ZooKeeper 内部原理

如果想实现一个新的群首选举的算法,需要实现一个quorum 包中的Election接口。为了可以让用户自己选择群首选举的实现,代码中使用了简单的整数标识符(请查看代码中QuorumPeer.createElectionAlgorithm()),另外两种可选的实现方式为LeaderElection类和AuthFastLeaderElection类

三、Zab:状态更新的广播协议

在接收到一个写请求操作后,追随者会将请求转发给群首,群首将探索性地执行该请求,并将执行结果以事务的方式对状态更新进行广播。

一个事务中包含服务器需要执行变更的确切操作,当事务提交时,服务器就会将这些变更反馈到数据树上,其中数据树为ZooKeeper用于保存状态信息的数据结构(请参考DataTree类)。

之后需要面对的问题便是服务器如何确认一个事务是否已经提交,由此引入了所采用的协议:
Zab:ZooKeeper原子广播协议(ZooKeeper Atomic Broadcast protocol)。

假设现在有一个活动的群首服务器,并拥有仲裁数量的追随者支持该群首的管理权,通过该协议提交一个事务非常简单,类似于一个两阶段提交。

【图9-4:提交提案的常规消息模式】
Chapter 9 ZooKeeper 内部原理

  • 1、群首向所有追随者发送一个PROPOSAL消息p。
  • 2、当一个追随者接收到消息p后,会响应群首一个ACK消息,通知群首其已接受该提案(proposal)。
  • 3、当收到仲裁数量的服务器发送的确认消息后(该仲裁数包括群首自己),群首就会发送消息通知追随者进行提交(COMMIT)操作。
在应答提案消息之前,追随者还需要执行一些检查操作。追随者将会检查所发送的提案消息是否属于其所追随的群首,并确认群首所广播的提案消息和提交事务消失的顺序正确。

【Zab保障了以下几个重要属性:】

  • 如果群首按顺序广播了事务T和事务T,那么每个服务器在提交T事务前保证事务T已经提交完成。
  • 如果某个服务器按照事务T、事务T的顺序提交事务,所有其他服务器也必然会在提交事务T前提交事务T。

第一个属性保证事务在服务器之间的传送顺序的一致,而第二个竖向地保证服务器不会跳过任何事务。

多个并发的群首可能会导致服务器提交事务的顺序发生错误,或者直接跳过了某些事务。为了阻止系统中同时出现两个服务器自认为自己是群首的情况是非常困难的,时间问题或消息丢失都可能导致这种情况,因此广播协议并不能基于以上假设。

【为了解决这个问题,Zab协议提供了以下保障】:

  • 一个被选举的群首确保在提交完所有之前的时间戳内需要提交的事务,之后才开始广播新的事务。
  • 在任何时间点,都不会出现两个被仲裁支持的群首。

第一个需求,群首并不会马上处于活动状态,直到确保仲裁数量的服务器认可这个群首新的时间戳值。一个时间戳的最初状态必
须包含所有的之前已经提交的事务,或者某些已经被其他服务器接受,但尚未提交完成的事务。这一点非常重要,在群首进行时间戳e的任何新的提案前,必须保证自时间戳开始值到时间戳e-1内的所有提案被提交。如果一个提案消息处于时间戳e'<e,在群首处理时间戳e的第一个提案消息前没有提交之前的这个提案,那么旧的提案将永远不会被提交。

第二个需求有些棘手,因为并不能完全阻止两个群首独立地运行。假如一个群首l管理并广播事务,在此时,仲裁数量的服务器Q判断群首l已经退出,并开始选举了一个新的群首l',假设在仲裁机构Q放弃群首l时有一个事务T正在广播,而且仲裁机构Q的一个严格的子集记录了这个事务T,在群首l'被选举完成后,在仲裁机构Q之外服务器也记录了这个事务T,为事务T形成一个仲裁数量,在这种情况下,事务T在群首l'被选举后会进行提交。不用担心这种情况,这并不是个bug,Zab协议保证T作为事务的一部分被群首l'提交,确保群首l'的仲裁数量的支持者中至少有一个追随者确认了该事务T,其中的关键点在于群首l'和l在同一时刻并未获得足够的仲裁数量的支持者。

【图9-5:群首发生重叠的情况】
Chapter 9 ZooKeeper 内部原理

在图中,群首l为服务器s 5 ,l'为服务器s 3 ,仲裁机构由s 1 到s 3 组成,事务T的zxid为(1,1)。在收到第二个确认消息之后,服务器s 5 成功向服务器s 4 发送了提交消息来通知提交事务。

之前提到Zab保证新群首l'不会缺失(1,1),现在来看看其中的细节。在新群首l'生效前,它必须学习旧的仲裁数量服务器之前
接受的所有提议,并且保证这些服务器不会继续接受来自旧群首的提议。此时,如果群首l还能继续提交提议,比如(1,1),这条提议必须已经被一个以上的认可了新群首的仲裁数量服务器所接受。我们知道仲裁数量必须在一台以上的服务器之上有所重叠,这样群首l'用来提交的仲裁数量和新群首l使用的仲裁数量必定在一台以上的服务器上是一致的。因此,l'将(1,1)加入自身的状态并传播给其跟随者。

在群首选举时,选择zxid最大的服务器作为群首。这使得ZooKeeper不需要将提议从追随者传到群首,而只需要将状态从群首传
播到追随者。假设有一个追随者接受了一条群首没有接受的提议。群首必须确保在和其他追随者同步之前已经收到并接受了这条提议。但是,如果选择zxid最大的服务器,将可以完完全全跳过这一步,可以直接发送更新到追随者。

在时间戳发生转换时,Zookeeper使用两种不同的方式来更新追随者来优化这个过程:

  • DIFF :追随者滞后于群首不多,群首只需要发送缺失的事务点。因为追随者按照严格的顺序接收事务点,这些缺失的事务点

永远是最近的。

  • SNAP:追随者滞后很久,因为发送完整的快照会增大系统恢复的延时,发送缺失的事务点是更优的选择。可是当追随者滞后太远的情况下,只能选择发送完整快照。

四、观察者

观察者和追随者之间有一些共同点。具体说来,他们提交来自群首的提议。不同于追随者的是,观察者不参与我们之前介绍过的选举过程。他们仅仅学习经由INFORM消息提交的提议。由于群首将状态变化发送给追随者和观察者,这两种服务器也都被称为学习者。

【注意:深入INFORM消息】

因为观察者不参与决定提议接受与否的投票,群首不需要发送提议到观察者,群首发送给追随者的提交消息只包含zxid而不包含提议本身。因此,仅仅发送提交消息给观察者并不能使其实施提议。这是我们使用INFORM消息的原因。INFORM消息本质上是包含了正在被提交的提议信息的提交消息。

简单来说,追随者接受两种消息而观察者只接受一种消息。追随者从一次广播中获取提议的内容,并从接下来的一条提交消息中获取
zxid。相比之下,观察者只获取一条包含已提交提议的内容的INFORM消息。

参与决定那条提议被提交的投票的服务器被称为PARTICIPANT服务器。一个PARTICIPANT服务器可以是群首也可以是追随者。而观察者则被称为OBSERVER服务器。

  • 引入观察者的一个主要原因是提高读请求的可扩展性。
    通过加入多个观察者,可以在不牺牲写操作的吞吐率的前提下服务更多的读操作。写操作的吞吐率取决于仲裁数量的大小。如果加入更多的参与投票的服务器,将需要更大的仲裁数量,而这将减少写操作的吞吐率。增加观察者也不是完全没有开销的。每一个新加入的观察者将对应于每一个已提交事务点引入的一条额外消息。然而,这个开销相对于增加参与投票的服务器来说小很多。
  • 采用观察者的另外一个原因是进行跨多个数据中心的部署。
    由于数据中心之间的网络链接延时,将服务器分散于多个数据中心将明显地降低系统的速度。引入观察者后,更新请求能够先以高吞吐率和低延迟的方式在一个数据中心内执行,接下来再传播到异地的其他数据中心得到执行。值得注意的是,观察者并不能消除数据中心之间的网络消息,因为观察者必须转发更新请求给群首并且处理INFORM消息。不同的是,当参与的服务器处于同一个数据中心时,观察者保证提交更新必需的消息在数据中心内部得到交换。

五、服务器的构成

群首、追随者和观察者根本上都是服务器。
在实现服务器时使用的主要抽象概念是请求处理器。请求处理器是对处理流水线上不同阶段的抽象。每一个服务器实现了一个请求处理器的序列。可以把一个处理器想象成添加到请求处理的一个元素。一条请求经过服务器流水线上所有处理器的处理后被称为得到完全处理。

【注意:请求处理器】
ZooKeeper代码里有一个叫RequestProcessor的接口。
这个接口的主要方法是processRequest,它接受一个Request参数。在一条请求处理器的流水线上,对相邻处理器的请求的处理通常通过队列现实解耦合。当一个处理器有一条请求需要下一个处理器进行处理时,它将这条请求加入队列。然后,它将处于等待状态直到下一个处理器处理完此消息。

1、独立服务器

Zookeeper中最简单的流水线是独立服务器(ZeeKeeperServer类)。图9-6描述了此类服务器的流水线。它包含三种请求处理器:PrepRequestProcessor、SyncRequestProcessor和FinalRequestProcessor。
【图9-6:一个独立服务器的流水线】:
Chapter 9 ZooKeeper 内部原理

  • PrepRequestProcessor:接受客户端的请求并执行这个请求,处理结果则是生成一个事务。我们知道事务是执行一个操作的结果,该操作会反映到ZooKeeper的数据树上。事务信息将会以头部记录和事务记录的方式添加到Request对象中。同时还要注意,只有改变ZooKeeper状态的操作才会产生事务,对于读操作并不会产生任何事务。因此,对于读请求的Request对象中,事务的成员属性的引用值则为null。
  • SyncRequestProcessor:负责将事务持久化到磁盘上。实际上就是将事务数据按顺序追加到事务日志中,并生成快照数据。
  • FinalRequestProcessor:如果Request对象包含事务数据,该处理器将会接受对ZooKeeper数据树的修改,否则,该处理器会从数据树中读取数据并返回给客户端。

2、群首服务器

当切换到仲裁模式时,服务器的流水线则有一些变化,首先我们群首的操作流水线(类LeaderZooKeeper),如图9-7所示。
【图9-7:群首服务器的流水线】
Chapter 9 ZooKeeper 内部原理

  • PrepRequestProcessor:
  • ProposalRequestProcessor:该处理器会准备一个提议,并将该提议发送给跟随者。ProposalRequestProcessor将会把所有请求都转发给CommitRequestProcessor,而且,对于写操作请求,还会将请求转发给SyncRequestProcessor处理器。
  • SyncRequestProcessor:处理器所执行的操作与独立服务器中的一样,即持久化事务到磁盘上。执行完之后会触发AckRequestProcessor处理器。
  • AckRequestProcessor:这个处理器是一个简单请求处理器,它仅仅生成确认消息并返回给自己。之前曾提到过,在仲裁模式下,群首需要收到每个服务器的确认消息,也包括群首自己,而AckRequestProcessor处理器就负责这个。
  • CommitRequestProcessor:CommitRequestProcessor会将收到足够多的确认消息的提议进行提交。实际上,确认消息是由Leader类处理的(Leader.processAck()方法),这个方法会将提交的请求加入到CommitRequestProcessor类中的一个队列中。这个队列会由请求处理器线程进行处理。
  • FinalRequestProcessor:它的作用与独立服务器一样。FinalRequestProcessor处理更新类型的请求,并执行读取请求。在FinalRequestProcessor处理器之前还有一个简单的请求处理器,这个处理器会从提议列表中删除那些待接受的提议,这个处理器的名字叫ToBeAppliedRequestProcessor。待接受请求列表包括那些已经被仲裁法定人数所确认的请求,并等待被执行。群首使用这个列表与追随者之间进行同步,并将收到确认消息的请求加入到这个列表中。之后ToBeAppliedRequestProcessor处理器就会在FinalRequestProcessor处理器执行后删除这个列表中的元素。
    注意,只有更新请求才会加入到待接受请求列表中,然后由ToBeAppliedRequest-Processor处理器从该列表移除。ToBeAppliedRequestProcessor处理器并不会对读取请求进行任何额外的处理操作,而是由FinalRequestProcessor处理器进行操作。

3、追随者和观察者服务器

【图9-8:追随者服务器的流水线】
Chapter 9 ZooKeeper 内部原理

  • FollowerRequestProcessor:首先从FollowerRequestProcessor处理器开始,该处理器接收并处理客户端请求。FollowerRequestProcessor处理器之后转发请求给CommitRequestProcessor,同时也会转发写请求到群首服务器。
  • CommitRequestProcessor:会直接转发读取请求到FinalRequestProcessor处理器,而且对于写请求,CommitRequestProcessor在转发给FinalRequestProcessor处理器之前会等待提交事务。
为了保证执行的顺序,CommitRequestProcessor处理器会在收到一个写请求处理器时暂停后续的请求处理。这就意味着,在一个写请求之后接收到的任何读取请求都将被阻塞,直到读取请求转给CommitRequestProcessor处理器。通过等待的方式,请求可以被保证按照接收的顺序来被执行。
  • SyncRequestProcessor:当群首接收到一个新的写请求操作时,直接地或通过其他追随者服务器来生成一个提议,之后转发到追随者服务器。当收到一个提议,追随者服务器会发送这个提议到SyncRequestProcessor处理器。
  • SendRequestProcessor:会向群首发送确认消息。当群首服务器接收到足够确认消息来提交这个提议时,群首就会发送提交事务消息给追随者(同时也会发送INFORM消息给观察者服务器)。当接收到提交事务消息时,追随者就通过CommitRequestProcessor处理器进行处理。

六、本地存储

1、日志和磁盘的使用

服务器通过事务日志来持久化事务。在接受一个提议时,一个服务器(追随者或群首服务器)就会将提议的事务持久化到事物日志中,该事务日志保存在服务器的本地磁盘中,而事务将会按照顺序追加其后。服务器会时不时地滚动日志,即关闭当前文件并打开一个
新的文件。

【组提交和补白】:因为写事务日志是写请求操作的关键路径,因此ZooKeeper必须有效处理写日志问题。一般情况下追加文件到磁盘都会有效完成,但还有一些情况可以使ZooKeeper运行的更快,组提交和补白。组提交(GroupCommits)是指在一次磁盘写入时追加多个事务。这将使持久化多个事物只需要一次磁道寻址的开销。

【冲刷(Flush)事务到磁盘介质】:冲刷在这里就是指告诉操作系统将脏页写入磁盘,并在操作完成后返回。因为在SyncRequestProcessor处理器中持久化事务,所以这个处理器同时也会负责冲刷。在SyncRequestProcessor处理器中当需要冲刷事务到磁盘时,事实上是冲刷的是所有队列中的事务,以实现组提交的优化。如果队列中只有一个事务,这个处理器依然会执行冲刷。该处理器并不会等待更多的事务进入队列,因为这样做会增加执行操作的延时。

【补白(padding)】:指在文件中预分配磁盘存储块。这样做,对于涉及存储块分配的文件系统元数据的更新,就不会显著影响文件的顺序写入操作。假如需要高速向日志中追加事务,而文件中并没有原先分配存储块,那么无论何时在写入操作到达文件的结尾,文件系统都需要分配一个新存储块。而通过补白至少可以减少两次额外的磁盘寻址开销:一次是更新元数据;另一次是返回文件。

2、快照

快照是ZooKeeper数据树的拷贝副本,每一个服务器会经常以序列化整个数据树的方式来提取快照,并将这个提取的快照保存到文件中。服务器在进行快照时不需要进行协作,也不需要暂停处理请求。因为服务器在进行快照时还会继续处理请求,所以当快照完成时,数据树可能又发生了变化,称这样的快照是模糊的(fuzzy),因为它们不能反映出在任意给点的时间点数据树的准确状态。

【举例说明】

一个数据树中只有2个znode节点:/z和/z'。一开始,两个znode节点的数据都是1。
现在有以下操作步骤:
1、开始一个快照。
2、序列化并将/z=1到到快照。
3、使/z的数据为2(事务T)。
4、使/z'的数据为2(事务T')。
5、序列化并将/z'=2写入到快照。

这个快照包含了/z=1和/z'=2。然而,数据树中这两个znode节点在任意的时间点上都不是这个值。这并不是问题,因为服务器会重播(replay)事务。每一个快照文件都会以快照开始时最后一个被提交的事务作为标记(tag),将这个时间戳记为TS。如果服务器最后加载快照,它会重播在TS之后的所有事务日志中的事务。在这个例子中,它们就是T和T。在快照的基础上重放T和T'后,服务器最终得到/z=2和/z'=2,即一个合理的状态。

接下来还需要考虑一个重要的问题,就是再次执行事务T'是会有问题,因为这个事务在开始快照开始之后已经被接受,而结果也被
快照中保存下来。就像之前所说的,事务是幂等的(idempotent),所以即使按照相同的顺序再次执行相同的事务,也会得到相同的结果,即便其结果已经保存到快照中。

为了理解这个过程,假设重复执行一个已经被执行过的事务。如上例中所描述,一个操作设置某个znode节点的数据为一个特定的值,这个值并不依赖于任何其他东西,无条件(unconditionly)地设置/z'的值(setData请求中的版本号为-1),重新执行操作成功,但因为递增了两次,所以最后以错误的版本号结束。
如以下方式就会导致问题出现,假设有如下3个操作并成功执行:

setData /z', 2, -1
setData /z', 3, 2
setData /a, 0, -1
第一个setData操作跟之前描述的一样,而后又加上了2个setData操作,以此来展示在重放中第二个操作因为错误的版本号而未能
成功的情况。假设这3个操作在提交时被正确执行。此时如果服务器加载最新的快照,即该快照已包含第一个setData操作。服务器仍然会重放第一个setData操作,因为快照被一个更早的zxid所标记。因为重新执行了第一个setData操作。而第二个setData操作的版本号又与期望不符,那么这个操作将无法完成。而第三个setData操作可以正常完成,因为它也是无条件的。

在加载完快照并重放日志后,此时服务器的状态是不正确的,因为它没有包括第二个setData请求。这个操作违反了持久性和正确性,以及请求的序列应该是无缺口(no gap)的属性。

这个重放请求的问题可以通过把事务转换为群首服务器所生成的state delta来解决。当群首服务器为一个请求产生事务时,作为事务生成的一部分,包括了一些在这个请求中znode节点或它的数据变化的值(delta值),并指定一个特定的版本号。最后重新执行一个事务就不会导致不一致的版本号。

七、服务器与会话

会话(Session)是Zookeeper的一个重要的抽象。保证请求有序、临时znode节点、监事点都与会话密切相关。因此会话的跟踪机制对ZooKeeper来说也非常重要。

ZooKeeper服务器的一个重要任务就是跟踪并维护这些会话。

  • 在独立模式下,单个服务器会跟踪所有的会话。
  • 在仲裁模式下,则由群首服务器来跟踪和维护。

群首服务器和独立模式的服务器实际上运行相同的会话跟踪器(参考SessionTracker类和SessionTrackerImpl类)。而追随
者服务器仅仅是简单地把客户端连接的会话信息转发给群首服务器(参考LearnerSessionTracker类)。

为了保证会话的存活,服务器需要接收会话的心跳信息。心跳的形式可以是一个新的请求或者显式的ping消息(参考LearnerHandler.run())。两种情况下,服务器通过更新会话的过期时间来触发(touch)会话活跃(参考SessionTrackerImpl.touchSession()方法)。
在仲裁模式下,群首服务器发送一个PING消息给它的追随者们,追随者们返回自从最新一次PING消息之后的一个session列表。群首服务器每半个tick(参考10.1.1节的介绍)就会发送一个ping消息给追随者们。所以,如果一个tick被设置成2秒,那么群首服务器就会每一秒发送一个ping消息。

对于管理会话的过期有两个重要的要点。一个称为过期队列(expiry queue)的数据结构(参考ExpiryQueue类),用于维护会话的过期。这个数据结构使用 bucket 来维护会话,每一个bucket对应一个某时间范围内过期的会话,群首服务器每次会让一个bucket的会话过期。为了确定哪一个bucket的会话过期,如果有的话,当下一个底限到来时,一个线程会检查这个expiry queue来找出要过期的bucket。这个线程在底限时间到来之前处于睡眠状态,当它被唤醒时,它会取出过期队列的一批session,让它们过期。当然取出的这批数据也可能是空的。
为了维护这些bucket,群首服务器把时间分成一些片段,以expirationInterval为单位进行分割,并把每个会话分配到它的过期时间对应的bucket里,其功能就是有效地计算出一个会话的过期时间,以向上取正的方式获得具体时间间隔。更具体来说,就是对下面的表达式进行计算,当会话的过期时间更新时,根据结果来决定它属于哪一个bucket。
(expirationTime / expirationInterval + 1) * expirationInterval
举例说明,比如expirationInterval为2,会话的超时时间为10。那么这个会话分配到bucket为12((10/2+1)*2的结果)。注意当触发(touch)这个会话时expirationTime会增加,所以随后需要根据之后的计算会话移动到其他的bucket中。
使用bucket的模式来管理的一个主要原因是为了减少让会话过期这项工作的系统开销。在一个ZooKeeper的部署环境中,可能其客户端就有数千个,因此也就有数千个会话。在这种场景下要细粒度地检查会话过期是不合适的。如果expirationInterval短的话,那么ZooKeeper就会以这种细粒度的方式完成检查。目前expirationInterval是一个tick,通常以秒为单位。

八、服务器与监视点

为了在服务端管理监视点,ZooKeeper的服务端实现了监视点管理器(watch manager)。一个WatchManager类的实例负责管理当前已被注册的监视点列表,并负责触发它们。所有类型的服务器(包括独立服务器,群首服务器,追随者服务器和观察者服务器)都使用同样的方式处理监视点。
DataTree类中持有一个监视点管理器来负责子节点监控和数据的监控,对于这两类监控,请参考4.2节,当处理一个设置监视点的读请求时,该类就会把这个监视点加入manager的监视点列表。类似的,当处理一个事务时,该类也会查找是否需要触发相应的监视点。如果发现有监视点需要触发,该类就会调用manager的触发方法。添加一个监视点和触发一个监视点都会以一个read请求或者FinalRequestProcessor类的一个事务开始。
在服务端触发了一个监视点,最终会传播到客户端。负责处理传播的为服务端的cnxn对象(参见ServerCnxn类),此对象表示客户端和服务端的连接并实现了Watcher接口。Watch.process方法序列化了监视点事件为一定格式,以便用于网络传送。ZooKeeper客户端接收序列化的监视点事件,并将其反序列化为监视点事件的对象,并传递给应用程序。
监视点只会保存在内存,而不会持久化到硬盘。当客户端与服务端的连接断开时,它的所有监视点会从内存中清除。因为客户端库也会维护一份监视点的数据,在重连之后监视点数据会再次被同步到服务端。

九、客户端

在客户端库中有2个主要的类:ZooKeeperClientCnxn

  • ZooKeeper类:实现了大部分API,写客户端应用程序时必须实例化这个类来建立一个会话。一旦建立起一个会话,ZooKeeper就会使用一个会话标识符来关联这个会话。这个会话标识符实际上是由服务端所生成的(参考SessionTrackerImpl类)。
  • ClientCnxn类管理连接到server的Socket连接。该类维护了一个可连接的ZooKeeper的服务器列表,并当连接断掉的时候无缝地切换到其他的服务器。当重连到一个其他的服务器时会使用同一个会话(如果没有过期的话),客户端也会重置所有的监视点到刚连接的服务器上(参考ClientCnxn.SendThread.primeConnection())。重置默认是开启的,可

以通过设置disableAutoWatchReset来禁用。

十、序列化

对于网络传输和磁盘保存的序列化消息和事务,ZooKeeper使用了Hadoop中的Jute来做序列化。如今,该库以独立包的方式被引入,在ZooKeeper代码库中,org.apache.jute就是Jute库。

对于Jute最主要的定义文件为zookeeper.jute。它包含了所有的消息定义和文件记录。下面是一个Jute定义的例子:

module org.apache.zookeeper.txn {
...
class CreateTxn {
    ustring path;
    buffer data;
    vector<org.apache.zookeeper.data.ACL> acl;
    boolean ephemeral;
    int parentCVersion;
    }
...
}
这个例子定义模块,该模块包括一个create事务的定义。同时。这个模块映射到了一个ZooKeeper的包中

相关推荐