ZooKeeper - O'Reilly Media ---- Zookeeper Internals (4)

本地存储

我们已经提到了事务日记和快照,还有SyncRequestProcessor就是在处理提议时写这些文件的处理器。接下来我们再在这方面深入一下。

 

日记和硬盘使用

回忆一下服务器使用事务日记来固话事务。在接受一个提议之前,服务器(跟随者或者是首领)会将提议中的事务固话到事务日记上,事务日记是服务器本地硬盘上的一个文件,用来按顺序的保存事务。服务器会不时的关闭当前文件,创建一个新的文件来回滚事务日记。

 

因为写日记对于写请求来说是一个关键的步骤,ZooKeeper需要高效的处理。追加文件可以通过硬件驱动来提高效率,ZooKeeper本身也采用了一些技巧来较快这个过程:组提交和填充。组提交就是将多个事务的追加操作组合在一个单个的硬盘写操作中,这样就允许多个事务已以此硬盘写的代价来进行固话。

 

对于固话事务到硬盘中有一个重要的关键。现在的操作系统一般都会缓存脏页,异步的将他们写道硬盘中,然而我们必须保证事务在我们继续工作之前进行固话。因此我们需要在固话事务的时候执行flush操作。Flush也就是我们告诉操作系统将脏页写到硬盘完成后再返回。因为我们是在SyncRequestProcessor中进行固话,因此SyncRequestProcessor需要执行flush操作,实际上我们多所有的队列中的食物都进行了组提交的优化实现,如果现在队列中只有一个事务,处理器同样会执行flush。处理器不会等待更多的事务进入队列,因为这样会造成执行的延迟。关于这方面的代码,可以关注SyncRequestProcessor.run()。

 

         硬盘写缓存

一个服务器止呕在保证事务已经写到事务日记之后才会承认一个提议。更详细的就是,服务器会调用ZKDatabase的commit方法,该方法会调用FileChannel.force。这样服务器就能在承认之前保证事务已经固话到硬盘了。从这里来看有一个警告,就是咸蛋的硬盘都会有写缓存来保存要写入硬盘的数据。如果写缓存是打开的话,调用force也不能保证当返回时数据就已经写入硬盘了,而可能仅仅是保留在缓存中,为了保证在调用FileChannel.force()返回之后数据就已经写入硬盘,必须关闭硬盘的缓存。可以通过操作系统提供的不同方式来关闭它。

 

填充包括预先为文件申请硬盘块。这样就可以使得块申请时更新文件系统元数据不会明显影响到对文件的持续写。如果事务是以一个较高的速度追加到日记中,而文件的块没有预先申请的话,文件系统就需要在每次在当前块要写完时要申请新的块,这至少包括两次额外的硬盘查找:一次是为了更新元数据,另一给是返回文件。

 

为了便面和系统的其它写操作相互影响,我们强烈推荐在 一个独立的设备上进行事务日记的写操作。而另一个设备可以用来进行操作文件系统和快照的操作。

 

快照

快照就是ZooKeeper数据树的拷贝,每一个服务器都会经常的对数据数进行序列化来得到一个快照然后将其保存到文件中。服务器不需要相互协同来获取快照,也不需要停止处理请求。因为服务器在产生快照的同时也在执行请求,也就是快照获取时数据树也在改变。因为他们无法准确的反应数据树任何时刻的状态,因此我们把这种快照形容为模糊的。

 

让我们通过一个流程来解释下这点。加入一个数据树只有两个节点:/z和/z’,初始状态下两个节点都是整数1.现在考虑下执行下面的步骤。

1.      启动一个快照

2.      序列化和写/z=1到快照中。

3.      设置/z的数据为2(事务T)

4.      设置/z’的数据到2(事务T’)

5.      序列化和写/z’到快照。

此时,快照中包含/z=1 和/z’=2。然后从没有一个时间点连个节点的值是这样子的。其实这并不是一个问题,因为服务器会重新执行这些事物。我们在快照启动时使用最后的一个已提交的事务来给快照打上标签,这里表现为TS。如果服务器最终装载了快照,他会重新执行事务日记中晚于TS的所有事物,也就是这里的T和T’。在快照重现T和T’之后,服务器也保持了/z=2和/z’=2,处于一个合理的状态。

 

