【ZooKeeper】数据初始化及同步

数据初始化

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

初始化流程

首先我们先从整体上来看 ZooKeeper 的数据初始化过程,图 7-48 展示了数据的初始化 流程。
在这里插入图片描述

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

  1. 初始化 FileTxnSnapLog 。

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

  2. 初始化 ZKDatabase 。

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

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

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

  3. 创建 PlayBackListener 监听器。

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

  4. 处理快照文件。

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

  5. 获取最新的 100 个快照文件。

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

  6. 解析快照文件

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

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

  7. 获取最新的 ZXID

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

  8. 处理事务日志

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

  9. 获取所有 zxid for snap 之后提交的事务。

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

  10. 事务应用

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

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

  11. 获取最新 ZXID 。

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

  12. 校验 epoch

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

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

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

PlayBackListener

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

void onTxnLoaded(TxnHeader hdr, Record rec);

用于对单条事务进行处理。在完成步骤 2 ZKDatabase 的初始化后, ZooKeeper 会立即 创建一个PlayBackListener 监听器,并将其置于 FileTxnSnapLog 中。在之后的步骤 10 事务应用过程中,会逐条回调该接口进行事务的二次处理。PlayBackListener 会将这些刚刚被应用到内存数据库中的事务转存到 ZKDatabase committedLog 中,以便集群中服务器间进行快速的数据同步。关于 ZooKeeper 服务 器之间的数据同步

数据同步

我们在讲解 ZooKeeper 集群服务器启动的过程中提到,整个集群完成 Leader 选举之后, Learner 会向 Leader 服务器进行注册。当 Learner 服务器向 Leader 完 成注册后,就进入数据同步环节。简单地讲,数据同步过程就是 Leader 服务器将那些没 有在 Learner 服务器上提交过的事务请求同步给 Learner 服务器
在这里插入图片描述

获取 Learner 状态

在注册 Learner 的最后阶段, Learner 服务器会发送给 Leader 服务器一个 ACKEPOCH 数 据包,Leader 会从这个数据包中解析出该 Learner 的 currentEpoch 和 lastZxid

数据同步初始化

在开始数据同步之前, Leader 服务器会进行数据同步初始化,首先会从 ZooKeeper 的内存数据库中提取出事务请求对应的提议缓存队列(下面我们用“提议缓存队列”来指代 该队列): proposals,同时完成对以下三个 ZXID 值的初始化。

  • peerLastZxid :该 Learner 服务器最后处理的 ZXID 。
  • minCommittedLog : Leader 服务器提议缓存队列 COmmittedLog 中的最小 ZX1D。
  • maxCommittedLog : Leader 服务器提议缓存队列 COmmittedLog 中的最大 ZXID 。

ZooKeeper 集群数据同步通常分为四类,分别是直接差异化同步 (DIFF 同步)、先回滚 再差异化同步 (TRUNC+DIFF 同步)、仅回滚同步 (TRUNC 同步)和全量同步 (SNAP 同步)。在初始化阶段, Leader 服务器会优先初始化以全量同步方式来同步数据 当 然,这并非最终的数据同步方式,在以下步骤中,会根据 Leader 和 Learner 服务器之间 的数据差异情况来决定最终的数据同步方式。

直接差异化同步 ( DIFF 同步)

场景: peerLastZxid 介于 minCommittedLog 和 maxCommittedLog 之间。

对于这种场景,就使用直接差异化同步 (DIFF 同步)方式即可。 Leader 服务器会首先 向这个Learner 发送一个 DIFF 指令,用于通知 Learner "进入差异化数据同步阶段, Leader 服务器即将把一些 Proposal 同步给自己”。在实际 Proposal 同步过程中,针对每个 Proposal, Leader 服务器都会通过发送两个数据包来完成,分别是 PROPOSAL 内容数据包和 COMMIT 指令数据包 这和
ZooKeeper 运行时 Leader 和 Follower 之间的事务请求的 提交过程是一致的。

举个例子来说,假如某个时刻 Leader 服务器的提议缓存队列对应的 ZXID 依次是:
0x500000001, 0x500000002, 0x500000003, 0x500000004, 0x500000005
而 Learner 服务器最后处理的 ZXID 为 0x500000003, 于是 Leader 服务器就会依次将 0x500000004和 0x500000005 两个提议同步给 Learner 服务器,同步过程中的数据包发 送顺序如表所示。
在这里插入图片描述

通过以上四个数据包的发送, Learner 服务器就可以接收到自己和 Leader 服务器的所有差异数据。 Leader 服务器在发送完差异数据之后,就会将该 Learner 加入到 forwarding Followers 或 observing Learners 队列中,这两个队列在 ZooKeeper 运行期间的事务请求处理过程中都会使用到。随后 Leader 还会立即发送一个 NEWLEADER 指令,用于通知 Learner, 已经将提议缓存队列中的 Proposal 都同步给自 己了。

