大概可以分为以下五个步骤:
相对于单机版的启动,集群版的差异之处在于:
创建QuorumPeer实例
Quorum是集群模式下特有的对象,是Zookeeper服务器实例的托管者,QuorumPeer代表了集群中的一台机器。在运行期间,QuorumPeer会不断检测当前服务器实例的运行状态,同时根据情况发起Leader选举
Leader选举
在集群启动期间,QuorumPeer的状态为Looking,因此进行Leader选举,使用的是FastLeaderElection算法,前文已经讲过。
Leader与Follower启动期间交互、数据同步
Leader与Follower启动
针对于客户端的一次请求,处理步骤一般分为 预处理、事务处理、事务应用、会话响应几个阶段。以setData节点数据事务请求为例。
阶段1. 预处理
将请求交给PreRequestProcessor
zookeeper对每个客户端的请求处理模型采用了责任链模式——每个客户端请求都会由几个不同的请求处理器依次进行处理。
PreRequestProcessor是请求的第一个处理器。
创建请求事务头
包含了事务最基本的信息,包括sessionId、ZXID、CXID和请求类型等。
服务端后续的请求处理器,都是基于该请求头来识别当前请求是否事务请求。
创建请求事务体SetDataTxn
阶段2. 事务处理
将请求交给ProposalRequestProcessor处理器。
ProposalRequestProcessor处理器与提案相关,是Zookeeper中针对事务请求所展开的一个投票流程中对事务操作的包装。从ProposalRequestProcessor开始,请求处理会进入三个子处理流程,这三个流程是同时进行的:
Sync流程——记录事务日志
收到上一个处理器流转过来的请求后。判断是否是事务请求,如果是事务请求,通过事务日志的形式记录下来。
Leader与Follower服务器的请求处理链路中都会有这个处理器,两者在事务日志的记录功能上是完全一致的。只是Leader服务器在接收到事务请求时就进入sync流程,而Follower在收到Leader发起的提议之后再进入sync。
Proposal流程——投票与统计的过程
发起投票,广播proposal
Leader服务器发起事务投票,生成一个Porposal提议,以ZXID作为标识,将该提议广播给所有的Follower服务器。
统计投票
(Follower服务器在接收到提议之后,进入Sync流程来记录事务日志,然后发送ACK消息给Leader。)
Leader根据这些ACK消息来统计是否超过半数,如果超过,进入提议的commit阶段。
广播Commit
Leader向Follower和Observer发送commit消息,以便所有的服务器都能提交该提议。(由于Observer没有参与投票,没有保留该事务的信息,因此Leader将发送一直INFORM消息,包含当前提议的内容)
Commit流程——事务提交
将请求交给CommitProcessor处理器,等待Proposal投票。在这个阶段commit流程需要等待,直到投票结束。
请求提交。投票通过之后,将请求交给下一个请求处理器:FinalRequestProcessor。
阶段3. 事务应用
将请求交给FinalRequestProcessor处理器
在之前的请求处理逻辑中,我们仅仅将事务记录到了事务日志中,而内存数据库中的状态尚未变更。因此在这个环节,需要将事务变更应用到内存数据库ZKDatabase中。
将事务请求放入队列:commitProposal。用来保存最近被提交的事务请求,以便集群之间进行数据的快速同步。
阶段4. 会话响应
创建响应体、创建响应头、序列化响应、IO层发送响应给客户端。
Zookeeper的客户端主要由以下几个核心组件组成
2. ClientCnxn:网络IO核心线程
包含两个线程:
SendThread:IO线程,负责客户端与服务端所有的网络IO
EventThread:事件线程,负责对服务端事件进行处理
队列:waitingEvents,待处理事件队列
两个关键队列:
outgoingQueue:客户端请求发送队列
pendingQueue:客户端等待服务端响应的等待队列
ClientCnxnSocket:底层Socket通信层
ClientCnxnSocket定义了底层Socket通信接口,是从ClientCnxn中抽取出的一个接口类。
在使用客户端时,可以通过在系统变量中配置自定义实现类,默认的实现是使用Java原生的NIO接口:ClientCnxnSocketNIO。另外还有实现Netty框架的方式。
Zookeeper中的数据存储分为两部分:内存数据存储和磁盘数据存储。
DataNode
DataNode是数据存储的最小单元,保存了节点的数据内容、ACL列表、节点状态等。
DataTree
DataTree是Zookeeper的内存数据存储核心,是一个树的数据结构,代表了内存中一份完整的数据。
DataTree的底层数据结构是一个ConcurrentHashMap键值对结构。可以说,对于Zookeeper数据的所有操作,都是对这个Map结构的操作。key为节点的路径path,value为具体节点内容DataNode。
ZKDatabase
ZKDatabase是Zookeeper的内存数据库,负责管理Zookeeper的所有会话、DataTree存储和事务日志。
ZKDatabase会定时向磁盘dump快照数据,同时在Zookeeper启动的时候,会通过磁盘上的事务日志和快照数据文件恢复成一个完整的内存数据库。
数据仅存储在内存是很不安全的,zk采用事务日志文件及快照文件的方案来落盘数据,保障数据在不丢失的情况下能快速恢复
针对于客户端的每一次事务操作,Zookeeper除了将数据变更到内存数据库中,还会将它们记录到事务日志中。
事务日志文件的特点:
事务日志文件名的后缀是一个事务id,并且是写入该事务日志文件的第一条事务的ZXID。
我们可以以此迅速地定位到某一个事务操作所在的事务日志。
文件大小都是64MB。
磁盘预分配空间,在文件创建之初就向操作系统预分配一个很大的磁盘块,默认为64MB,一旦分配的文件空间不足4KB,将再次进行预分配,使用"\0"来填充这些被扩容的文件空间。
Zookeeper中所有的事务操作都需要记录到日志文件中,事务日志记录的次数达到一定数量后,就会将内存数据库序列化一次,使其持久化保存到磁盘上,序列化后的文件称为"快照文件"。有了事务日志和快照,就可以让任意节点恢复到任意时间点。
顾名思义,数据快照记录的是Zookeeper中某一个时刻的全量内存数据内容。
与事务日志一致,快照数据文件也是使用ZXID的十六进制来作为文件后缀名。
该后缀标志了本次快照开始时刻的服务器最新ZXID,在数据恢复阶段,会根据此后缀来确定数据恢复的时间点。
不一致的是,快照文件没有预分配机制。
数据的初始化工作,其实就是从磁盘中加载数据的过程,主要包括了从快照文件中加载快照数据和根据事务日志进行数据订正两个过程。
初始化FileTxnSnapLog
FileTxnSnapLog是Zookeeper事务日志和快照数据访问层,分为了FileTxnLog事务日志管理器和FileSnap快照数据管理器的初始化。
初始化ZKDatabase
创建PlayBackListener监听器
PlayBackListener主要用来接收事务应用过程中的回调。
处理快照文件
解析完快照文件之后,就已经基于快照构建了一个完整的DataTree实例了,根据这个快照文件的文件名就可以解析出一个最新的ZXID:zxid_for_snap,该ZXID代表了该Zookeeper开始进行数据快照的时刻。
处理事务日志
获取所有zxid_for_snap之后提交的事务,将其逐个应用到之前基于快照数据文件恢复出来的DataTree中去。
在事务应用的过程中,每当有一个事务被应用到内存数据库中,Zookeeper同时会回调PlayBackListener监听器,将这个事务操作记录转换成Proposal,并保持到ZKDatabase.committedLog中,以便Follower进行快速同步。
当机器完成Leader选举之后,Learner会向Leader服务器进行注册,完成注册之后,就进入数据同步环节。数据同步就是Leader服务器将那些没有在Learner服务器上提交过的事务请求同步给Learner服务器。
数据同步初始化工作
在开始数据同步之前,Leader服务器会进行数据同步初始化。首先从Zookeeper的内存数据库中提取出事务请求对应的提议缓存队列:Proposals,同时完成对以下三个ZXID值的初始化:
集群数据同步
集群数据同步通常分为四类:
1. 差异化同步(DIFF)
场景:minCommittedLog < peerLastZxid < maxCommittedLog
同步内容:peerLastZxid ~ maxCommittedLog 中的每个proposal
Leader
Learner
2. 先回滚再差异化同步(TRUNC + DIFF)
3. 回滚同步(TRUNC)
4. 全量同步(SNAP)