随之而来的一个重要的问题就是,重新执行T’时因为在快照生成的时候已经执行过了这样会不会产生问题。像我们之前提到的,事务是幂等的,只要我们按照相同的顺序来执行相同的事务,执行的结果是一样的,尽管某一些事务已经应用到快照里面了。

 

要理解这个过程,假定应用一个事物相当于重新执行相关操作。就像例子中描述的,操作将节点指定了一个特定的值,而这个值是不依赖于任何其他东西的。也就是说设定/z’的节点是无条件的(在setData请求中,版本数字是-1)。但是重复执行操作成功后,我们却有了了一个错误的版本数字,因为我们增加了版本数字两次。这可能造成向下面这样的问题。假定有三个操作提交和执行成功:

setData /z’, 2,-1

setData /z’, 3, 2

setData /a, 0, -1

 

第一的setData的操作就像我们之前描述的一样,但我们添加了两个额外的操作来演示我们会出现这样的结果,在重现的过程中,第二次操作会因为错误的版本数字而不被执行。这里假定服务器装在了最新的快照,这里面已经包含了第一个setData的结果,但是快照是用一个较早的zxid来标记的,在重现的时候服务器会重新执行第一个setData.因此重复执行的第一个setData,这时候的版本数字就和第二个setData操作所期望的版本数字不一样了,因此这个操作就不会被执行。而第三个操作会被正常执行,因为它是条件无关的。

 

在装载快照和重现日记后,因为没有包含第一个setData请求,服务器的状态处于一个不正确的状态。这个结果违背了耐用性和在请求顺序执行后结果的一致性。

 

这个问题通过由首领将事务转换为状态变化来小心处理。当首领生成一个请求的事务时,作为生成的事务的一部分,包好了请求对节点或其数字造成的变化和制定了一个固定的版本数字,因此在重现事务时不会出现版本不一致的版本数字(为什么?需要从源码分析)。

 

服务器和回话

回话构成了Zookeeper的一个重要的抽象。执行顺序的保证,临时节点和观察机制都和会馆紧密相关,因此回话的跟踪机制对于Zookeeper来说非常重要。

 

对于Zookeeper服务器来说一个重要的任务就是维持回话的轨迹。运行独立模式时,一个服务器跟踪所有的回话,而法定人数模式,则是首领跟踪所有回话。首领服务器和单独模式下的服务器实际上是跑的同样的回话跟踪器(见 SessionTracker和SessionTrackerImpl)。跟随者服务器负责将连接到它的所有的客户端的回话信息推送到首领(见LearnerSessionTracker)。

 

要保持一个回话存活,服务器需要接收到回话的心跳。心跳以新请求的形式或外在的ping信息到来(见 LearnerHandler.run())。在所有的市况下,服务器通过更新回话的过期时间来保持和回话的联系(见 SessionTrackerImpl.touchSession()).在法定人数模式下,首领向学习者发送PING信息,然后学习者将上次PING到现在的回话列表返回。首领没个半个滴答发送一次PING。滴答(见基础配置179页的)就是Zookeeper使用的最小的时间单位,通常用好毫秒来表示。因此如果一个滴答是2秒,那么首领就是每一秒发送一次PING。

 

管理回话的过期有两个重要的点。 一个叫做expiryqueue的数据结构(见ExpiryQueue)为了回话过期的目的保持着回话的信息。这个数据结构用桶的概念保存回话,每一个桶对英语一个时间段,而相应的回话就是值在这个时间段内会过期的回话。首领每次终止一个桶的回话。有一个线程会检查过期队列来发现下一个死亡线是那个来决定那个哪个桶要被终止。这个线程会休眠直到死亡线时醒过来然后来检查过期队列中的一部分回话来进行终止,当然这个集合可能是空的。

 

要位置这些桶,首领将时间分隔成expirationInterval的单元,并将回话指定给下一个回话过期的桶里面。执行这个分配的函数本质上就是对一个回话的过期时间取整到下一个更高的间隔上。更具体的就是,这个函数会在回话的过期时间更新时计算它属于哪个桶。:

         (expirationTime/ expirationInterval + 1) * expirationInterval

看一个例子,加入expirationInterval是2,一个回话的过期时间是10.我们会知道这个回话道12( (10/12+1)*2)。注意每一次我们和回话进行联系时会更增加expirationTime,因此我们会将回话移动到更晚过期的桶里面。

 

