zookeeper(三) 管理

Posted by ZhouJ000 on February 11, 2019

zookeeper(一) 基础
zookeeper(二) Java API
zookeeper(三) 管理

内部原理

角色

zk-ele1

  • 领导者Leader:负责进行投票的发起和决议,更新系统状态
  • 学习者Learner
    • 跟随者Follower:用于接受客户端请求并向客户端返回结果,在选主过程中参与投票
    • 观察者ObServer:可以接受客户端的连接,将写请求转发给Leader节点。但是ObServer不参与投票过程,只是简单的接收投票结果,同步Leader的状态。因此增加再多的ObServer也不会影响集群的写性能,ObServer的目的就是为了扩展系统,提高读取速度,除此之外与Follower一样
  • 客户端Client:发起请求

群首Leader为集群中的服务器选择出来的一个服务器,并会一直被集群所认可。设置群首的目的是为了对客户端发起的ZooKeeper状态变更请求进行排序,包括create、setData和delete操作。群首将每一个请求转换为一个事务,将这些事务发送给追随者,确保集群按照群首确定的顺序接受并处理这些事务

因为观察者不参与决定提议接受与否的投票,群首不需要发送提议到观察者,群首发送给观察者的提交信息只包含zxid而不包含提议本身。因此仅仅发送提交消息给观察者并不能使其实施提议。简单来说,追随者接受两种消息而观察者之接受一种消息。追随者从一次广播中获取提议的内容,并从接下来的一条提交消息中获取zxid。相比之下观察者只获取一条包含已提交提议的内容的INFORM消息

由于ObServer不参与投票的特性,所以它不属于ZooKeeper集群的关键部位,即使Failed或者从集群中断开,也不会影响集群的可用性。根据Observer的这个特点,可以使用ObServer做跨数据中心部署。如果把Leader和Follower分散到多个数据中心的话,因为数据中心之间的网络的延迟,势必会导致集群性能的大幅度下降。使用ObServer的话,将ObServer跨机房部署,而Leader和Follower部署在单独的数据中心,这样更新操作会在同一个数据中心来处理,并将数据发送给其他(包含ObServer的)数据中心,然后Client就可以在其他数据中心查询数据了。但是使用了ObServer并非就能完全消除数据中心之间的延迟,因为ObServer还得接收Leader的同步结果,和ObServer有更新请求也必须转发到Leader,所以在网络延迟很大的情况下还是会有影响的,它的优势就为了本地读请求的快速响应

使用ObServer模式,可以在对应节点的配置文件中添加peerType=observer告诉ZooKeeper该节点是ObServer。其次还需要在配置文件中指定哪些节点被指定为ObServer,即server.4:127.0.0.1:5555:5556:observer

每个Server在工作中有三种状态:
1、LOOKING:竞选状态,当前Server不知道Leader是谁,正在搜寻
2、LEADING:领导者状态,当前Server即为选举出来的Leader
3、FOLLOWING:随从状态,Leader已经选举出来了,同步Leader状态,参与投票
4、OBSERVING:观察状态,Leader已经选举出来了,同步Leader状态,不参与投票

流水线

群首、追随者和观察者根本上都是服务器。在实现服务器时使用的主要抽象概念是请求处理器,请求处理器是对处理流水线上不同阶段的抽象。每一个服务器实现了一个请求处理器的序列

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

Zookeeper中最简单的流水线是独立服务器(ZeeKeeperServer类) zk-flow1

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

群首服务器:(LeaderZooKeeper类) zk-flow2

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

追随者和观察者服务器: zk-flow3

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

选举

一个服务器必须被仲裁的法定数量的服务器所认可。选举并支持一个群首的集群服务器数量必须至少存在一个服务器进程的交叉,我们使用属于仲裁(quorum)来表示这样一个进程子集,仲裁模式要求服务器之间两两相交。一组服务器达到仲裁法定数量是必需条件,如果足够多的服务器永久性地退出,无法达到仲裁法定数量,ZooKeeper也就无法取得进展

