zookeeper 数据同步原理
首先来看Leader做的工作:
二)中提到的同步数据时使用的逻辑时钟,它的初始值是0,每次选举过程都会递增的,在leader正式上任之后做的第一件事情,就是根据当前保存的数据id值,设置最新的逻辑时钟值:
long epoch = self.getLastLoggedZxid() >> 32L;
epoch++;
zk.setZxid(epoch << 32L);
(函数Leader::lead()中)
随后,leader构建NEWLEADER封包,该封包的数据是当前最大数据的id,广播给所有的follower,也就是告知follower leader保存的数据id是多少,大家看看是不是需要同步.
然后,leader根据follower数量给每个follower创建一个线程LearnerHandler,专门负责接收它们的同步数据请求.
leader主线程开始阻塞在这里,等待其他follower的回应(也就是LearnerHandler线程的处理结果),同样的,只有在超过半数的follower已经同步数据完毕,这个过程才能结束,leader才能正式成为leader.
所以其实leader与follower同步数据的大部分操作都在LearnerHandler线程中处理的,接着看这一块.
leader接收到的来自某个follower封包一定是FOLLOWERINFO,该封包告知了该服务器保存的数据id.之后根据这个数据id与本机保存的数据进行比较:
- 如果数据完全一致,则发送DIFF封包告知follower当前数据就是最新的了.
- 判断这一阶段之内有没有已经被提交的提议值,如果有,那么:
a) 如果有部分数据没有同步,那么会发送DIFF封包将有差异的数据同步过去.同时将follower没有的数据逐个发送COMMIT封包给follower要求记录下来.
b) 如果follower数据id更大,那么会发送TRUNC封包告知截除多余数据. - 如果这一阶段内没有提交的提议值,直接发送SNAP封包将快照同步发送给follower.
以上消息完毕之后,发送UPTODATE封包告知follower当前数据就是最新的了,再次发送NEWLEADER封包宣称自己是leader,等待follower的响应.
其次来看follower做的工作,follower与leader同步数据的操作在函数Follower::followLeader中进行.
首先会尝试与leader建立连接,这里有一个机制,如果一定时间内没有连接上,就报错退出,重新回到选举状态.
其次在函数learner::registerWithLeader中发送FOLLOWERINFO封包,该封包中带上自己的最大数据id,也就是会告知leader本机保存的最大数据id.
最后,根据前面对LeaderHandler的分析,leader会根据不同的情况发送DIFF,UPTODATE,TRUNC,SNAP,依次进行处理就是了,此时follower跟leader的数据也就同步上了.
由于leader端发送的最后一个封包是UPTODATE,因此在接收到这个封包之后follower结束同步数据过程,发送ACK封包回复leader.
以上过程中,任何情况出现的错误,服务器将自动将选举状态切换到LOOKING状态,重新开始进行选举.
在准备阶段完成follower连接到leader,具备通信状态
leader阻塞等待follower发来的第一个packet
校验packet类型是否是Leader.FOLLOWERINFO或者Leader.OBSERVERINFO
读取learner信息
sid
protocolVersion
校验follower的version不能比leader的version还要新
leader发送packet(Leader.LEADERINFO)给follower
follower收到Leader.LEADERINFO后给leader回复Leader.ACKEPOCH
leader根据follower ack的packet内容来决定同步的策略
lastProcessedZxid == peerLastZxid,leader的zxid和follower的相同
peerLastZxid > maxCommittedLog && !isPeerNewEpochZxid follower超前,删除follower多出的txlog部分
(maxCommittedLog >= peerLastZxid) && (minCommittedLog <= peerLastZxid) follower落后于leader,处于leader的中间 同步(peerLaxtZxid, maxZxid]之间的commitlog给follower
peerLastZxid < minCommittedLog && txnLogSyncEnabled follower落后于leader,使用txlog和commitlog同步给follower
接下来leader会不断的发送packet给follower,follower处理leader发来的每个packet
同步完成后follower回复ack给leader
leader、follower进入正式处理客户端请求的while循环
如果是对zk进行读取操作,读取到的数据可能是过期的旧数据,不是最新的数据。
image已上图为例,如果一个zk集群有10000台节点,当进行写入的时候,如果已经有6K个节点写入成功,zk就认为本次写请求成功。但是这时候如果一个客户端读取的刚好是另外4K个节点的数据,那么读取到的就是旧的过期数据。
在zk的官方文档中对此有解释,地址在:https://zookeeper.apache.org/doc/r3.1.2/zookeeperProgrammers.html
zookeeper一致性的保证:
ZooKeeper是一种高性能,可扩展的服务,虽然读取速度比写入快,但是读取和写入操作都设计的极为快速,这样做的原因是在读取的情况下,ZooKeeper可能会提供较旧的数据,但这是为了ZooKeeper的一致性保证:
- 顺序一致性:来自客户端的更新将按照发送的顺序被写入到zk
- 原子性:更新操作要么成功要么失败,没有中间状态
- 单系统快照:客户端将看到服务的相同视图,而不管它连接到的服务器。
- 可靠性:一旦应用更新,数据将被持久化,直到数据被再次更新,对于该保证有两个推论:1、如果客户端得到了成功的返回码,说明写入成功,数据被持久化,如果出现了通信错误,超时等一些故障,客户端将不知道更新是否已应用。我们采取措施尽量减少失败,但唯一的保证是只有成功的返回码。 (这在Paxos中称为单调性条件。)2、如果客户端已经读取到了数据或者写入成功了数据,都不会因为zk的失败而导致回滚;
- 及时性:在一段时间后,客户端将看到最新的系统更新,在此期间客户端将看到这种变更。
有时开发人员错误地假定ZooKeeper实际上没有做出另一个保证:跨客户端的强一致性
ZooKeeper并不保证在每个实例中,两个不同的客户端将具有相同的ZooKeeper数据的视图。由于诸如网络延迟的因素,一个客户端可以在另一客户端被通知该改变之前执行更新,考虑两个客户端A和B的场景。如果客户端A将znode / a的值从0设置为1,则告诉客户端B读取/ a,则客户端B可以读取旧值0,这取决于它连接到的服务器。如果客户端A和客户端B读取相同的值很重要,则客户端B应该在执行读取之前从ZooKeeper API方法调用sync()方法。
因此,ZooKeeper本身不保证所有服务器上同步发生变化,但ZooKeeper原语可用于构建更高级的函数,提供有用的客户端同步。
zk的sync方法的解释:异步的实现当前进程与leader之间的指定path的数据同步;
对于zookeeper来说,它实现了P分区容错性、C中的写入强一致性,丧失的是C中的读取一致性。
有所丧失,才有所获得,没有十全十美。
参与者成功提交事务
过程:
1》协调者给每一个参与者发起一个事务提交请求。
2》各个参与者收到请求后,给出回应:要么执行该事务成功,要么执行该事务失败。
3》如果所有参与者都回复成功执行该事务,那么协调者发起 commit 请求。
4》参与者提交事务后,给协调者一个反馈。
某些参与者提交事务失败
过程:
1》协调者给每一个参与者发起一个事务提交请求。
2》各个参与者收到请求后,给出回应:要么执行该事务成功,要么执行该事务失败。
3》参与者2执行事务失败,协调者直接给所有参与者发送回滚请求。(只要有一个参与者执行事务失败,那么都要回滚。)
4》参与者回滚事务后,给协调者一个反馈。
1》客户端发起一个写请求。
2》如果是 follower 节点接收到该请求,那么它会将该请求转发给 leader 节点处理。
3》leader 会把这个请求转化成一个事务 Proposal(提议),并把这个 Proposal 分发给集群中的所有 Follower 节点(Observer不会被转发)。
4》Leader 节点需要等待所有 Follower 节点的反馈,一旦超过半数的 Follower 节点进行了正确的反馈(执行事务成功),那么 Leader 就会再次向所有的 Follower 节点发送 commit 消息,要求各个 follower 节点对前面的一个 Proposal 进行提交。
5》leader 节点将最新数据同步给 observer 节点。
6》follower 节点将结果返回给客户端。
Zookeeper的同步过程
当leader选举完成之后,就需要将最新Leader的消息同步。
首先在leader端:
leader需要告知其他服务器当前的最新数据,即最大zxid是什么,此时leader会构建 一个NEWLEADER的数据包,包括当前最大的zxid,发送给follower或者observer,此时leader会启动一个leanerHandler的线程来处理所有follower的同步请求,同时阻塞主线程,只有半数以上的folower同步完毕之后,leader才成为真正的leader,退出选举同步过程。
Follower端:
首先与leader建立连接,如果连接超时失败,则重新进入选举状态选举leader,如果连接成功,则会将自己的最新zxid封装为FOLLOWERINFO发送给leader
同步算法:
直接差异化同步(DIFF同步)
仅回滚同步,即删除多余的事务日志(TRUNC)
先回滚再差异化同步(TRUNC+DIFF)
全量同步(SNAP同步)
差异化同步(DIFF):
条件:MinCommitedLog < peerLastZxid < MaxCommitedLog
举例:leader的未proposal的队列中有0X50001,0X50002,0X50003,0X50004,0X50005,此时follower的peerLastZxid为0X50003,因此需要使用差异化同步将0X50004和0X50005同步给follower。同步顺序如下:
0X50004 -> Proposal -> Commit
0X50005 -> Proposal -> Commit
TRUNC+DIFF同步:
假设此时leaderB发送proposal并且提交了0X50001,0X50002,但没有提交0X50003,但是没有发送commit命令宕机,如果server C成为leader,经过同步后其自大MaxCommitedLog为0X60002,此时server B重新加入集群,由于Leader C中没有proposal 0X50003的提交记录,因此,发送TRUNC回滚数据,回滚完成之后,C向B发送确认消息,确认当前B的最新zxid为0X50002,然后发送DIFF进行差异化同步,此时B发送ACK给C,接着C会差异化同步相应的Proposal,然后提交,接着通知B,B在同步完成之后会发送确认ACK消息给C,同步结束。
全量同步(SNAP)
使用与当一个节点宕机太久,中间已经生成了大量的文件,此时集群的MinCommitedLog比宕机节点的最大zxid还要大,此时需要进行全量同步。
首先leader会发送SNAP命令给follower,follower接收到命令后进入同步阶段,leader会将所有的数据全量发送给follower,follower处理完毕之后leader还会将同步期间发生变化的数据增量发送给follower进行同步。
到这里本篇文章已经讲述完了Zookeeper的选举过程已经数据一致性同步过程,如有任何问题,欢迎各位前辈留言指教。
什么叫顺序一致性:
假设有一个Zookeeper集群(N>=3,N为奇数),那么只有一个Leader(通过FastLeaderElection选主策略选取),所有的写操作(客户端请求Leader或Follower的写操作)都由Leader统一处理,Follower虽然对外提供读写,但写操作会提交到Leader,由Leader和Follower共同保证同一个Follower请求的顺序性,Leader会为每个请求生成一个zxid(高32位是epoch,用来标识leader选举周期,每次一个leader被选出来,都会有一个新的epoch,标识当前属于哪个leader的统治时期,低32位用于递增计数)
针对同一个Follower A提交的写请求request1、request2,某些Follower虽然可能不能在请求提交成功后立即看到(也就是强一致性),但经过自身与Leader之间的同步后,这些Follower在看到这两个请求时,一定是先看到request1,然后再看到request2,两个请求之间不会乱序,即顺序一致性
Leader在处理第4步Follower的ack回复时,采用过半数响应即成功原则,也就是这时候有的Follower是还没有处理或者处理成功这个请求的
那么问题来了,怎么保证顺序一致性的呢?
FollowerRequestProcessor为Follower的首个处理器,如果是写请求,先将请求写入commitprocessor的queuedRequests(方便后续commit时判断是否本Follower提交的写请求),然后转Leader
Leader为每个请求生成zxid,下发proposal给Follower,Follower会将请求写入到pendingTxns阻塞队列及txnLog中,然后发送ack给Leader
public void logRequest(TxnHeader hdr, Record txn) {
Request request = new Request(hdr.getClientId(), hdr.getCxid(), hdr.getType(), hdr, txn, hdr.getZxid());
if ((request.zxid & 0xffffffffL) != 0) {
pendingTxns.add(request);
}
syncProcessor.processRequest(request);
}
proposal这步是会发给所有的follower的(放到LearnerHandler的请求处理队列中,一个Follower一个LearnerHandler),之后Follower的ack就不一定全返回了
ack过半,Leader发送commit请求给所有Follower,Follower对比commit request的zxid和前面提到的pendingTxns的zxid,不一致的话Follower退出,重新跟Leader同步
long firstElementZxid = pendingTxns.element().zxid;
if (firstElementZxid != zxid) {
LOG.error("Committing zxid 0x" + Long.toHexString(zxid)
+ " but next pending txn 0x"
+ Long.toHexString(firstElementZxid));
System.exit(12);
}
Follower处理commit请求,如果不是本Follower提交的写请求,直接调用FinalRequestProcessor做持久化,触发watches;如果是本Follower提交,则做一些特殊处理(主要针对客户端连接断开的场景),然后调用FinalRequestProcessor等后续处理流程
FinalRequestProcessor做持久化,返回客户端
总之:Follower通过队列和zxid等顺序标识保证请求的顺序处理,一言不合就会重新同步Leader
zookeeper 如何保证半数提交后剩下的节点上最新的数据呢?
zookeeper 的leader和follower的prepare和commit时,只要半数的节点通过就算同意,leader就会commit,那么剩下的半数节点的数据如何同步到最新的呢?
剩下的节点,会进行版本比对,发现版本不一致的话,会更新节点的数据。