一个使用桶模式的原因是减少检查回话过期的代价。一个Zookeeper部署可能有成千上万的客户端进行连接,也就是成千上万个回话。太过细腻的检查回话过期的方式在这个情况下就不合适了.和这个解释相关的就是,如果expirationInterval很小,那Zookeeper执行回话过期检查就成为一个细密的方式了,当前expirationInterval等于一个滴答,通常是在秒数量级上的。

 

服务器和监听

监听器(见监听和通知 20页)是一次性的,由读操作设置的触发器,每一个监听器会被一个特定的操作出发。为了在服务端管理这些监听器,Zookeeper服务器实现了监听器管理器。这是WatchManager的一个实例,负责管理当前注册的一系列监听器和出发他们,所有类型的服务器(独立模式,首领,跟随者和观察者)都是以同样的方式来处理监听。

 

DataTree类保持了一个监听管理器来进行子节点监听和其它的数据监听。在71页的更多详细信息:如何设置监听器中我们讨论了这两种类型的监听器。当处理一个设置监听器的读操作时,DataTree类会将监听器添加到管理器的监听器列表中。类似的,当处理一个事务时,该类会检查是不是有哪些监听器要被对应的修改触发。如果存在,该类会调用管理器的触发器方法。添加一个监听器和触发监听器都是在读请求或事物的执行中开始的,也就是在FinalRequestprocessor中。

 

在服务器端出发的一个监听器会传递到客户端,负责这件事情的类是服务器的cnxn对象(见ServerCnxn类),该类代表了客户端和服务器端之间的连接和实现了Watcher箭扣。Watcher.process方法会序列化监听时间成一个可以通过网络传输使用的格式。当Zookeeper客户端接收到序列化版本的监听事件时,将其转变会一个监听时间,然后传递到应用。

 

监听器只是在内存中保留,不会被胡话到硬盘中。当一个客户端和服务器失去连接后,所有相关的监听器会从内存中移除。因为客户端会保存这些外部的监听器,当他们重新建立连接时,也会重新在他们连接到的新的服务器上建立这些监听器。

 

客户端

客户端库中有两个主要的类:Zookeeper和ClientCnxn。Zookeeper类实现了大部分的API,是客户端创建回话时必须要实例化的类。一旦创建了一个回话,Zookeeper会将一个回话标志关联到回话上,这个标志时间上是由服务端的服务(见SessionTrackerImpl)生成的。

 

ClientCnx类管理了客户端和服务器的socket连接。它位置者一个可以连接的Zookeer服务器的列表,可以建立连接并在连接中断发生时透明的转换到另一个服务器上。当在一个不同的服务器上重新连接一个回话时,客户端也会重新建立所有的监听器(见ClientCnxn.SendThread.primeConnection())。这个重建模式是是能的,但是也可以通过设置disableAutoWatchReset来关闭。

 

序列化

为了序列化信息和事务便于在网络上进行传输,在硬盘上进行存储,Zookeeper使用了Jute,这也是脱胎于Hadoop。现在这两个代码已经是独立进化了。Zookeeper代码中的org.apache.jute包中是Jute编译器的代码。(很长一段时间,Zookeeper开发团队都在讨论一个替代Jute的方案,但是我们到目前为止还没有发现一个合适的替代者。Jute在Zookeeper中也工作的很好,因此替换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;

}

...

}

这个例子定义了一个包含创建事务的模块,该模块会映射到一个Zookeeper包。

 

其它信息

本章猫叔了Zookeeper的核心机制。首领选举对可用性来说是一个临界点。没有首领选举,一个Zookeeper集群就无法保持可靠性。拥有一个首领是必须的但还不够,Zookeeper同样需要Zap协议来传播状态更新,该协议保证了一个状态的一致性,而不管Zookeeper的服务器可能发生崩溃。

 

我们回顾了服务器的类型:独立模式,首领,跟随者和观察者。他们在实现机制和执行协议上都不同。他们的使用对一个部署来说也有不同的意义。比如,增加观察者可以在不影响写吞吐量的情况向增加读吞吐量。但是增加观察者并不会提高系统的可用性。

 

在内部,Zookeeper服务器试下了一系列的机制和数据结构,我们已经关注了关于回话和监听的实现,这是理解和实现Zookeeper应用的关键概念。

 

虽然我们已经在文件中点到了源码,但是并不是为了提供一个关于源代码的详细视角。我们强烈建议读者获取一份源码的拷贝,然后以文章中引用来作为开始学习。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值