每个服务器启动后进入LOOKING状态,开始选举一个新的群首或查找已经存在的群首。如果群首已经存在,其他服务器就会通知这个新启动的服务器,与此同时新的服务器会与群首建立连接,以确保自己的状态与群首一致。如果集群中所有的服务器均处于LOOKING状态,这些服务器之间就会进行通信来选举一个群首,通过信息交换对群首选举达成共识的选择。在本次选举过程中胜出的服务器将进入LEADING状态,而集群中其他服务器将进入FOLLOWING状态 zk-ele2 对于群首选举的消息,称为群首选举通知消息,简称为通知。该协议非常简单,当一个服务器进入LOOKGING状态,就会向集群中每个服务器发送一个通知消息,该消息中包括该服务器的投票(vote)信息,投票中包含服务器标识符(sid)和最近执行的事务的zxid信息。比如一个服务器发送的投票为(1, 5),表示该服务器的sid为1,最近执行的事务的zxid为5(出于群首选举的目的,zxid只有一个数字,而在其他协议中,zxid则有时间戳与计数器组成)

zxid

ZooKeeper服务器会在本地处理只读请求(exists、getData和getChildren)。假如一个服务器接受到客户端的getData请求,服务器读取该状态信息,并将这些信息返回给客户端。因为服务器会在本地处理请求,所以ZooKeeper在处理以只读请求为主的负载时,性能会很高

会改变ZooKeeper状态的客户端请求(create、delete和setData)将会被转发给群首,群首执行相应的请求,并形成状态更新,称之为事务(transaction)。其中请求表示源自于客户端发起的操作,而事务则包含了对应请求处理而改变ZooKeeper状态所需要执行的步骤。比如一个客户端提交了一个对/z节点的setData请求,setData将会改变该znode节点的数据信息,并会增加该节点的版本号,因此对于这个请求的事务包括了两个重要字段:节点中新的数据字段值和该节点新的版本号。当处理该事务时,服务端将会用事务中的数据信息来替换/z节点中原来的数据信息,并会用事务中的版本号更新该节点,而不是增加版本号的值

一个事务为一个单位,也就是所有变更需要以原子方式执行。因此ZooKeeper集群以事务方式运行,并确保所有的变更操作以原子方式被执行,同时不会被其他事务所干扰。在ZooKeeper中,并不存在传统的关系型数据库中的回滚机制,而是确保事务的每一步操作都互不干扰。同时一个事务还具有幂等性,也就是可以对一个事务执行多次,得到的结果也是一样的,前提是确保多个事务的执行顺序是一样的。事务的幂等性可以使恢复处理更加简单

当群首产生了一个事务,就会为该事务分配一个标识符,称之为ZooKeeper会话ID(zxid),通过zxid对事务进行标识,就可以按照群首所指定的顺序在各个服务器中按序执行。服务器之间在进行新的群首选举时也会交换zxid信息,这样就可以知道哪个无故障的服务器接受了更多的事务,并可以同步它们之间的状态信息。zxid为一个long型(64位)整数,分为两部分:时间戳(epoch)部分和计数器(counter)部分,每个部分32位

算法

zookeeper提供了三种方式:
1、LeaderElection
2、AuthFastLeaderElection
3、FastLeaderElection(默认)
在3.4.0后的Zookeeper的版本只保留了TCP版本的FastLeaderElection选举算法