下面我们再来看 Learner 对 Leader 发送过来的数据包的处理。根据上面讲解的 Leader 服务器的数据包发送顺序, Learner 会首先接收到一个 DIFF 指令,于是便确定了接下来 进入 DIFF 同步阶段。然后依次收到表中的四个数据包, Learner 会依次将其应用 到内存数据库中。紧接着,Learner 还会接收到来自 Leader 的 NEWLEADER 指令,此 时 Learner 就会反馈给 Leader 一个 ACK消息,表明自己也确实完成了对提议缓存队列 中 Proposal 的同步。

Leader 在接收到来自 Learner 的这个 ACK 消息以后,就认为当前 Learner 已经完成了数 据同步,同时进入“过半策略”等待阶段—— Leader 会和其他 Learner 服务器进行上述同样的数据同步流程,直到集群中有过半的 Learner 机器响应 Leader 这个 ACK 消息。 一旦满足“过半策略”后,Leader 服务器就会向所有已经完成数据同步的 Learner 发送 一个 UPTODATE 指令,用来通知Learner 已经完成了数据同步,同时集群中已经有过半 机器完成了数据同步,集群已经具备了对外服务的能力了。

Learner 在接收到这个来自 Leader 的 UPTODATE 指令后,会终止数据同步流程,然后向 Leader再次反馈一个 ACK 消息。

整个直接差异化同步过程中涉及的 Leader 和 Learner 之间的数据包通信如图所示。
在这里插入图片描述

先回滚再差异化同步(TRUNC+DIFF 同步)

场景:针对上面的场景,我们已经介绍了直接差异化同步的详细过程。但是在这种场景 中,会有一个罕见但是确实存在的特殊场景:设有 A, B 、 C 三台机器,假如某一时刻 B 是 Leader 服务器,此时的 Leader_Epoch 为 5, 同时当前已经被集群中绝大部分机器都 提交的 ZXID 包括:0x500000001 和 0x500000002 此时, Leader 正要处理 ZXID: 0x500000003, 并且已经将该事务写入到了 Leader 本地的事务日志中去——就在 Leader 恰好要将该 Proposal 发送给其他 Follower机器进行投票的时候, Leader 服务器挂了, Proposal 没有被同步出去。此时 ZooKeeper 集群会进行新一轮的 Leader 选举,假设此次 选举产生的新的 Leader 是 A, 同时 Leader_Epoch 变更为 6,之后 A 和 C 两台服务器继 续对外进行服务,又提交了 0X600000001 和 0x600000002 两个事务。此时,服务器 B 再 次启动,并开始数据同步。

简单地讲,上面这个场景就是 Leader 服务器在已经将事务记录到了本地事务日志中,但 是没有成功发起 Proposal 流程的时候就挂了。在这个特殊场景中,我们看到 ,peerLastZxid、minCommittedLog 和 maxCommittedLog 的值分别是 0x500000003 、 0x500000001 和 0x600000002, 显然, peerLastZxid 介于 minCommittedLog 和 maxCommittedLog 之间。

对于这个特殊场景,就使用先回滚再差异化同步 (TRUNC+DIFF 同步)的方式。当 Leader 服务器发现某个 Learner 包含了一条自己没有的事务记录,那么就需要让该 Learner 进行 事务回滚 —— 回滚到 Leader 服务器上存在的,同时也是最接近于 peerLastZxid 的 ZXID 。

在上面这个例子中, Leader 会需要 Learner 回滚到 ZXID 为 0x500000002 的事务记录。

先回滚再差异化同步的数据同步方式在具体实现上和差异化同步是一样的,都是会将差 异化的Proposal 发送给 Learner 。同步过程中的数据包发送顺序如表所示。
在这里插入图片描述

仅回滚同步 ( TRUNC 同步)

场景: peerLastZxid 大于 maxCommittedLog

这种场景其实就是上述先回滚再差异化同步的简化模式, Leader 会要求 Learner 回滚到 ZXID 值为 maxCommitedLog 对应的事务操作,这里不再对该过程详细展开讲解。

全量同步 ( SNAP 同步)

场景 1: peerLastZxid 小于 minCommittedLog
场景 2: Leader 服务器上没有提议缓存队列, peerLastZxid 不等于 lastProcessedZxid (Leader 服务器数据恢复后得到的最大 ZXID) 。

上述这两个场景非常类似,在这两种场景下, Leader 服务器都无法直接使用提议缓存队 列和Learner 进行数据同步,因此只能进行全量同步 (SNAP 同步)。

所谓全量同步就是 Leader 服务器将本机上的全量内存数据都同步给 Learner 。 Leader 服 务器首先向 Learner 发送一个 SNAP 指令,通知 Learner 即将进行全量数据同步。随后, Leader 会从内存数据库中获取到全量的数据节点和会话超时时间记录器,将它们序列化后传输给 Learner ,Learner服务器接收到该全量数据后,会对其反序列化后载入到内存 数据库中。

以上就是 ZooKeeper 集群间机器的数据同步流程了。整个数据同步流程的代码实现主要 在LearnerHandler 和 Learner 两个类中,读者可以自行进行更为深入、详细的了 解。

相关推荐
©️2020 CSDN 皮肤主题: 撸撸猫 设计师:马嘣嘣 返回首页