本文主要是介绍zookeeper06-ZooKeeper内部原理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
- ZooKeeper运行在一组服务器上,而客户端连接到这些服务器上执行操作。但是这些服务器对客户端发送的操作到底做了什么呢?我们要在这组ZooKeeper服务器中选择某一个服务器,称之为群首(leader)。其他服务器追随leader,被称为追随者(follower)。
- leader是处理所有改变ZooKeeper系统的请求的中心。它作为一个定序器,建立更新ZooKeeper状态的顺序。
- follower接收leader发出的更新操作,并进行更新,以确保状态更新能够在leader崩溃时幸存下来。
- leader和follower组成了保障状态变化有序的核心实体。同时,还存在第三类服务器,称为观察者(observer)。观察者不参与申请什么请求的决策过程,只是观察决策的结果,观察者的设计只是为了系统的可扩展性。
1、请求、事务和标识符
- ZooKeeper服务器会在本地处理读请求(exists、getData和getChildren)。假如服务器接收到客户端的getData请求,服务器读取其状态并返回给客户端。因为服务器会在本地处理读请求,所以ZooKeeper在处理读请求时,性能会很高。还可以增加服务器到ZooKeeper集群来提高整体的吞吐量能力。
- 改变ZooKeeper状态的客户端请求(create、delete和setData)将会被转发给leader。
- leader执行请求,产生一个状态更新,我们称之为事务(transaction)。
- 请求表达了客户端发起操作的方式,事务包含修改ZooKeeper状态和反映请求执行情况的步骤。
- 也许一个直观的解释方法是提出一个简单的,非zookeeper操作。假如操作是inc(i),它增加变量i的值,如果一个请求是inc(i)。假如i的值是10,执行该操作后,其值变为11。使用请求和事务的概念,请求是inc(i),事务是i和11(变量i的值是11)。
- 现在让我们看一个ZooKeeper的例子,假如一个客户端提交了一个对/z znode的setData请求,setData将会改变该znode的数据,并会增加该znode的版本号。因此,此请求的事务包含两个重要字段:znode的新数据和znode的新版本号。当处理该事务时,服务器只是简单地用事务中的数据替换/z中的数据,用事务中的值替换版本号,而不是增加版本号的值。
- 一个事务为一个操作单元,也就是说所有的变更处理需要以原子方式执行。在setData示例中,变更数据而不改变版本号将会发生错误。因此,ZooKeeper集群应用事务时,确保所有的变更操作以原子方式被执行,同时不会被其他事务所干扰。在ZooKeeper中,并不存在传统关系数据库中所涉及的回滚机制,而是确保事务的每一步操作都互不干扰。在很长的一段时间里,Zookeeper所采用的设计方式为,在每个服务器中启动一个单独的线程来处理事务,通过单独的线程来保障事务之间的执行顺序互不干扰。最近,ZooKeeper增加了多线程的支持,以便提高事务处理的速度。
- 一个事务还要具有幂等性。也就是说,我们可以对同一个事务执行两次,并将得到相同的结果。我们甚至还可以对多个事务执行多次,并得到相同的结果,只要每次都以相同的顺序执行它们。事务的幂等性可以让我们在进行恢复处理时更加简单。
- 当leader产生一个新事务时,就会为该事务分配一个标识符,我们称之为ZooKeeper事务ID(zxid)。通过Zxid对事务的标识,各个follower就可以按照leader所指定的顺序应用事务。在进行新的leader选举时也会交换zxid信息,这样就可以知道哪个无故障的服务器接收了更多的事务,并可以同步他们之间的状态信息。
- zxid为一个long型(64位)整数,分为两部分:时间戳(epoch)部分和计数器counter)部分,每个部分为32位。
2、leader选举
- leader是由一组服务器选择的一个服务器,并会一直被这些服务器所认可。leader的作用是为了对客户端所发起的变更ZooKeeper状态的请求进行排序。leader将每个请求转换为事务,各个follower按照leader确定的顺序接受并处理这些事务。
- 要发挥leader作用,一个服务器必须获得法定人数的服务器的支持。法定数量必须大于集群数量的1/2,以避免脑裂问题(split brain):即两个集合的服务器分别独立的运行,形成了两个集群。脑裂将导致整个系统状态的不一致性,最终客户端也将根据其连接的服务器而获得不同的结果。
- 选举和支持leader的组至少在一个服务器进程上相交。使用仲裁(quorum)来表示这样的进程子集。
- 每个服务器启动后进入LOOKINC状态,开始选举一个新的leader或查找已经存在的leader。如果leader已经存在,其他服务器就会通知新服务器哪个是leader。此时,新服务器连接到leader,并确保它自己的状态与leader的状态一致(进行同步)。
- 如果集群中所有的服务器都处于LOOKING状态,它们必须通过通信来选举出一个leader。它们通过信息交换对leader选举达成共识。在本次选举过程中胜出的服务器将进入LEADING状态,而集群中其他服务器将会进入FOLLOMING状态。
- 对于leader的选举消息称为leader选举通知消息(leader election notifications),或简称为通知(notifications)。该协议非常简单,当一个服务器进入LOOKING状态,它就会向集群中的每个服务器发送一个通知消息,该消息包括投票(vote)信息,投票中包含服务器标识符(sid)和最近执行的事务的zxid信息。比如,一个服务器所发送的投票信息为(1,5),表示该服务器的sid为1,最近执行的事务的zxid为5(出于leader选举的目的,zxid只有一个数字,而在其他协议中,zxid则有时间戳epoch和计数器组成)。
- 服务器收到投票后,会根据以下规则改变自己的投票:
- (1)将接收到voteId和voteZxid作为自己投票中的标识符和zxid,而myZxid和mySid则是自己本身的值。
- (2)如果(voteZxid > myZxid)或(votezxid = myZxid且voteld > mysid),保持当前的投票。
- (3)否则,通过将myZxid赋值给voteZxid,将mySid赋值给Zxid来更改当前的投票。
- 如果多个服务器拥有最新的zxid值,其中的sid值最大的将赢得选举。这样做将会简化leader崩溃后重新仲裁的流程。
- 一旦某个服务器收到的投票数达到法定人数,这个服务器就会宣布了leader。
- 如果选出的leader是服务器本身,它就开始执行leader角色。
- 否则,它就会成为一个follower,并试图与当选的leader建立联系。注意,这并不能保证follower能够连接到被选出的leader。例如,当选的leader可能已经崩溃。一旦连接起来,follower和leader之间将会进行状态,只有在同步完成之后,follower才能开始处理新的请求。
- 现在,让我们通过示例来重温这个协议的执行过程。图9-1显示了三个服务器,每个服务器都有一个不同的初始投票,分别对应于服务器的标识符和服务器的最后一个zxid。每个服务器都会收到另外两个服务器发送的投票信息,在第一轮之后,服务器s2和服务器s3将他们的投票更改为(1,6)。服务器s1和服务器s2在改变投票后会发送新的通知消息,在接收到这些新的通知消息后,每个服务器从仲裁处收到一样的投票值,最后选举出服务器s1为leader。
- 并不是所有的执行都像图9-1中那样良好。在图9-2中,我们展示了另一种情况的例子,服务器s1向服务器s2传送消息时发生了网络故障导致长时间延迟,因此导致服务器s2做出了错误判断,选举了另一个服务器s3,而不是服务器s1,虽然s1的zxid值更高。最终,服务器s1和服务器s3组成了仲裁数量(quorum),并将忽略服务器s2。
- 虽然服务器s2选择了另一个leader,但并未导致整个服务发生错误。因为服务器s3并不会以leader的角色响应服务器s2的请求,最终服务器s2将会在等待s3的响应时超时,并开始再次重试。再次尝试,意味着在这段时间内,服务器s2无法处理任何客户端的请求,这样做并不可取。
- 从这个例子,我们发现如果让服务器s2在进行leader选举时多等待一会,它就能做出正确的判断,如图9-3所示。我们很难确定服务器需要等待多长时间,在现在的实现中,默认的leader选举的实现类为FastLeaderElection,其中使用固定值200ms(常量finalizeWait)。这个值比在当今数据中心所预计的延迟消息(不到1毫秒到几毫秒的时间)要长得多,但比恢复时间要小的多。如果这个延迟(或任何其他选择的延迟)不够长,一个或多个服务器最终会错误地选出一个没有足够follower的leader,那么服务器将不得不再次进行leader选举。错误地选举一个leader可能会导致整个恢复时间更长,因为服务器将会进行不必要的连接以及同步操作,并需要发送更多消息来进行另一轮的leader选举。
3、Zab:状态更新的广播协议
- 在接收到一个写请求操作后,follower会将请求转发给leader,leader将探索性地执行该请求,并将状态更新的执行结果以事务的方式进行广播。事务中包含服务器需要执行变更的确切操作,当事务提交后,这些变更就会应用到数据树上。数据树是ZooKeeper用于保存状态信息的数据结构(请参考DataTree类)。
- 服务器如何确认一个事务已经提交。由此引入了Zab协议:ZooKeeper原子广播协议(ZooKeeper Atomic Broadcast protocol)。假设现在我们有一个活动的leader,并拥有仲裁数量的follower支持它,通过该协议提交一个事务非常简单,类似于两阶段提交。
- (1)leader向所有follower发送PROPOSAL消息p。
- (2)当follower接收到消息p后,会响应leader一个ACK消息,通知leader它已接受该提议(proposal)。
- (3)当收到仲裁数量的服务器发送的确认消息后(该仲裁数包括leader自己),leader就会发送消息通知follower进行提交(COMMIT)操作。
- 图9-4说明了这个过程的具体步骤和顺序。在图中,我们假设leader隐式地向自己发送消息。
- follower在应答提议之前,还需要执行一些检查操作。follower需要检查提议是否来自它当前追随的leader,并且确认提议和提交事务的顺序与leader广播的顺序相同。
- Zab保障了以下几个重要属性:
- 如果leader按顺序广播了事务T1和事务T2,那么每个服务器在提交事务T2前必须保证事务T1已经提交完成。
- 如果某个服务器按照T1、T2的顺序提交事务,所有其他服务器也必然会在提交事务T2前提交事务T1。
- 第一个属性保证事务在服务器之间的传送顺序相同,而第二个属性保证服务器不会跳过任何事务。假设事务是状态变更操作,并且每个状态变更又依赖前一个状态变更,如果跳过事务就可能导致状态的不一致。两阶段提交保证了事务的顺序。Zab在仲裁数量的服务器中记录了事务。集群中仲裁数量的服务器需要在leader提交事务之前确认事务,而且follower也会在硬盘中记录事务的确认信息。
- 事务在某些服务器上可能终结,而其他服务器上却不会,因为在写入事务到存储中时,服务器可能会发生崩溃。只要选举了新的leader,ZooKeeper就将所有服务器的状态更新到最新。
- 但是,ZooKeeper并不期望在整个过程中都有一个活动的leader。leader服务器可能会崩溃或短时间地失去连接,此时,其他服务器需要选举一个新的leader以保证系统仍然可用。其中时间戳(epoch)的概念代表了管理权随时间的变化情况,一个时间戳表示了某个服务器行使管理权的这段时间。在一个时间戳内,leader会广播提议消息,并根据计数器(counter)识别每一个消息。zxid的第一个元素为时间戳信息,因此每个zxid可以很容易地与事务被创建时间戳相关联。
- 时间戳的值在每次选举leader的时候便会增加。同一个服务器在不同时期成为leader后持有不同的时间戳信息,但从协议的角度出发,一个服务器行使管理权时,如果持有不同的时间戳,该服务器就会被认为是不同的leader。例如,服务器s成为leader并且持有的时间戳为4,但当前集群中leader的时间戳为6,集群中的follower会追随时间戳为6的leader,处理leader时间戳是6的消息。当然,follower在恢复阶段才会接收时间戳是4到6之间的提议消息,之后才会开始处理时间戳为6的新消息,而实际上这些提议消息是以时间戳6的消息来发送的。
- 在仲裁模式下,记录已接收的提议消息非常关键,这样可以确保所有的服务器最终提交了被某个或多个服务已经提交完成的事务,即使leader发生了故障。完美检测leader(或任何服务器)是否发生故障是非常困难的,所以在很多设置中,很有可能会错误地判断leader已经崩溃。
- 实现这个广播协议遇到的最多的困难在于多个leader并存的情况,这不一定是脑裂。多个并存的leader可能会导致服务器提交事务的顺序发生错误,或者直接跳过了某些事务。防止系统中同时出现两个服务器认为自己是leader的情况是非常困难的,时间问题或消息丢失都可能导致这种情况。为了解决这个问题,Zab协议提供了以下保障:
- 一个刚被选举的leader在广播新的事务之前,要先提交之前时间戳的所有事务。
- 在任何时间,都不会出现两个被仲裁数量支持的leader。
- 为了实现第一个需求,新leader并不会马上处于活动状态,而是要确保有仲裁数量的服务器认可自己的时间戳。时间戳的初始状必须包含所有之前已经提交的事务,或者某些已经被其他服务器接受,但尚未提交完成的事务。在新leader进行时间戳e的任何新的提议前,必须保证自时间戳开始到时间戳e-1的所有提议已经被提交。如果一个提议消息处于时间戳e'(e'<e),在新leader处理时间戳e的第一个提议前没有提交这个提议,那么新的提议将永远不会被提交。
- 对于第二个需求有些棘手,因为我们并不能完全阻止两个leader独立地运行。假如一个leader L1正在管理并广播事务,但如果某一时刻,仲裁数量的服务器集Q认为leader L1已经退出,它们将选举一个新的leader L2。我们假设在Q放弃leader L1时有一个事务T正在广播,而且Q的子集中的服务器记录了事务T,在leader L2被选举完成后,在Q之外服务器也记录了事务T,这就为事务T形成一个仲裁数量,在这种情况下,事务T在新leader被选举后仍会被提交。不用担心这种情况,这并不是个bug。Zab协议保证了事务T作为新leader L2的一部分被提交,确保新leader L2的仲裁数量的支持者中至少有一个follower确认了该事务T。关键点在于leader L1和L2在同一时刻没有仲裁数量的支持者。
- 图9-5说明了这一场,在图中,leader L1为服务器s5,L2为服务器s3,Q由s1 ~ s3组成,事务T的zxid为(1,1)。s5在收到s3的确认消息之后,向s4发送了提交消息,通知s4提交事务。其他服务器因追随s3,忽略了s5的消息。注意s3确认的xzid为(1,1),因此它获得管理权后知道事务点。
- 之前我们提到Zab保证新leader L2不会错过(1,1)。在新leader L2生效前,它必须学习旧的仲裁数量服务器已经接受的所有建议,并且它必须得到这些服务器不再接受旧leader L1的任何提议的承诺。此时,如果leader L1还能继续提交提议,比如(1,1),这条提议必须已经被至少一个认可了新leader的follower接受。我们知道leaderl的仲裁数量服务器集和新leaderlʹ的一定是相交的。因此,L2将(1,1)加入自身的状态并传播给其follower。
- 在leader选举时,我们选择zxid最大的服务器作为leader。这使得ZooKeeper不需要将提议从follower传到leader,而只需要将状态从leader传播到follower。假设至少有一个follower接受了一条leader没有接受的提议,那么leader必须确保在和其他follower同步之前已经收到并接受了这条提议。但是,如果我们选择zxid最大的服务器,我们将可以完完全全跳过这一步,直接让follower进行同步。
- 在时间戳发生转换时,Zookeeper使用两种不同的方式来更新follower。
- 如果follower滞后于leader不多,leader只需要发送缺失的事务。因为follower按照严格的顺序接收事务,这些缺失的事务总是最近的。这种更新在代码中被称之为DIFF。
- 如果follower滞后很多,ZooKeeper将发送一个完整快照,在代码中称为SNAP。
- 因为发送完整的快照会增加系统恢复的时间,所以发送缺失的事务点是更优的选择。可是当follower滞后很多的情况下,我们只能选择发送完整快照。
- leader发送给follower的DIFF对应于已经存在于事务日志中的提议,而SNAP对应于leader拥有的最新有效快照。
4、观察者
- 观察者和follower之间有一些共同点。相同的是观察者也提交来自leader的提议。不同的是,观察者不参投票过程。观察者仅仅学习经由INFORM消息提交的提议。由于leader将状态变化发送给follower和观察者,这两种服务器都被称为学习者。
- 哪些参与投票决定哪条提议被提交的服务器被称为PARTICIPANT服务器,PARTICIPANT服务器可以是leader,也可以是follower。而观察者则被称为OBSERVER服务器。
- 观察者的主要作用是提高读请求的可扩展性。通过增加更多的观察者,我们可以在不牺牲写操作的吞吐率的前提下服务更多的读操作。请注意,写操作的吞吐率取决于仲裁数量的大小。如果我们加入更多的参与投票的服务器,仲裁数量会变大,并会减小写操作的吞吐率。增加观察者也不是完全没有开销的,每个新的观察者都会为每个提交的事务带来额外的消息成本,但这个开销相对于增加参与投票服务器来说小很多。
- 观察者另外一个作用是跨多个数据中心部署ZooKeeper。由于数据中心之间存在网络延迟,将服务器分散于多个数据中心将明显地降低系统的速度。引人观察者后,更新请求能够先以高吞吐率和低延迟的方式在一个数据中心内执行,接下来再传播到异地的其他数据中心。值得注意的是,观察者并不能消除数据中心之间的网络消息的传递,因为观察者必须转发更新请求给leader并且处理INFORM消息。不同的是,当参与的服务器在同一个数据中心时,允许观察者在单个数据中心交换提交更新所需的消息。
- 深入INFORM消息
- 因为观察者不参与决定提议接受与否的投票,leader不需要发送提议到观察者,leader发送给follower的提交消息(commit)只包含zxid而不包含提议本身。因此,仅仅发送提交消息(commit)给观察者并不能使其实施提议。这是我们使用INFORM消息的原因,INFORM消息本质上是包含了正在被提交的提议的提交消息。
- 简单来说,follower接收两种消息,而观察者只有一种消息。follower从一次广播中获取提议的内容,并从接下来的一条提交消息中获取zxid。观察者只获取一条包含已提交提议的内容的INFORM消息。
5、服务器的构成
- leader、follower和观察者都是服务器。我们将这些服务器抽象为请求处理器。请求处理器是处理流水线上不同阶段的抽象。每个服务器都实现了一个请求处理器的序列。一条请求经过服务器流水线上所有处理器的处理后被称为得到完全处理。
5.1、独立服务器
- Zookeeper中最简单的流水线是独立服务器(ZooKeeperServer类,没有复制)。图9-6描述了此类服务器的流水线。它包含三种请求处理器:PrepRequestProcessor、SyncRequestProcessor和FinalRequestProcessor。
- PrepRequestProcessor接受客户端的请求并执行请求,将处理的结果生成一个事务。我们知道事务是执行一个更新操作的结果,该操作会反映到ZooKeeper的数据树上。事务信息将会以头部记录和事务记录的方式添加到Request对象中。同时还要注意,只有改变ZooKeeper状态的操作才会产生事务,读操作并不会产生事务。因此,对于读请求的Request对象中,事务的成员属性的引用值为null。
- SyncRequestProcessor负责将事务持久化到磁盘上。实际上就是将事务数据按顺序追加到事务日志中,并频繁地生成快照。
- FinalRequestProcessor:如果Request对象包含事务数据,该处理器将会修改Zookeeper数据树。否则,该处理器会从数据树中读取数据并返回给客户端。
5.2、仲裁模式
- 当我们切换到仲裁模式时,服务器的流水线则有一些变化。
5.2.1、leader服务器
- leader的流水线(LeaderZooKeeperServer类),如图9-7所示。
- PrepRequestProcessor处理器会准备一个提议,并将该提议发送给follower。
- ProposalRequestProcessor处理器将会把所有请求都转发给CommitRequestProcessor,但写请求还会转发给SyncRequestProcessor。
- SyncRequestProcessor处理器所执行的操作与独立服务器中的一样,即将事务持久化到磁盘上,执行完之后会触发AckRequestProcessor。
- AckRequestProcessor处理器是一个简单的请求处理器,它仅仅生成确认消息并返回给自己。正如我们前面提到的,在仲裁模式下,leader需要收到仲裁数量的服务器的确认消息(包括leader自己),AckRequestProcessor处理器就负责这个。
- CommitRequestProcessor处理器会将收到足够多(达到仲裁数量)的确认消息的提议进行提交(将提议加入到自己的队列中)。实际上,确认消息是由Leader类处理的(Leader.processAck()方法),这个类会将提交的请求加入到CommitRequestProcessor类中的队列中。请求处理器线程会处理这个队列。
- ToBeAppliedRequestProcessor处理器会将FinalRequestProcessor处理过的请求从队列中删除。队列中包含已经被仲裁数量的服务器确认并等待被执行的请求。leader使用这个队列与follower进行同步,并将收到仲裁数量的服务器确认的请求加入到这个队列中。
- FinalRequestProcessor处理器的作用与在独立服务器中一样。FinalRequestProcessor处理写请求,也处理读取请求。
- 注意,只有写请求才会被加入到待接受请求队列中,然后由ToBeAppliedRequestProcessor从该队列移除。ToBeAppliedRequestProcessor并不会对读取请求进行任何额外的处理操作,而是由FinalRequestProcessor处理器进行处理。
5.2.2、follower和观察者服务器
- 现在我们来讨论follower(FollowerRequestProcessor类)。follower使用的请求处理器如图9-8所示。请注意,处理器的序列不是单一的,输入也有不同形式:客户端请求、提议和提交事务。我们通过箭头来将标识follower处理的不同路径。
- FollowerRequestProcessor处理器接收并处理客户端请求。FollowerRequestProcessor将请求转发给CommitRequestProcessor,但写请求还会转发给leader服务器。
- CommitRequestProcessor直接将读请求转发给FinalRequestProcessor处理器,但对于写请求,CommitRequestProcessor必须在转发给FinalRequestProcessor之前等待提交消息(等待leader的commit消息)。
- 当leader直接或通过follower收到新的写请求时,它会生成一个提议,并将其转发给follower。当follower收到一个提议后,会将这个提议发送到SyncRequestProcessor。SyncRequestProcessor处理请求,将其记录到磁盘,并将其转发给SendAckRequestProcessor。SendRequestProcessor会向leader发送确认消息。当leader服务器接收到足够多的确认消息来提交这个提议时,leader就会发送提交(commit)消息给follower(同时也会发送INFORM消息给观察者服务器)。当follower收到提交(commit)消息时,会通过CommitRequestProcessor处理器进行处理。
- 为了保证执行的顺序,CommitRequestProcessor处理器会在收到一个写请求后,会暂停处理后续的所有请求。这就意味着,在一个写请求之后接收到的任何读取请求都将被阻塞,直到写请求通过CommitRequestProcessor。通过等待的方式,可以保证请求按照接收的顺序来执行。
- 对于观察者的请求流水线(observerZookeeperServer类)与follower的流水线非常相似。但是因为观察者不需要确认提议消息,因此观察者并不需要发送确认消息给leader,也不用持久化事务到磁盘。不过,目前正在讨论如何让观察员将事务持久化到磁盘,以加速观察员的恢复,因此ZooKeeper的未来版本可能会有这个特性。
6、本地存储
- SyncRequestProcessor处理器将提议写入事务日志。
6.1、日志和磁盘的使用
- Zookeeper服务器使用事务日志来持久化事务。在接受提议之前,服务器(follower或leader)将提议中的事务持久化到事务日志中,事务日志是服务器本地磁盘上的一个文件,事务被按顺序追加到该文件中。服务器会时不时地滚动日志,即关闭当前文件并打开一个新的文件。
- 因为写事务日志在写请求的关键路径上,所以ZooKeeper需要做到高效处理写日志问题。一般情况下,追加文件到磁盘上可以高效地完成,但ZooKeeper还使用了一些其他的技巧来加快它的速度:组提交和补白。组提交(Group Commits)是指对磁盘的一次写入中追加多个事务,这将使持久化多个事务只需要一次磁道寻址的开销。
- 关于将事务持久化到磁盘,有一个重要说明。现代操作系统通常会缓存脏页(DirtyPage),并将它们异步写入磁盘介质。但是,在继续处理请求之前,我们需要确保事务已经被持久化。因此,我们需要将事务刷新(Flush)到磁盘上。这里的刷新是指让操作系统将脏页写入磁盘,并在操作完成时返回。因为我们让SyncRequestProcessor处理器进行持久化事务,所以这个处理器负责刷新。当需要SyncRequestProcessor处理器将事务刷新到磁盘时,我们实际上对所有排队的事务都这样做,以实现组提交的优化。如果有一个事务排队,处理器仍然执行刷新。处理器不会等待更多的事务,因为这样会增加执行延迟。有关代码参考,请检查syncrequestprocessesor.run()。
- 补白(padding)是指对文件预分配磁盘的存储块。这样做是为了使对文件系统元数据更新时,不会对文件的顺序写性能有显著的影响。如果事务以很高的速度被追加到日志中,并且文件没有预先分配存储块,那么无论何时在写入操作到达文件的结尾,文件系统都需要分配一个新存储块。而通过补白至少可以减少两次额外的磁盘寻址开销:一次是更新元数据,另一次是返回文件。
- 为了避免受到系统中其他写操作的干扰,我们强烈建议您将事务日志写入一个独立的磁盘,将另一个磁盘用于操作系统文件和快照文件。
- 注意:磁盘写缓存
- 服务器只有在强制将事务写入事务日志后才会确认该提议。更准确地说,服务器调用ZKDatabase的commit方法,该方法最终调用FileChannel.force。通过这种方式,服务器保证事务在确认之前已经被持久化到磁盘。不过,有一个需要注意的地方,现代磁盘有一个写缓存,用来存储要写入磁盘的数据。如果启用了写缓存,force调用在返回时并不能保证数据已经写入到磁盘上。实际上,它可能还在写缓存中。为了保证从FileChannel.force()调用返回时写入的数据在媒体上,磁盘写缓存必须被禁用。操作系统有不同的方法来禁用它。
6.2、快照
- 快照是ZooKeeper数据树的副本。每个服务器经常通过序列化整个数据树来获取快照,并将其保存到文件中。服务器在进行快照时不需要进行协作,也不需要停止处理请求。因为服务器在进行快照时还会继续处理请求,所以当快照完成时,数据树可能又发生了变化,我们称这种快照为模糊(fuzzy)快照,因为它们不能反映数据树在特定时间点的确切状态。
- 让我们通过一个示例来说明这一点。假设数据树只有两个znode:/z1和/z2。最初,/z1和/z2的数据都是整数1。现在有以下操作步骤:
- (1)开始一个快照。
- (2)将/z1 = 1序列化并写入快照。
- (3)将/z1的数据设置为2(事务T1)。
- (4)将/z2的数据设置为2(事务T2)。
- (5)序列化并写入/z2 = 2到快照。
- 该快照包含/z1 = 1和/z2 = 2,然而,数据树中这两个znode节点在任意的时间点上都不是这个值。这并不是问题,因为服务器会重播(replay)事务。每一个快照文件都会以快照开始时最后一个被提交的事务作为标记(tag),我们将这个时间戳标记为TS。如果服务器最终加载快照,它会重播事务日志中TS之后的所有事务。在这个例子中,它们是T1和T2。在快照上重放T1和T2之后,服务器获得/z1 = 2和/z2 = 2,这是一个有效的状态。
- 接下来我们还需要考虑一个重要的问题,就是再次应用T2是否有会有问题,因为在快照拍摄时这个事务已经被应用了。如前所述,事务是幂等的(idempotent),因此只要我们以相同的顺序应用相同的事务,我们将得到相同的结果,即使其中一些事务已经应用到快照。
- 为了理解这个过程,假设重复应用一个事务。如上例中所描述,该操作将znode的数据设置为一个特定的值,该值不依赖于其他任何东西。假设我们正在无条件地设置/z2的数据(在setDa请求中版本号是-1)。重新应用操作成功,但我们最终得到了错误的版本号,因为我们增加了它两次。假设下面三个操作都被提交并成功执行,就会导致问题出现:
- setData /z2, 2, -1
- setData /z2, 3, 2
- setData /a, 0, -1
- 第一个setData操作与我们前面描述的相同,但我们已经进行了两个以上的setData操作,我们可能会出现这样的情况:由于版本号不正确,在重播期间第二个操作没有执行。假设这三个请求在提交时都是正确执行的。如果此时服务器加载了最新的快照,其中包含了第一个setData。但服务器仍然会重播第一个setData操作,因为第一个setData是用较早的zxid标记的,所以重新执行第一个setData时,由于zxid的版本与第二个setData操作所期望的版本不匹配,该操作没有通过。而第三个setData操作可以正常完成,因为它也是无条件的。
- 在加载完快照并重放日志后,服务器的状态是不正确的,因为它不包括第二个setData请求。这种执行违反了持久性和执行请求的序列应该是无缺口(nogap)的属性。
- 这种重新应用请求的问题可以通过将事务转换为leader生成的状态增量(tate delta)来解决。当leader为给定的请求生成一个事务时,作为事务生成的一部分,它包括对znode或其数据的请求中的更改,并指定一个固定的版本号。因此,重新应用事务不会导致版本号不一致。
7、服务器与会话
- 会话(Session)是ZooKeeper的一个重要的抽象。保证请求有序、临时znode节点、监事点都与会话密切相关。因此会话的跟踪机制对ZooKeeper来说也非常重要
- ZooKeeper服务器的一个重要任务就是跟踪并维护会话。在独立模式下,单个服务器会跟踪所有的会话。在仲裁模式下,则由leader来跟踪和维护会话。leader和独立服务器实际上运行相同的会话跟踪器(参见SessionTracker和SessionTrackerImpl)。follower只是将所有连接到它的客户端的会话信息转发给leader(参见LearnerSessionTracker)。
- 为了保证会话的存活,服务器需要接收会话的心跳。心跳的形式可以是一个新的请求或显式的ping消息(参见LearnerHan.run())。在这两种情况下,服务器都通过更新会话的过期时间来保持会话的活跃(请参阅SessionTrackerImpl.touchSession())。在仲裁模式下,leader向follower发送PING消息,follower发送自上次PING以来的session列表。leader每隔半tick就向follower发送一个ping信号。一个tick(在179页的“基本配置”中描述)是ZooKeeper使用的最小时间单位,单位是毫秒。所以,如果tick被设置为2秒,那么leader每秒发送一个ping。
- 对于管理会话的过期有两点很重要。一个称为过期队列(参见ExpiryQueue)的数据结构用于保存过期的会话。这个数据结构将会话保存在bucket中,每个bucket对应某一个时间会过期的会话,leader每次会让一个bucket中的会话过期。要确定哪个bucket过期(如果有的话),线程会检查过期队列,找出将要过期的bucket。这个线程在底限时间到来之前一直处于休眠状态,当它醒来时,它将轮询过期队列,取出一批会话让它们过期。当然取出的这批数据也可能是空的。
- 为了维护这些bucket,leader服务器把时间分成一些片段,以expirationInterval为单位进行分割,并把每个会话分配到它的过期时间对应的bucket里,其功能就是有效地计算出一个会话的过期时间,以向上取正的方式获得具体时间间隔。更具体来说,就是对下面的表达式进行计算,当会话的过期时间更新时,根据结果来决定它属于哪个bucket。
- 为了维护这些bucket,leader将时间分割成一些片段,以expirationInterval为单位,并把每个会话分配到它的过期时间对应的bucket里。执行赋值的函数本质上是将会话的过期时间向上取正的方式获得具体的时间间隔。更具体来说,就是对下面的表达式进行计算,当会话的过期时间更新时,根据结果来决定它属于哪个bucket:
- (expirationTime / expirationInterval + 1) * expirationInterval
- 示例,假设expirationInterval是2,会话的超时时间(expirationTime)是10。我们将这个会话分配给bucket12((10/2 + 1)* 2的结果)。注意,当我们触发(touch)到会话时,expirationTime会不断增加,所以我们将会话移动到相应的bucket中,之后会过期。
- 使用bucket方案的一个主要原因是减少检查会话过期的开销。一个ZooKeeper可能有数千个客户端,因此会有数千个会话。以细粒度的方式检查会话过期在这种情况下是不合适的。请注意,如果expirationInterval很短,那么ZooKeeper就会以这种细粒度的方式完成检查。目前expirationInterval是一个tick,通常以秒为单位。
8、服务器与监视点
- 监视点是读取操作设置的一次性触发器,每个监视点都是由特定的操作触发的。为了在服务端管理监视点,ZooKeeper服务器实现了监视点管理器(watch manager)。由WatchManager类的一个实例负责管理当前已被注册的监视点列表,并负责触发它们。所有类型的服务器(独立服务器、leader、follower和observer)都使用同样的方式处理监视点。
- DataTree类中有一个监视点管理器来负责子节点监控和数据的监控,对于这两类监控,当处理一个设置监视点的读请求时,该类就会把这个监视点加人manager的监视点列表。类似的,当处理一个事务时,该类也会查找是否需要触发相应的监视点。如果发现有监视点需要触发,该类就会调用manager的触发方法。添加一个监视点和触发一个监视点都会以一个read请求或者FinalRequestProcessor类的一个事务开始。
- DataTree类中有一个监视点管理器来负责子节点监控和数据的监控,对于这两类监控。当处理一个设置监视点的读操作时,该类将监视点添加到管理员(manager)的监视点列表中。类似地,当处理一个事务时,该类也会查找是否需要触发相应的监视点。如果发现有监视点需要触发,该类就会调用管理员(manager)的触发方法。在FinalRequestProcessor中执行读请求或事务时,可能会添加监视点和触发监视点。
- 在服务端触发了一个监视点,最终会传播到客户端。负责处理传播的为服务端的cnxn对象(参见ServerCnxn类),此对象表示客户端和服务端的连接并实现了watcher接口。Watch.proces5方法序列化了监视点事件为一定格式,以便用于网络传送。ZooKeeper客户端接收序列化的监视点事件,并将其反序列化为监视点事件的对象,并传递给应用程序。
- 在服务端触发了一个监视点,最终会传播到客户端。负责传播任务的是服务器cnxn对象(请参阅ServerCnxn类),此对象表示客户端和服务端的连接并实现Watcher接口。Watch.Process方法将监视点事件序列化为一定的格式,以便网络传送。ZooKeeper客户端接收序列化的监视点事件,并将其反序列化为监视点事件的对象,并传递给应用程序。
- 监视点只会保存在内存中,而不会持久化到硬盘。当客户端与服务端的连接断开时,它的所有监视点都会从内存中清除。因为客户端库也会维护一份监视点的数据,在重连之后监视点数据会再次被同步到新的服务端上。
9、客户端
- 客户端库中有两个主要的类:ZooKeeper和ClientCnxn。Zookeeper类实现了大部分API,写客户端应用程序时必须实例化这个类来创建会话。在创建会话时,ZooKeeper就会使用一个会话标识符来关联这个会话。会话标识符是由服务端所生成的(请参阅SessionTrackerImpl)。
- ClientCnx类管理与服务器的客户端套接字(Socket)连接。该类维护一个可以连接到的ZooKeeper服务器的列表,并且当连接断掉的时候无缝地切换到其他的服务器。当重连到其他的服务器时会使用同一个会话(如果没有过期的话),客户端也会重置所有的监视点到刚连接的服务器上请参阅ClientCnxn.SendThread.primeCon nection())。重置默认是开启的,可以通过设置disableAutolatchReset来禁用。
10、小结
- leader竞选机制是可用性的关键因素,没有这个机制,ZooKeeper套件将无法保持可靠性。拥有leader是必要但非充分条件,ZooKeeper还需要Zab协议来传播状态的更新等,即使某些服务器可能发生崩溃,也能保证状态的一致性。
- 多种服务器类型:独立服务器、leader服务器、follower服务器和观察者服务器。这些服务器之间因运转的机制及执行的协议的不同而不同。在不同的部署场景中,各个服务器可以发挥不同的作用,比如增加观察者服务器可以提供更高的读吞吐量,而且还不会影响写吞吐量。不过,增加观察者服务器并不会增加整个系统的高可用性。
# #
这篇关于zookeeper06-ZooKeeper内部原理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!