在选举过程中,某个服务器首先向所有服务器提议自己要成为Leader,当其它服务器收到提议以后,解决epoch和zxid的冲突,并接受对方的提议,然后向对方发送接受提议完成的消息,重复这个流程,统计投票,最后一定能选举出Leader。其中当服务器收到一个投票信息,该服务器将会根据以下规则修改自己的投票信息:
1、将收到的voteId和voteZxid作为一个标识符,并获取接收方当前的投票中的信息,用myZxid和mySid表示接收方服务器自己的值
2、如果(voteZxid > myZxid)或者(voteZxid = myZxid && voteId > mySid),修改自己的投票信息,将voteZxid赋值给myZxid,将voteId赋值给mySid,并再次将该投票发送出去
3、否则,保留当前的投票信息,坚持自己的投票,不做任何变更
简而言之,只有最新的服务器将赢得选举,因为其拥有最近一次的zxid。如果多个服务器拥有最新的zxid,其中的sid值最大的将赢得选举。当一个服务器接收到仲裁数量的服务器发送的投票都一样时,就表示群首选举成功,如果被选举的群首为某个服务器自己,该服务器将会开始行使群首角色,否则就成为一个追随者并尝试连接被选举的群首服务器(并未保证追随者必然会连接成功)。一旦连接成功,追随者和群首之间将会进行状态同步,在同步完成后,追随者才可以处理新的请求,即选举成功后:
1、Leader等待Follower连接
2、Follower发送zxid
3、确定同步点、同步完成
4、集群开始对外提供服务 zk-ele3

在现在的实现中,默认的群首选举的实现类为FastLeaderElection,其中使用固定值200ms为延迟时间,这个值比当今数据中心所预计的长消息延迟(不到1秒到几毫秒)要长得多,但与恢复时间相比还不够长。最初的群首选举算法的实现采用基于拉取式的模型,一个服务器拉取投票值的间隔大概为1秒,该方法增加了恢复的延迟时间,相比现在的实现方式,我们可以更加快速地进行群首选举。万一延迟时间不是很长,一个或多个服务器最终将错误选举一个群首,从而导致该群首没有足够的追随者,那么服务器将不得不再次进行群首选举。错误地选举一个群首可能会导致整个恢复时间更长,因为服务器将会进行链接以及不必要的同步操作,并需要发送更多消息来进行另一轮的群首选举

扩展:
【分布式】Zookeeper的Leader选举
ZooKeeper 工作、选举 原理

basic paxos:选举线程向所有server发起一次询问,按照服务器选取规则去比较,选出下一次要询问的server,当被选取的server有一半server支持,则成为leader服务器,不然就一直选举,直到选出了leader
fast paxos:一个server声明自己要做leader,其它server将配合工作,解决zxid和epoch冲突,并向该server发送接收提议的消息(zxid优先、轮次(淘汰性能,网络条件不好的竞争者)、sid(配置文件指定)较大者胜出)

扩展:
Paxos协议超级详细解释+简单实例
Basic-Paxos原理
Basic Paxos介绍与论述
Fast Paxos
Paxos–>Fast Paxos–>Zookeeper分析
分布式系统理论进阶 - Paxos变种和优化
比较raft ,basic paxos以及multi-paxos

zad协议

  • 使用timeout来重新选择leader
  • 采用quorum来确定整个系统的一致性(也就是对某一个值的认可),这个quorum一般实现是集群中半数以上的服务器,ZK还提供了带权重的quorum实现
  • 由leader来发起写操作
  • 采用心跳检测存活性
  • leader election都采用先到先得的投票方式
  • 使用epoch和count的组合来唯一表示一个值

原子广播

服务器怎么确认一个事务是否已经提交?

Zookeeper的核心是原子广播,这个机制保证了各个Server之间的同步。实现这个机制的协议叫做Zab协议。Zab协议有两种模式,它们分别是恢复模式(选主)和广播模式(同步)。通过该协议提交一个事务非常简单,类似于一个两阶段提交:
1、群首向所有追随者发送一个PROPOSAL消息p
2、当一个追随者接受到消息p后,会响应群首一个ACK消息,通知群首其已接受该提案(proposal)
3、当收到仲裁数量的服务器发送的确认信息后(该仲裁数量包括群首自己),群首就会发送消息通知追随者进行提交(COMMIT)操作

