ZooKeeper系统模型之数据初始化。

        在ZooKeeper服务器启动期间,首先会进行数据初始化工作,用于将存储在磁盘上的数据文件加载到到ZooKeeper服务器内存中。

初始化流程

        首先我们先从整体上来看ZooKeeper的数据初始化过程,下图展示了数据的初始化流程。

ZooKeeper系统模型之数据初始化。

        数据的初始化工作,其实就是从磁盘中加载数据的过程,主要包括了从快照文件中加载快照数据和根据事务日志进行数据订正两个过程。

初始化FileTxnSnapLog

        FileTxnSnapLog是ZooKeeper事务日志和快照数据访问层,用于衔接上层业务与底层数据存储。底层数据包含了事务日志和快照数据两部分,因此FileTxnSnapLog内部又分为FileTxnLog和FileSnap的初始化,分别代表事务日志管理器和快照数据管理器的初始化。

初始化ZKDatabase

        完成FileTxnSnapLog的初始化后,我们就完成了ZooKeeper服务器和底层数据存储的对接。接下来就要开始构建内存数据库ZKDatabase了。在初始化过程中,首先会构建一个初始化的DataTree,同时会将步骤1中初始化过程中,首先会构建一个初始化的DataTree,同时会将步骤1中初始化的FileTxnSnapLog交给ZKDatabase,以便内存在数据库能够对事务日志和快照数据进行访问。

        DataTree是ZooKeeper内存数据的核心模型,简而言之就是一棵树,保存了ZooKeeper上的所有节点信息,在每个ZooKeeper服务器内部都是单例。在ZKDatabase被初始化的时候,DataTree也会进行相应的初始化工作——创建一些ZooKeeper的默认节点,包括/、/zookeeper和/zookeeper/quota三个节点的创建。

        除了ZooKeeper的数据节点,在ZKDatabase的初始化阶段还会创建一个用于保存所有客户端会话超时时间的记录器:sessionsWithTimeouts——我们称之为“会话超时时间记录器”。

创建PlayBackListener监听器

        PlayBackListener监听器主要用来接收事务应用过程中的回调。在ZooKeeper数据恢复后期,会有一个事务订正的过程,在这个过程中,会回调PlayBackListener监听器来进行对应的数据订正。

处理快照文件

        完成内存数据库的初始化之后,ZooKeeper就可以开始从磁盘中恢复数据了。在上文中我们已经提到,每一个快照数据文件中都保存了ZooKeeper服务器近似全量的数据,因此首先从这些快照文件开始加载。

获取最新的100个快照文件

        一般在ZooKeeper服务器运行一段时间之后,磁盘上都会保留许多个快照文件。另外由于每次数据快照过程中,ZooKeeper都会将全量数据Dump到磁盘快照文件中,因此往往更新时间最晚的那个文件包含了最新的全量数据。那么是否我们只需要这个最新的快照文件就可以了呢?在ZooKeeper的实现中,会获取最新的至多100个快照文件(如果磁盘上仅存在不到100个快照文件,那么就获取所有这些快照文件)。

解析快照文件

        获取到这至多100个文件之后,ZooKeeper会开始“逐个”进行解析。每个快照文件都是内存数据序列化到磁盘的二进制文件,因此在这里需要对其进行反序列化,生成DataTree对象和sessionsWithTimeouts集合。同时在这个过程中,还会进行文件的checkSum检验以确定快照文件的正确性。

        需要注意的一点是,虽然在步骤5中获取到的是100个快照文件,但其实在这里的“逐个”解析过程中,如果正确性检验通过的话,那么通常只会解析最新的那个快照文件。换句话说,只有当最新的快照文件不可用的时候,才会逐个进行解析,直到将这100个文件全部解析完。如果将步骤4中获取的所有快照文件都解析完后还是无法成功恢复一个完整的DataTree和sessionsWithTimeouts,则认为无法从磁盘中加载数据,服务器启动失败。

获取最新的ZXID

        完成步骤6的操作之后,就已经基于快照文件构建了一个完整的DataTree实例和sessionWithTimeouts集合了。此时根据这个快照文件的文件名就可以解析出一个最新的ZXID:zxid_for_snap,该ZXID代表了ZooKeeper开始进行数据快照的时刻。

处理事务日志

        在经过前面7步流程的处理后,此时ZooKeeper服务器内存中已经有了一份近似全量的数据,现在开始就要通过事务日志来更新增量数据了。

获取所有zxid_for_snap之后提交的事务

        到这里,我们已经获取到了快照数据的最新ZXID。ZooKeeper中数据的快照机制决定了快照文件中并非包含了所有的事务操作。但是未被包含在快照文件中的那部分事务操作是可以通过数据订正来实现的。因此这里我们只需要从事务日志中获取所有ZXID比步骤7中得到的zxid_for_snap大的事务操作。

事务应用

        获取到所有ZXID大于zxid_for_snap的事务后,将其逐个应用到之前基于快照数据文件恢复出来的DataTree和sessionsWithTimeouts中去。

        在事务应用的过程中,还有一个细节需要我们注意,每当有一个事务被应用到内存数据库中去后,ZooKeeper同时会回调PlayBackListener监听器,将这一事务操作记录转换成Proposal,并保存到ZKDatabase.committedLog中,以便Follower进行快速同步。

获取最新ZXID

        待所有的事务都被完整的应用到内存数据库中之后,基本上也就完成了数据的初始化过程,此时再次获取一个ZXID,用来标识上次服务器正常运行时提交的最大事务ID。

校验epoch

        epoch是ZooKeeper中一个非常特别的变量,其字面意思是“纪元、时代”,在ZooKeeper中,epoch标识了当前Leader周期。每次选举产生一个新的Leader服务器之后,就会生成一个新的epoch。在运行期间集群中机器相互通信的过程中,都会带上这个epoch以确保彼此在同一个Leader周期内。

        在完成数据加载后,ZooKeeper会从步骤11中确定的ZXID中解析出事务处理的Leader周期:epochOfZxid。同时也会从磁盘的currentEpoch和acceptedEpoch文件中读取出上次记录的最新的epoch值,进行校验。

        通过以上流程的讲解,相信读者已经对ZooKeeper服务器启动器的数据初始化过程有了一个大体的认识,接下去将进一步从技术细节上展开,来对数据初始化过程做更深入的讲解。

PlayBackListener

        PlayBackListener是一个事务应用监听器,用于在事务应用过程中的回调:每当成功将一条事务日志应用到内存数据库中后,就会调用这个监听器。其接口定义非常简单,只有一个方法:

       ZooKeeper系统模型之数据初始化。

        用于对单挑事务进行处理。在完成步骤2 ZKDatabase的初始化后,ZooKeeper会立即创建一个PlayBackListener监听器,并将其置于FileTxnSnapLog中,在之后的步骤10事务应用过程中,会逐条回调该接口进行事务的二次处理。

        PlayBackListener会将这些刚刚被应用到内存数据库中的事务转存到ZKDatabase.committedLog中,以便集群中服务器进行快速的数据同步。