文章目录
服务端启动
单机版启动大致可以分为如下步骤:
- 配置文件解析
- 初始化数据管理器
- 初始化网络IO管理器
- 数据恢复
- 对外服务
核心类介绍:
- QuorumPeerMain:启动入口类。
- DataDirCleanuoManager:历史文件清理器,包括事务日志和快照数据文件。
- ZooKeeperServer:单机版 ZooKeeper 最为核心的实体类。
- FileTxnSnapLog:上层服务器和底层数据存储之间的对接层,提供一系列操作数据文件的接口。
- ServerCnxnFactory:创建 ServerCnxn 的工厂类,可以选择 NIO 实现或 Netty 实现。
- ServerStats:服务器运行时记录一些统计信息。
集群版启动和单机版相比,主要是多了 Leader 选举和 Leader&Follower 交互过程。
集群版相比单机版主要是多了 QuorumPeer 类,它是服务器实例的托管者,代表集群中的一台机器。在运行期间 QuorumPeer 会不断检测当前服务器实例的运行状态,同时根据情况发起 Leader 选举。
Leader 选举
ZooKeeper 集群有三种服务器角色:Leader、Follower、Observer。Leader 选举是 ZooKeeper 中最重要的技术之一,也是保证分布式数据一致性的关键所在。
Leader 选举算法
在 ZooKeeper 里曾经有三种选举算法,分别是 LeaderElection、UDP 版本的 FastLeaderElection 和 TCP 版本的 FastLeaderElection。从 3.4.0 开始只保留了 TCP 版本的 FastLeaderElection,下面只讲此算法。
讲算法之前我们先简单介绍些相关的专业术语:
- SID:一个数字,用来唯一标识一台服务器,值和 myid 一致。
- ZXID:事务 ID,用来唯一标识一次服务器状态的变更。在某一时刻,集群中每台机器的 ZXID 不一定全都一致。
- Vote:投票,Leader 选举顾名思义,必须通过投票来实现。当集群中的机器发现自己无法检测到 Leader 机器的时候,就会尝试进行投票。
- Quorum:过半机器数,如果一台服务器要称为 Leader,必须获得至少 n/2 + 1 的投票。
当 ZooKeeper 集群中的一台机器出现以下两种情况之一时,就会进入 Leader 选举:
- 服务器初始化启动。
- 服务器运行期间无法和 Leader 保持连接。
而当一台机器进行选举状态时,集群也可能处于两种状态:
- 集群中已存在 Leader。
- 集群中确实不存在 Leader。
第一种情况比较简单,当集群中某一台机器启动比较晚,在它启动时,集群中已经有了 Leader。当该机器试图选举 Leader 时,就会被告知当前的 Leader 信息。下面重点看集群中不存在 Leader 的情况。
在服务器集群初始化阶段,当只有一台服务器(假设 myid 为 1)启动的时候,它是无法进行 Leader 选举的。当第二台机器(假设 myid 为 2)启动后,此时两台机器可以互相通信,进入 Leader 选举流程。
- 每个 server 会发出一个投票。初始情况下,每个 server 都会将自己作为 leader 服务器来进行投票。每次投票的基本元素是:所推举的服务器的 SID 和 ZXID,以 (SID, ZXID) 来表示。
- 接收来自各个服务器的投票:接收到投票时,首先要判断该投票的有效性,包括检查是否是本轮投票、是否来自 Looking 状态的服务器。
- 处理投票:将别人的投票和自己的投票进行 PK,优先选择 ZXID 较大的,ZXID 相同的就选 SID 大的。
- 统计投票:如果某台机器得到了过半的选票,即认为已经选出了 Leader。
- 改变服务器状态:一旦确定了 Leader,服务器就会更新自己的状态。如果是 Follower,就变更为 FOLLOWING。如果是 Leader,就变更为 LEADING。
上面讲的是集群初始化阶段的 Leader 选举,在运行过程中,如果 Leader 突然挂了,非 Observer 服务器都会将自己的服务器状态变为 LOOKING,进入 Leader 选举流程。其选举过程和初始化阶段的 Leader 选举类似。
简单的说,通常哪台服务器上的数据越新,那么越有可能成为 Leader,因为数据越新,ZXID 越大,也就越能保证数据的恢复。当集群中有几台机器的 ZXID 一致时,SID 较大的服务器成为 Leader。
Leader 选举的实现细节
ZooKeeper 集群中服务器有 4 种状态,分别是:LOOKING、LEADING、FOLLOWING、OBSERVING。LOOKING 状态表示处于选举过程中,后三种都是选举结束后的不同角色对应的状态。
在 ZooKeeper 里投票信息 Vote 数据结构如下所示:
final private long id; // sid
final private long zxid; // 事务 id
final private long electionEpoch; // 逻辑时钟,用于判断多个投票是否在同一轮选举周期中。每进入新一轮投票,都会对该值进行加 1 操作。
final private long peerEpoch; // 被推举的 Leader 的 epoch
final private ServerState state; // 当前服务器状态
ClientCnxn 是 ZooKeeper 客户端用于处理网络 I/O 的管理器,在 Leader 选举过程中也有类似角色,就是 QuorumCnxManager —— 负责服务器之间 Leader 选举过程中的网络通信。
在 QuorumCnxManager 内部维护了几个消息队列,用于保存接收到的、待发送的消息,以及消息的发送器。除了接收队列之外,其他像发送队列、发送器等都是为每个 SID 创建了一份的。
- recvQueue:消息接收队列,用于存放那些从其他服务器接收到的消息。
- queueSendMap:消息发送队列,保存待发送的消息。
- senderWorkerMap:发送器集合,每个 SendWorker 消息发送器都对应一台远程 ZooKeeper 服务器,负责消息发送。
- lastMessageSent:最近发送过的消息,在这个集合中,为每个 SID 保留最近发送过的消息。
为了能够互相投票,集群中所有机器都需要两两建立起网络连接。QuorumCnxManager 在启动的时候,会创建一个 ServerSocket 来监听 Leader 选举的通信端口(默认端口是 3888)。为避免重复建立连接,ZooKeeper 只运行 SID 较大的服务主动和其他服务器建立连接。一旦建立了连接,就会根据 SID 来创建相应的消息发送器 SendWorker 和消息接收器 RecvWorker。
ZooKeeper 中的选票管理模块 FastLeaderElection 包含了选票发送队列 sendqueue、选票接收队列 recvqueue、选票接收器 WorkerReceiver、选票发送器 WorkerSender,它与 QuorumCnxManager 的关系如下图所示:
FastLeaderElection 里最核心的选举算法流程如下所示:
- 自增选举轮次:FastLeaderElection 里有个 logicalclock 属性用于标识当前 Leader 的选举轮次,ZooKeeper 规定所有有效的投票都必须在同一轮次中。
- 初始化选票:第一轮投票都选自己为 Leader。
- 发送初始化选票。
- 接收外部投票。
- 判断选举轮次
- 如果外部投票选举轮次大于内部投票,则更新自己的选举轮次,清空收到的投票,然后使用初始化的投票来进行 PK 以确定是否变更内部投票,最终再将内部投票发出去。
- 如果外部投票选举轮次小于内部投票,则忽略之。
- 开始下一步的选票 PK。
- 选票 PK:优先选 ZXID 大的,ZXID 一致则选 SID 大的。
- 变更投票:选票 PK 之后,如果确定外部投票优于内部投票,就进行投票变更,然后再将变更后的内部投票发送出去。
- 选票归档:无论是否进行了投票变更,都要将刚刚的外部投票放入选票集合“recvset”中进行归档。
- 统计投票:统计投票是为了确认是否已经有过半机器认可了当前的内部投票。如果有,就终止投票,否则返回步骤 4。
- 更新服务器状态:终止投票后,Leader 将自己状态变为 LEADING,其他的服务器将自己状态变为 FOLLOWING 或 OBSERVING。
各服务器角色介绍
前面已经了解到 ZooKeeper 集群有三种服务器角色:Leader、Follower、Observer。这一节将深入介绍这三种服务器角色。
Leader
Leader 是整个 ZooKeeper 集群工作机制中的核心,其主要工作有以下两个:
- 事务请求的唯一调度和处理者,保证集群事务处理的顺序性。
- 集群内部各服务器的调度者。
ZooKeeper 里使用责任链模式来处理每一个客户端请求,Leader 服务器的请求处理链如下图所示:
从上图可以看到,一共有 7 个请求处理器构成了 Leader 服务器的请求处理链:
- PrepRequestProcessor:Leader 服务的请求预处理器,识别客户端请求是否是事务请求(创建节点、更新数据、删除节点、创建会话等),如果是则对其进行一系列预处理,比如创建请求事务头、事务体、会话检查、ACL 检查和版本检查。
- ProposalRequestProcessor:对于非事务请求,会直接转发给 CommitProcessor;对于事务请求,除了交给 CommitProcessor 处理器之外,还会根据请求类型创建对应的 Proposal 提议,并发给所有的 Follower 服务器来发起一次集群内的事务投票。同时,还会讲事务请求交给 SyncRequestProcessor 进行事务日志的记录。
- SyncRequestProcessor:是事务日志记录处理器,主要用来将事务请求记录到事务日志文件中,同时还会触发 ZooKeeper 进行数据快照。
- AckRequestProcessor:Leader 特有的处理器,主要负责在 SyncRequestProcessor 处理器完成事务日志记录后,向 Proposal 的投票收集器发送 ACK 反馈,以通知投票收集器当前服务器已经完成了对该 Proposal 的事务日志记录。
- CommitProcessor:事务提交处理器。对于非事务请求会直接转发给下一个处理器;而对于事务请求,会等待集群内针对该 Proposal 的投票直到该 Proposal 可以被提交。
- ToBeCommitProcessor:其内部有一个 toBeApplied 队列,专门用来存储那些被 CommitProcessor 处理过的可被提交的 Proposal。该处理器会将请求交给 FinalRequestProcessor 处理,等其处理完后才回从 toBeApplied 队列中移除。
- FinalRequestProcessor:进行客户端请求返回之前的收尾工作,包括创建客户端请求的响应;针对事务请求将事务应用到内存数据库中。
为了保持集群内部的实时通信,同时也是为了确保可以控制所有的 Follower/Observer 服务器,Leader 服务器会与每一个 Follower/Observer 建立长连接,并为每个 Follower/Observer 服务器创建一个名为 LearnerHandler 的实体。LearnerHandler 主要负责服务器之间的网络通信,包括数据同步、请求转发、Proposal 提议的投票等。
Follower
Follower 服务器是 ZooKeeper 集群状态的跟随者,其主要工作有以下三个:
- 处理客户端非事务请求,转发事务请求给 Leader 服务器。
- 参与事务请求 Proposal 的投票。
- 参与 Leader 选举投票。
Follower 也同样使用了责任链模式组装的请求处理链来处理每一个客户端请求,由于不需要负责事务请求的投票处理,其处理链要简单一些。其第一个处理器换成了 FollowerRequestProcessor,也没有了 ProposalRequestProcessor。
- FollowerRequestProcessor:对于事务请求,直接转发给 Leader 服务器。
- SendAckRequestProcessor:负责事务日志记录反馈,在完成事务日志记录后,会向 Leader 服务器发送 ACK 消息以表明自身完成了事务日志记录的工作。在 Leader 服务器上也有一个类似的 AckRequestProcessor,区别是一个是本地操作,一个是要以服务器通信的方式来反馈。
Observer
Observer 是自 3.3.0 版本开始引入的一个全新的角色,该类型服务器充当了一个观察者的角色 —— 观察集群最新状态并同步过来。Observer 和 Follower 的区别是它不参与任何形式的投票,包括 Leader 选举投票和事务请求 Proposal 的投票。
Observer 的请求处理链和 Follower 基本一样,虽然也有 SyncRequestProcessor,但是 Leader 并不会将事务请求的投票发给 Observer 服务器。
集群间消息通信
在 ZooKeeper 集群工作过程中,都是由 Leader 服务器来负责各服务器之间的协调,同时各服务器之间的网络通信,都是通过不同类型的消息床底来实现的。ZooKeeper 的消息类型大体分为四类:数据同步型、服务器初始化型、请求处理型、会话管理型。
数据同步型
数据同步消息是 Learner 和 Leader 服务器进行数据同步时的消息,通常有四种:
- DIFF:Leader 通知 Learner 服务器即将进行 DIFF 方式的数据同步。
- TRUNC:触发 Learner 服务器进行内存数据库的回滚操作。
- SNAP:Leader 通知 Learner 服务器即将进行全量方式的数据同步。
- UPTODATE:告知 Learner 服务器已经完成了数据同步,可以对外提供服务了。
服务器初始化型
服务器初始化消息是整个机器或部分新机器初始化时,Leader 和 Learner 之间相互通信所使用的消息类型,常见的有:
- OBSERVERINFO:Observer 服务器在启动时发送给 Leader 服务器的,用于向 Leader 注册自己,消息中包含了自身的 SID 和 ZXID。
- FOLLOWERINFO:同理,是 Follower 向 Leader 注册自己的消息。
- LEADERINFO:Leader 在接到 Learner 的注册消息后,也会将自己的基本信息发送给 Learner,通常包含了当前 Leader 服务器的最新 EPOCH 值。
- ACKEPOCH:Learner 在接收到 Leader 发的 LEADERINFO 消息后,会把自己最新的 ZXID 和 EPOCH 以 ACPEPOCH 消息的形式发给 Leader。
- NEWLEADER:当有足够多的 Follower 服务器连上 Leader 或完成数据同步后,Leader 会向 Learner 发送 NEWLEADER 消息,内容是 Leader 最新的 ZXID。
请求处理型
请求处理型消息是指在进行请求处理过程中,Leader 和 Learner 服务器之间互相通信所使用的消息,常见的有:
- REQUEST:当 Learner 接到客户端的事务请求后,会以 REQUEST 消息形式发给 Leader 服务器。
- PROPOSAL:在处理事务请求时,Leader 会将事务请求以 PROPOSAL 消息的形式创建投票发送给集群中的所有的 Follower 服务器来进行事务日志的记录。
- ACK:Follower 在接到 Leader 的 ROPOSAL 消息后,会进行事务日志的记录,然后以 ACK 的形式反馈给 Leader。
- COMMIT:用于通知所有 Follower 服务器可以进行事务请求的提交了。
- INFORM:Leader 向 Observer 通知刚提交的事务请求。
- SYNC:通知 Learner 服务器已经完成了 Sync 操作。
会话管理型
会话管理型消息是指 ZooKeeper 在进行会话管理的过程中,和 Learner 服务器之间互相通信所使用的消息,常见的有 PING 和 REVALIDATE 两种:
- PING:用于 Leader 同步 Learner 服务器上的客户端心跳检测,用于激活存活的客户端。
- REVALIDATE:用于 Learner 校验客户端会话是否有效,同时也会激活会话。通常发送在客户端重连过程中,新服务器向 Leader 发送 REVALIDATE 消息以确定该会话是否已经超时。
请求处理
针对客户端的一次请求,ZooKeeper 究竟是如何进行处理的呢?下面分别从会话创建、SetData 请求(事务)、事务请求转发、GetData 请求(非事务)的角度进行讲解。
会话创建请求
ZooKeeper 服务端对于会话创建的处理可以分为六个步骤:请求接收、会话创建、预处理、事务处理、事务应用和会话响应,大体流程图如下所示:
请求接收
- I/O 层接收来自客户端的请求:NIOServerCnxn 实例负责维护客户端和服务端之间的通信,统一接收来自客户端的所有请求,并将请求内容从底层网络 I/O 中完整地读取处理。
- 判断是否是客户端“会话创建请求”:每个会话对应一个 NIOServerCnxn 实体,如果会话还没有对应的 NIOServerCnxn 实体,需要创建并初始化一个。
- 反序列化 ConnectRequest 请求。
- 判断是否是 ReadOnly 客户端:如果 ZooKeeper 服务器是以 ReadOnly 模式启动的,那么所有来自非 ReadOnly 型客户端的请求将无法被处理。-- 啥是 ReadOnly 客户端???怎么标记 ReadOnly ?
- 检查客户端 ZXID:如果客户端 ZXID 大于服务端 ZXID,那么服务端将不接受该客户端的会话创建请求。
- 协商 sessionTimeout。
- 判断是否需要重新创建会话:如果客户端请求中包含了 sessionID,那就说明该客户端正在进行会话重连,服务端只需要重新打开这个会话即可,否则需要重新创建。
会话创建
- 为客户端生成 sessionID:sessionID 需要保证全局唯一。
- 注册会话:主要是向 SessionTracker 中注册会话,方便后续的会话管理。SessionTracker 中维护了两个数据结构:sessionsWithTimeout、sessionsById。前者根据 sessionID 保存了所有会话的超时时间,后者根据 sessionID 保持了所有会话实体。
- 激活会话:激活会话涉及 ZooKeeper 管理的分桶策略,目的是为会话安排一个区块,以便会话清理程序能够快速高效地进行会话清理。
- 生成会话密码:会话密码是会话在集群中不同机器之间转移的凭证。
预处理
- 将请求交给 PrepRequestProcessor 处理。
- 创建请求事务头:包含标识客户端的唯一标识 clientID、客户端操作序列化 CXID、事务 ZXID、服务器开始处理该请求的时间 time、事务请求类型(create、delete、setData、createSession)。
- 创建请求事务体:会话创建请求对应的是 CreateSessionTxn。
- 注册与激活会话:这一步做的事和第 9 步一样,不同的是这一步是 Leader 服务器处理由非 Leader 服务器转发过来的请求,也就是说 Leader 之前还没在 SessionTracker 注册的情况。
事务处理
- 将请求交给 ProposalRequestProcessor 处理器,事务处理包含三个子流程:Sync 流程、Proposal 流程和 Commit 流程。
Sync 流程
Sync 流程的核心就是使用 SyncRequestProcessor 记录事务日志的过程,该过程存在于 Leader 和 Follower 服务器上。
Proposal 流程
每一个事务请求都需要集群中过半机器投票认可才能被真正应用到 ZooKeeper 的内存数据库中,这个投票与统计的过程“Proposal 流程”。
- 发起投票前的检查:检查当前服务器的 ZXID 是否可用,如果不可用就抛出 XidRolloverException。但是怎么样才算可用呢???
- 生成提议 Proposal:将之前创建的请求头和事务体,以及 ZXID 和请求本身序列化到 Proposal 对象中。
- 广播提议:以 ZXID 作为标识,将该提议放入投票箱 outstandingProposals,同时广播给所有的 Follower 服务器。
- 收集投票:Follower 服务器在接收到 Leader 发来的提议后,会进入 Sync 流程来进行事务日志的记录,一旦日志记录完成后,就会发送 ACK 消息给 Leader 服务器。当 Leader 收集到过半投票,就可以进入 Commit 阶段了。
- 将请求放入 toBeApplied 队列。
- 广播 Commit 消息:Leader 给 Follower 和 Observer 发的 Commit 消息是不一样的,由于 Observer 之前并没有参与 Proposal 投票,因此 Leader 会向其发送 INFORM 请求,其中包含了当前提议的内容,而给 Follower 发的消息只有 ZXID。
Commit 流程
- 将请求交给 CommitProcessor 处理器:CommitProcessor 不会立即处理,而是先放入 queuedRequests 队列中。
- 处理 queuedRequests 队列请求:会有单独的线程处理。
- 标记 nextPending:如果从 queuedRequests 队列中取出的是事务请求,则将 nextPending 标记位当前请求。标记的作用一方面是为了确保事务请求的顺序性,另一方面也是便于 CommitProcessor 检测当前请求中是否正在进行事务请求的投票。
- 等待 Proposal 投票,投票通过后,会把请求放入 commitedRequests 队列中,同时唤醒 Commit 流程。
- 提交请求:一旦发现 commitedRequests 队列中有数据了,Commit 流程就会开始提交请求。当然提交之前还要对比是否是当前标记的 nextPending 请求,一致则将请求放入 toProcess 队列,交付给下一个请求处理器:FinalRequestProcessor。
事务应用
- FinalRequestProcessor 处理:检查 outstandingChanges 队列中请求的有效性,如果发现这些请求已经落后于当前正在处理的请求,那么直接从队列中移除。
- 事务应用:将事务变更应用到内存数据库。
- 将事务请求放入队列 commitProposal:用于集群间机器进行数据的快速同步。
会话响应
- 统计处理:计数服务端处理花费的时间,统计最新的 ZXID、lastOp(最后一次和服务端的操作)等。
- 创建响应 ConnectResponse:包含了通信协议版本号、会话超时时间、sessionID 和会话密码。
- 序列化 ConnectResponse。
- I/O 层发送响应给客户端。
SetData 请求
服务端对于 SetData 请求的处理大致可以分为四个步骤:请求的预处理、事务处理、事务应用和请求响应。整个事务请求的处理和上一节“会话创建”非常相似,尤其是投票部分,本节不再讲解重复的部分。
预处理
- I/O 层接收来自客户端的请求,判断是否“会话创建”请求,是则按照上一节的流程执行。
- 将请求交给 ZooKeeper 的 PrepRequestProcessor 进行处理。
- 创建请求事务头。
- 会话检查:检查会话是否超时,超时则抛出 SessionExpiredException。
- 反序列化 SetDataRequest 请求对象,并创建 ChangeRecord 记录放入 outstandingChanges 队列中。
- ACL 检查。
- 数据版本检查。
- 创建事务请求体 SetDataTxn。
- 保存事务操作到 outstandingChanges 队列中。
事务处理
无论是会话创建请求还是 setData 请求,事务处理流程都是一致的,都是由 ProposalRequestProcessor 处理器发起,通过 Sync、Proposal、Commit 三个子流程相互协作完成任务。
事务应用
- 交给 FinalRequestProcessor 处理器。
- 事务应用:将请求事务头和事务体交给内存数据库 ZKDatabase 进行事务应用,同时返回 ProcessTxnResult 对象,包含数据节点更新后的 stat。
- 将事务请求放入队列 commitProposal。
请求响应
最后,创建响应体 SetDataResponse,创建响应头、序列化响应并且 I/O 层发给客户端。
public class SetDataResponse implements Record {
private org.apache.zookeeper.data.Stat stat;
}
事务请求转发
为保证事务请求被顺序执行,从而确保 ZooKeeper 集群的顺序一致性,所有事务请求必须由 Leader 服务器来处理。但是,并不是所有的 ZooKeeper 客户端都和 Leader 服务器保持连接,那么如何保证所有的事务请求都由 Leader 来处理呢?
所有非 Leader 服务器接收到了来自客户端的事务请求,那么必须将其转发给 Leader 服务器来处理。
在 Follower 或 Observer 服务器中,第一个请求处理器分别是 FollowerRequestProcessor 或 ObserverRequestProcessor,无论是哪个处理器,都会检查当前请求是否是事务请求,如果是则将该客户端请求以 REQUEST 消息的形式转发给 Leader 服务器。
GetData 请求
GetData 请求是非事务请求,相比事务请求要简单的多,大体分为三个步骤:请求的预处理、非事务处理和请求响应。
数据与存储
前面讲了 ZooKeeper 服务端的工作原理,下面来看看底层数据与存储的计数内幕。数据存储分为两个部分:内存数据存储、磁盘数据存储。
内存数据
ZooKeeper 的数据模型是一棵树,这棵树在内存里是如何存储的呢?DataTree 是 ZooKeeper 中内存数据存储的核心,是一个相对独立的组件,不直接与网络、客户端请求交互,其结构如下所示:
DataTree 用于存储所有 ZooKeeper 节点路径、数据内容、ACL 信息等,底层数据结构是一个 ConcurrentHashMap,key 是节点路径,Value 是节点的数据内容 DataNode。DataNode 是数据存储的最小单元,其中除了保存节点内容 data[]、ACL 列表、节点状态 Stat 之外,还记录了父节点的引用和子节点列表,还提供了 addChild、removeChild、getChildren 方法对子节点列表进行操作。
为方便实时访问和及时清理,DataTree 将普通节点和临时节点分开存储,分别对应 nodes 属性和 ephemerals属性。
事务日志
这一节将从事务日志的存储、日志格式和日志写入过程几个方面,来讲解 ZooKeeper 底层实现数据一致性过程中最重要的一部分。
事务日志存储的目录可以通过配置文件里的参数 dataDir 或 dataLogDir 来指定,ZooKeeper 运行一段时间后可以在事务日志目录下看到类似这样的文件:
这些文件名的后缀是文件里第一条事务记录的 ZXID,使用 ZXID 作为后缀可以帮助我们迅速定位到某一事务操作所在的事务日志。另外这些文件都是 64 MB 大小的,这是为啥呢?ZooKeeper 在写事务日志文件时会做空间预分配,也就是一开始就创建一个 64 MB 的文件,这样做当然是为了提升磁盘写入性能。对于文件的不断追加写入会触发底层磁盘 I/O 为文件开辟新的磁盘块,即磁盘 Seek。提前预分配可以减少磁盘 Seek 的频率,提高磁盘 I/O 效率。
事务日志里主要记录了一次事务操作的时间、客户端会话 ID、CXID、ZXID、操作类型、会话超时时间、操作类型、节点路径、节点数据内容、ACL 信息、版本号等。
日志截断
在 ZooKeeper 运行过程中,可能会出现非 Leader 机器上记录的事务 ID 比 Leader 服务器大,无论如何这都是一个非法的状态。ZooKeeper 遵循一个原则:所有机器都必须与 Leader 的数据保持同步。因此,一旦某机器遇到上述情况,Leader 会发送 TRUNC 命令给这个机器,要求其进行日志截断。
数据快照
数据快照是用来记录 ZooKeeper 服务器上某一时刻的全量内存数据内容的。和事务日志类似,ZooKeeper 的数据快照也存在了磁盘上指定目录,也可以通过 dataDir 属性进行配置。在该目录下可以看到像下面这样的快照文件:
和事务日志文件的命名规则一致,快照数据文件也使用 ZXID 的十六进制表示作为文件名后缀。和事务日志文件不同的是,快照数据文件大小不都一样,也没有采用“预分配”机制。每个快照数据文件的所有内容都是有效的,因此该文件的大小在一定程度上能够反映当前 ZooKeeper 内存中的全量数据的大小。
在 ZooKeeper 里,FileSnap 类负责维护快照数据对外的接口,包括快照数据的写入和读取等。将内存数据写入到快照数据文件中其实是一个序列化的过程,那么 ZooKeeper 何时写快照数据文件呢?在 ZooKeeper 里,可以使用 snapCount 参数来配置每次数据快照之间的事务操作次数,即 ZooKeeper 会在 snapCount 次事务日志记录后进行一次数据快照。下面重点讲一下数据快照的过程:
- 判断是否需要进行数据快照:数据快照对系统性能是由一定影响的,为避免所有机器同时进行数据快照影响集群性能,ZooKeeper 采取过半随机策略来决定是否进行数据快照,即当上次快照之后记录的事务日志 logCount > (snapCount/2 + randRoll) 时,才进行数据快照。其中 randRoll 是 1 ~ snapCount/2 之间的随机数。
- 切换事务日志文件:写数据快照之前,首先要进行事务日志文件的切换。
- 创建数据快照异步线程。
- 获取全量数据和会话信息:数据快照的本质就是将内存中所有数据节点信息(DataTree)和会话信息保存到本地磁盘中去。
- 生成快照数据文件名。
- 数据序列化。
初始化
在 ZooKeeper 服务器启动期间,首先会进行数据初始化工作,将磁盘上存储的数据文件加载到服务器内存中。ZooKeeper 数据初始化流程如下图所示:
数据的初始化包括加载快照数据和根据事务日志进行数据订正两个过程,详细说明:
- 初始化 FileTxnSnapLog 是 ZooKeeper 事务日志和快照数据访问层,用于衔接上层业务和底层数据存储。底层分为快照和事务日志两个部分,因此 FileTxnSnapLog 内部又分为 FileTxnLog 和 SnapLog 的初始化。
- 初始化 ZKDataBase,其中包含了一个 DataTree 属性。
- 创建 PlayBackListener 监听器:该监听器用于接收事务应用过程中的回调。在数据恢复后期,会有一个事务订正的过程,在这个过程中会回调 PlayBackListener 来进行对应的数据订正。
- 加载快照文件中的数据。
- ZooKeeper 默认会读取 100 个快照文件,从中选出最新的可用快照文件,进行加载。怎样算可用呢?主要是根据文件的 checkSum 校验文件的正确性。
- 获取快照文件中最新的 ZXID:zxid_for_snap。
- 处理事务日志。
- 快照文件相当于数据的全量备份,事务日志相当于增量备份。
- 获取 ZXID 比 zxid_for_snap 大的事务操作日志,逐个应用到内存数据库中。
- 除了应该到内存数据库,ZooKeeper 还会回调 PlayBackListener,将这一事务操作记录转化成 Proposal,并保存到 ZKDataBase 的 commitLog 中,以便 Follower 进行快速数据同步。
- 获取最新 ZXID,也就是上次服务器运行时提交的最大 ZXID。
- 校验 epoch:校验 ZXID 里的 epoch 和磁盘文件 currentEpoch、acceptedEpoch 值。
数据同步
Learner 服务器向 Leader 注册完毕后,会进入数据同步环节。数据同步过程就是 Leader 服务器将那些没有在 Learner 服务器上提交过的事务请求同步给 Learner 服务器。数据同步大体流程如下所示:
在注册 Learner 的最后阶段,Learner 服务器会发送给 Leader 服务器一个 ACKEPOCH 数据包,Leader 会从这个数据包中解析出该 Learner 的 currentEpoch 和 lastZXID。
在开始同步之前,ZooKeeper 会进行数据初始化,完成下面三个 ZXID 的初始化:
- peerLastZxid:该 Learner 服务器最后处理的 ZXID。
- minCommitLog:Leader 服务器的提议缓存队列 commitLog 中的最小 ZXID。
- maxCommitLog:Leader 服务器的提议缓存队列 commitLog 中的最大 ZXID。
ZooKeeper 集群数据同步通常分为四类:直接差异化同步(DIFF 同步)、先回滚再差异化同步(TRUNC + DIFF 同步)、仅回滚同步(TRUNC 同步)、全量同步(SNAP 同步),具体使用哪一类和上面三个 ZXID 之间的关系有关。
- 直接差异化同步(DIFF 同步):适合 peerLastZxid 介于 minCommitLog 和 maxCommitLog 之间的情况。
- 假设 Leader 的 commitLog 里的 ZXID 依次是 0x500000001、0x500000002、0x500000003、0x500000004、0x500000005,而 Learner 的 peerLastZxid 是 0x500000003。
- 那么 Leader 会把后两个提议同步给 Learner,同步方法和正常的事务提议、提交类似,也是按顺序发 PROPOSAL、COMMIT 消息。
- 数据同步完毕后,会再发一个 NEWLEADER 指令,用于告知 Learner 已经将提议缓存队列中的 Proposal 都同步完成了。
- Learner 在收到 NEWLEADER 指令后,会反馈一个 ACK 指令,表示自己也完成了数据同步。
- Leader 收到过半表示的数据同步完成的 ACK 消息后,会给已经同步完成的 Learner 发送 UPTODATE 消息,表示集群中过半机器完成了数据同步,可以对外提供服务了。
- 仅回滚(TRUNC 同步):适合 peerLastZxid 大于 maxCommitLog 的情况。
- 先回滚再差异化同步(TRUNC + DIFF 同步):适合 peerLastZxid 介于 minCommitLog 和 maxCommitLog 之间,但是 commitLog 队列里并没有 peerLastZxid 的情况。
- 当 Leader 服务器已经将某事物操作记录到本地事物日志,但是没有成功发起 proposal 流程时就挂了。在新 Leader 选举出,并进行了几次事物操作后,挂掉的服务才重新启动,就会遇到这样的情况。对于这种特殊情况,就需要使用先回滚再同步的方式。
- 当 Leader 服务器发现 Learner 包含自己没有的 ZXID 时,会发送 TRUNC 命令,并带上自己有的最接近 peerLastZxid 的 ZXID。
- 回滚之后,再按照差异化同步的方式同步 Leader 有的新数据即可。
- 全量同步(SNAP 同步):适合 peerLastZxid < minCommitLog,或 Leader 服务器上没有提议缓存队列并且两边数据不一致的情况。(两边最新 ZXID 一致就不用同步了。)
- Leader 服务器先向 Learner 发送一个 SNAP 指令,通知 Learner 即将进行全量数据同步。
- 随后获取全量数据,将它们序列化后传输给 Learner。
- Learner 服务器接收到全量数据后,反序列化之并载入到内存数据库中。