在应答提案消息之前,追随者还需要执行一些检查操作。追随者将会检查所发送的提案消息是否属于其追随的群首,并确认群首所广播的提案消息和提交事务的顺序正确

Zab保障了一下几个重要属性:
1、如果群首按书序广播了事务T和T’,那么每个服务器在提交T’事务前保证事务T已经提交完成(保证事务在服务器之间的传送顺序一致)
2、如果某个服务器按照事务T、事务T’的顺序提交事务,所有其他服务器也必然会在提交事务T’前提交事务T(竖向地保证服务器不会跳过任何事务)

广播模式

在接收到一个写请求操作后,追随者会将请求转发给群首,群首将探索性地执行该请求,并将执行结果以事务的方式对状态更新进行广播。一个事务中包含服务器需要执行变更的确切操作,当事务提交时,服务器会将这些变更反馈到数据树上,数据数即为ZooKeeper保存状态信息的数据结构

  • leader以相同的顺序向所有followers发送提案,且这一顺序和收到请求的顺序保持一致。因为使用了FIFO通道,于是保证followers也按此顺序收到提案
  • followers以收到提案的顺序处理消息。这意味着消息将被有序地ACK且leader按此顺序收到ACK,仍然是由FIFO通道保证。这也意味着如果某提案上的消息m被写到非易失性存储(硬盘)上,所有在m前提出的提案上的消息也已被写到非易失性存储上
  • 当法定人数的followers全部ACK某消息后,leader会发出一个COMMIT提案。因为所有消息是按序ACK的,leader发出COMMIT且followers收到该提案也是按序的
  • COMMIT按序被处理。提案被提交后意味着followers可以分发提案上的消息了(发送给客户端)

恢复模式

ZooKeeper自始至终并不总是只有一个活动的群首,因为群首服务器也可能崩溃,或短时间失去连接,此时其他服务器需要选举一个新的群首以保证系统整体仍然可用。当服务启动或者在领导者崩溃后,Zab就进入了恢复模式,当领导者被选举出来,且大多数Server完成了和Leader的状态同步以后,恢复模式就结束了。状态同步保证了Leader和Server具有相同的系统状态。其中时间戳(epoch)的概念代表了管理权随时间的变化情况,一个时间戳表示了某个服务器行使管理权的这段时间,在一个时间戳内,群首会广播提案消息,并根据计数器(counter)识别每一个消息。zxid的第一个元素就是时间戳信息,因此每个zxid可以很容易地与事务被创建时间戳相关联

时间戳的值在每次新群首选举发生的时候便会增加。同一个服务器成为群首后可能持有不同的时间戳信息,但从协议的角度出发,一个服务器行使管理权时,如果持有不同的时间戳,该服务器就会被认为是不同的群首。如果服务器s成为群首并且持有的时间戳为4,而当前已经建立的群首的时间戳为6,集群中的追随者会追随时间戳为6的群首s,处理群首在时间戳6之后的消息。当然追随者在恢复阶段也会接收时间戳4到时间戳6之间的提案消息,之后才会开始处理时间戳为6之后的消息,而实际上这些提案消息是以时间戳6’的消息来发送的

在仲裁模式下,记录已接收的提案消息非常关键,这样可以确保所有的服务器最终提交了被某个或多个服务已经提交完成的事务,即使群首在此时发生了故障。完美检测群首(或任何服务器)是否发生故障是非常困难的,虽然不是不可能,但在很多设置的情况下,都可能发生对一个群首是否发生故障的错误判断

实现这个广播协议所遇到最多的困难在于群首并发存在情况的出现,这种情况并不一定是脑裂场景。多个并发的群首可能会导致服务器提交事务的顺序发生错误,或者直接跳过了某些事务。为了阻止系统中同时出现两个服务器自认为自己是群首的情况是非常困难的,时间问题或消息丢失都可能导致这种情况,因此广播协议并不能基于以上假设。为了解决这个问题,Zab协议提供了以下保障:
1、一个被选举的群首确保在提交完所有之前的时间戳内需要提交的事务,之后才开始广播新的事务
2、在任何时间点,都不会出现两个被仲裁支持的群首
为实现第一个需求,群首不会马上处于活动状态,直到确保仲裁数量的服务器认可这个群首新的时间戳值。一个时间戳的最初状态必须包含所有的之前已经提交的事务,或者某些已经被其他服务器接受,但尚未提交完成的事务。这一点非常重要,在群首进行时间戳e的任何新的提案之前,必须保证自时间戳开始至到时间戳e-1内的所有提案被提交。如果一个提案消息处于时间戳e’<e,在群首处理时间e的第一个提案消息前没有提交之前的这个提案,那么旧的提案将永远不会被提交。
对于第二个需求,并不能完全阻止两个群首独立地运行。假设一个群首l管理并广播事务,在此时,仲裁数量的服务器Q判断群首l已经退出,并开始选举了一个新的群首l’,假设在仲裁机构Q放弃群首l时有一个事务T正在广播,而且仲裁机构Q的一个严格的子集记录了这个事务T,在群首l’被选举完后,在仲裁机构Q之外的服务器也记录了这个事务T,为事务T形成了一个仲裁数量,在这种情况下,事务T在群首l’被选举后会进行提交。Zab协议保证T作为事务的一部分被群首l’提交,确保群首l’的仲裁数量的支持者中至少有一个追随者确认了该事务T。我们知道仲裁数量必须有一台以上服务器之上有所重叠,这样群首l用来提交的仲裁数量和新群首l’使用的仲裁数量必定在一台以上的服务器是一致的。因此l’将事务T加入自身的状态并传播给其他追随者。在群首选举时,我们选择zxid最大的服务器作为群首,这使得ZooKeeper不需要将提议从追随者传到群首,而只需要将状态从群首传播到追随者

在时间戳发生转换时,ZooKeeper使用两种不同的方式来更新追随者来优化这个过程。如果追随者滞后于群首并不多,群首只需要发送缺失的事务点。因为追随者按照严格的顺序接收事务点,这些缺失的事务点永远是最近的。这种更新在代码中被称之为DIFF。如果追随者滞后很多,ZooKeeper将发送在代码中被称为SNAP的完整快照。因为发送完整的快照会增大系统恢复的延时,发送缺失的事务是更优的选择。可是当追随者滞后太远的情况下,只能选择发送快照

扩展:
分布式事务与一致性算法Paxos & raft & zab
Raft对比ZAB协议

其他

本地存储

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

因为写事务日志是写请求操作的关键路径,因此ZK必须有效处理写日志问题,一般情况下追加文件到磁盘会有效完成,但还有一些情况可以使ZK运行更快,组提交和补白。组提交是指一次磁盘写入时追加多个事务,这将使持久化多个事务只需要一次磁道寻址的开销。补白指在文件中预分配磁盘存储块。为避免磁盘受到系统中其他写操作的干扰,建议将事务日志写入到一块独立的磁盘,将第二块磁盘用于操作系统文件和快照文件

服务器只有在强制将事务写入事务日志之后才确认对应的提议。更准确的说,服务器调用ZKDatabase的commit方法,这个方法最终调用FileChannel.force,服务器保证在确认事务之前已经将它持久化到磁盘中。需要注意的是,现在磁盘有一个缓存用于保存将要写入磁盘的数据,如果写缓存开启,force操作返回后并不能保证数据已经写入介质,因此需要关闭

快照
是ZK数据树的拷贝副本,每一个服务器会经常以序列化整个数据树的方式来提取快照,并将这个提取的快照保存到文件中。服务器在进行快照时不需要进行协作,也不需要暂停处理请求。因为服务器在进行快照时还会继续处理请求,所以当快照完成时,数据树可能又发生了变化,称这样的快照是模糊的(fuzzy),因为它们不能反映出在任意给定时间点数据树的准确状态。服务器会重播(replay)事务,每一个快照文件都会以快照开始时最后一个被提交的事务作为标记(tag),将这个时间戳记为TS,如果服务器最后加载快照,它会重播在TS之后的所有事务日志中的事务。而且事务是幂等的,所以按照相同的顺序再次执行相同的事务,也会得到相同的结果,即便其结果已经保存到快照中

服务器与会话

会话(session)是ZooKeeper的一个重要抽象,会话跟踪机制对ZooKeeper来说也非常重要。ZooKeeper服务器的一个很重要任务就是跟踪并维护这些会话。在独立模式下,单个服务器会跟踪所有会话,而在仲裁模式下则由群首服务器来跟踪和维护。群首服务器和独立模式的服务器实际上运行相同的会话跟踪器(SessionTracker类和SessionTrackerImpl类)。而追随者服务器仅仅是简单地把客户端连接的会话信息转发给群首服务器

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

对于管理会话的过期有两个重要的要点。一个称为过期队列的数据结构(ExpiryQueue),用于维护会话的过期。这个数据结构使用bucket来维护会话,每个bucket对应一个某时间范围内的会话,群首服务器每次会让一个bucket的会话过期。为确定哪一个bucket的会话过期,当下一个底限到来时,一个线程会检查这个expiry queue来找出要过期的bucket。这个线程在底限时间到来之前处于睡眠状态,当它被唤醒时,它会取出过期队列的一批session,让它们过期。为维护这些bucket,群首服务器把时间分成一些片段,以expirationInterval为单位进行分割,并把每个会话分配到它的过期时间对应的bucket里,其功能就是有效地计算出一个会话的过期时间,以向上取整的方式获得具体时间间隔。使用bucket模式来管理的一个重要原因是为了减少会话过期这项工作的系统开销

服务器与监视点

监视点是由读取操作所设置的一次性触发器,每个监视点由一个特定操作触发。为了在服务端管理监视点,ZK服务端实现了监视点管理器,一个WatchManager类的实例负责管理当前已被注册的监视点列表,并负责触发它们

DataTree类中持有一个监视点管理器来负责子节点监控和数据的监控。当处理一个设置监视点的读请求时,该类就会把这个监视点加入manager的监视点列表。类似的,当处理一个事务时,该类也会查找是否需要触发相应的监视点,如果发现有监视点需要触发,该类就会调用manager的触发方法。添加和触发一个监视点都会以一个read请求或FinalRequestProcessor类的一个事务开始

在服务端触发了一个监视点,最终会传播到客户端,负责处理传播的为服务端的cnxn对象(ServerCnxn),此对象表示客户端和服务端的连接并实现了Watcher接口。Watcher.process方法序列化了监视点事件为一定格式,以便网络传输,ZK客户端接受序列化的事件,反序列化为监视点的事件对象,传递给应用程序

监视点只会保存在内存,而不会持久化到硬盘。当客户端与服务端的连接断开时,它的所有监视点会从内存中清除。因为客户端库也会维护一份监视点的数据,在重连之后监视点数据会再次被同步到服务端

客户端

客户端库中有2个主要的类:ZooKeeper与ClientCnxn。ZooKeeper类实现了大部分API,写相关代码必须实例化这个类来建立一个会话。一旦建立起一个会话,ZooKeeper就会使用一个会话标识符来关联这个会话,这个会话标识符实际上是由服务端生成的

ClientCnxn类管理连接到server的Socket连接,该类维护了一个可连接的ZK服务器列表,并当连接断掉的时候无缝切换到其他服务器。当重连到一个其他服务器时会使用同一个会话(如果还没过期),客户端也会重置所有的监视点到刚连接的服务器上(ClientCnxn.SendThread.primeConnection)。重置默认是开启的,可以通过设置disableAutoWatchReset来禁用

运维管理

ZooKeeper服务器在启动时从一个名为zoo.cfg的配置文件读取所有选项,多个服务器如果角色相似,同时基本的配置信息一样,就可以共享一个文件。data目录下的myid文件用于区分各个服务器,对每个服务器来说,data目录必须是唯一的,因此这个目录可以更加方便地保存一些差异化文件。服务器ID将myid文件作为一个索引引入到配置文件中,一个特定的ZooKeeper服务器可以知道如何配置自己的参数。在启动服务器时,也可以通过-D选项设置属性,不过配置文件中的配置参数优先于系统属性中的配置

基础配置
clientPort:客户端所连接的服务器所监听的TCP端口,默认为2181
dataDir和dataLogDir:dataDir用于配置内存数据库保存的模糊快照的目录,如果某个服务器为集群中的一台,id文件也会保存在该目录下。dataDir并不需要配置到一个专用存储设备上,快照将会以后台线程方式写入,且不会锁数据库,也非同步写入。事务日志对该目录所处的存储设备上其他活动更加敏感,服务端会尝试进行顺序写入事务日志,因为服务端在确认一个事务前必须将数据同步到存储中,该设备其他活动(尤其是快照)可能导致同步时磁盘忙碌,影响写入吞吐能力,因此最佳实践是使用专用的日志存储设备,将dataLogDir的目录配置指向该设备
tickTime:为ZK使用的基本时间度量单位,单位毫秒,该值还决定了会话超时的存储器大小。ZK集群中使用的超时时间单位通过tickTime制定,实际上是设定了超时时间的下限值,因为最小的超时时间为一个tick时间,客户端最小会话超时时间为两个tick时间。默认值为3000毫秒,更低的tickTime值可以更快地发现超时问题,当也会导致更高的网络流量(心跳消息)和更高的CPU使用率(会话存储器的处理)

存储配置
preAllocSize:设置预分配的事务日志文件的大小值,单位KB。当写入事务日志文件时,服务端每次会分配preAllocSize的存储大小,可以分摊文件系统将磁盘分配存储空间和更新元数据的开销,而且也减少了文件寻址操作的次数。默认大小为64MB,缩小该值的原因是事务日志永远不会达到这么大
snapCount:指定每次快照之间的事务数。当ZooKeeper服务器重启后要恢复其状态,恢复时间因素分别是为恢复状态而读取快照的时间以及快照启动后所发生的事务的执行时间。执行快照可以减少读入快照文件后需要应用的事务数量,但是进行快照也会影响服务器性能,即便是通过后台线程方式进行写入操作。默认值为100000,因为进行快照时影响性能,所以集群中所有服务器最好不要同一时间进行快照操作,只要仲裁服务器不会一同进行快照,因此每次快照实际事务数为接近snapCount的随机值
autopurge.snapRetainCount:当进行清理数据操作时,需要保留在快照数量和对应的事务日志文件数量。ZooKeeper会定期对快照和事务日志进行垃圾回收操作,这个值指定了垃圾回收时需要保留的快照数
autopurge.purgeInterval:对快照和日志进行垃圾回收(清理)操作的时间间隔的小时数
fsync.warningthresholdms:触发警告的存储同步时间阀值
weight.x=n:常常以一组配置,该选项指定组成一个仲裁机构的某个服务器的权重为n,即投票时的权重值
traceFile:持续跟踪ZooKeeper的操作,并将操作记录到跟踪日志中。除非设置了该选项,否则跟踪功能不会启用

网络配置
globalOutstandingLimit:ZooKeeper中待处理请求的最大值。ZK客户端请求比ZK服务端处理快的多,服务端对接收到的请求队列化,最终可能导致服务端内存溢出,该值可以限制待请求数量。但不是硬限制,因为每个客户端至少有一个待处理的请求,否则会导致客户端超时,因此达到该值后,服务端还会继续接受客户端连接中的请求,条件是这个客户端在服务器中没有任何待处理的请求
maxClientCnxns:允许每个IP地址的并发socket连接的最大数量
clientPortAddress:限制客户端连接到指定的接收信息的地址上。默认ZooKeeper服务器会监听在所有网络接口地址上等待客户端连接。主要针对多网络接口服务器
minSessionTimeout:最小会话超时时间,单位毫秒,默认值为tickTime值的两倍
maxSessionTimeout:会话的最大超时时间,客户端建立一个连接后会请求一个明确的超时值,该值不小于minSessionTimeout不大于maxSessionTimeout

集群配置
initLimit:对追随者最初连接到群首时的超时值,单位为tick的倍数,需要在网络中按数据量进行追随者与群首间网络基准测试,规划时间
syncLimit:对于追随者与群首进行sync操作的超时值
leaderServes:配置yes或no标志,指示群首服务器是否为客户端提供服务
server.x=[hostname]:n:n[:observer]:服务器x的配置参数
cnxTimeout:在群首选举打开一个新的连接的超时值
electionAlg:选举算法的配置选项,实际上除了默认配置外,其他算法已经弃用了

集群配置

多数原则:当集群中有足够的ZooKeeper服务器来处理请求时,称这组服务器的集合为仲裁法定人数,我们不希望有两组不相交的服务器集合同时处理请求,否则就会进入脑瘤模式中。所以通过定义仲裁法定人数至少为所有服务器中的多数(一半以上)。当配置多个服务器来组成ZooKeeper集群时,默认使用多数原则作为仲裁法定人数量,ZooKeeper会自动检测是否运行于复制模式,从配置文件中读取时确认是否拥有多个服务器的配置信息,并默认使用多数原则的仲裁法定人数

法定人数的一个重要属性是,如果一个法定人数解散了,集群中另一个法定人数形成,这两个法定人数至少有一个服务器必须交集。多数原则的法定人数无疑满足了这一交集的属性。一般法定人数并未限制必须满足多数原则,ZooKeeper也允许灵活配置,这种特殊方案就是对服务器进行分组配置的时候,会将服务器分组成不相交的集合并分配不同的服务器权重,通过这种方案来组成法定人数,需要使用多数组中的服务器形成多数投票原则。例如:

group.x=n[:n]

分为3组,每个服务器的权重一样,为了构成法定人数,需要两个组以及这两个组各取两个服务器,也就是总共4个服务器
group.1=1:2:3
group.2=4:5:6
group.3=7:8:9

当在跨多个数据中心部署ZooKeeper服务时,这种配置方式有很多优点。例如一组可能表示运行不同数据中心的一组服务器,即使这个数据中心崩溃,ZooKeeper服务也可以继续提供服务

使用weight.x=n配置不同的权重,也可以在只有两个数据中心时表示优先权

重配置:当需要扩展ZooKeeper服务器列表时,可以不需要重启。在配置文件中添加dynamicConfigFile=./dyn.cfg指定动态配置,在dyn.cfg文件中配置server.id=host:n:n[:role];[client_address:]client_port,与正常的配置文件一样,role选项为participant或observer。在使用重配置之前必须先创建这些文件,就绪后可以通过reconfig操作来重新配置一个集群,该操作可以增量或全量地进行更新操作

监控

提供了一些命令,比如:
1、ruok:提供(有限的)服务器的状态信息
2、stat:提供服务器的状态信息和当前活动的连接情况
3、dump:提供会话信息
4、conf:列出该服务器启动运行使用的基本配置参数
5、envi:提供各种java环境参数
6、mntr:提供比stat更详细的服务器统计数据
7、wchs:列出该服务器所追踪的监视点的摘要信息
8、wchc:列出所有跟踪监视点的详细信息,根据会话进行分组
9、wchp:列出所有跟踪监视点的详细信息,根据znode节点路径分组
10、cons:列出服务器每个连接的详细统计信息

除此之外,还可以通过JMX进行监控,比如jconsole等工具