最近WPaxos框架已经开源,WPaxos开源地址。作为旁观者之一,大致简单的梳理一下整体的代码结构,做一个源码浅析,后面有时间再对每一部分做深入的分析。
Paxos算法是一个强一致性的算法,最精简的描述应当是Paxos原作者在《Paxos Made Simple》中的描述:
Phase 1
(a) A proposer selects a proposal number n and sends a prepare request with number n to a majority of acceptors.
(b) If an acceptor receives a prepare request with number n greater than that of any prepare request to which it has already responded, then it responds to the request with a promise not to accept any more proposals numbered less than n and with the highest-numbered pro-posal (if any) that it has accepted.
Phase 2
(a) If the proposer receives a response to its prepare requests (numbered n) from a majority of acceptors, then it sends an accept request to each of those acceptors for a proposal numbered n with a value v , where v is the value of the highest-numbered proposal among the responses, or is any value if the responses reported no proposals.
(b) If an acceptor receives an accept request for a proposal numbered n, it accepts the proposal unless it has already responded to a prepare request having a number greater than n.
在Paxos算法中主要有三个角色,proposer、acceptor、leaner,其中learner是不参与协商过程,主要是proposer与acceptor交互。简单来说整个协商过程分为两个阶段:
Prepare阶段:
由propose发起提议,提议要携带一个全局递增的编号n,该提议并不携带提议内容,只是有编号n。当acceptor接受到该提议时,如果是第一次收到提议或者接收到的提议比自身已经接受的提议编号大,则接受该提议,设置接受的最大提议编号n,并不再接受所有小于等于n的提议编号,回复给proposer接受。否则的话,回复拒绝该提议。
Accept阶段:
当proposer接收到超过半数节点同意该提议,propose正式发起提议[n,value]。同样acceptor收到该提议时,如果是第一次收到提议或者接收到的提议比自身已经接受的提议编号大,则接受该提议,设置接受的最大提议编号n,并不再接受所有小于等于n的提议编号,回复给proposer接受。否则的话,回复拒绝该提议。
太多经典的原理讲解,这里从工程实现,来简单理解paxos算法过程和原理。
一切的propose的源头来源于如下代码:
public int newValue(byte[] value) {
if(this.canSkipPrepare && !this.wasRejectBySomeone) {
accept();
} else {
prepare(this.wasRejectBySomeone);
}
return 0;
}
在工程实现中,如果当前节点发起过propose,即已经执行过prepare并且被接受了,就可以跳过prepare阶段直接进行accept阶段,因为每个阶段都会有网络消耗,所以为了减少网络消耗,在保证paxos算法核心原理的情况下,有条件的跳过了prepare阶段。
Prepare阶段如下代码:
public void prepare(boolean needNewBallot) {
exitAccept();
this.isPreparing = true;
this.canSkipPrepare = false;
this.wasRejectBySomeone = false;
this.proposerState.resetHighestOtherPreAcceptBallot();
if(needNewBallot) {
this.proposerState.newPrepare();
}
PaxosMsg paxosMsg = new PaxosMsg();
paxosMsg.setMsgType(PaxosMsgType.paxosPrepare.getValue());
paxosMsg.setInstanceID(getInstanceID());
paxosMsg.setNodeID(this.pConfig.getMyNodeID());
paxosMsg.setProposalID(this.proposerState.getProposalID());
this.msgCounter.startNewRound();
// 将当前的 prepare 加入到定时器当中去
addPrepareTimer(0);
// 广播给所有的节点尝试 prepare
int runSelfFirst = BroadcastMessageType.BroadcastMessage_Type_RunSelf_First.getType();
int sendType = MessageSendType.UDP.getValue();
paxosMsg.setTimestamp(System.currentTimeMillis());
broadcastMessage(paxosMsg, runSelfFirst, sendType);
}
可以看到,prepare首先是设置当前节点状态,即设置为prepare状态。如果之前被其他节点拒绝过,则需要重新设置提议的编号,被其他节点拒绝是因为有其他节点发起的提议编号要大于自己发起的提议编号,而且可能会成功,如果再用相同的提议编号,则还会被其他节点拒绝。所以这里设置新的提议编号如代码:
public void newPrepare() {
long maxProposalId = this.proposalID > this.highestOtherProposalID ? this.proposalID : this.highestOtherProposalID;
this.proposalID = maxProposalId + 1;
}
即再次发起prepare时候的提议编号一定要大于已经知晓的最大提议编号,即原提议编号与已知晓最大提议编号最大值再加1。
然后对这一次prepare增加超时任务,因为网络延迟等原因,prepare可能会长时间没有结果,所以要对每次prepare增加一个定时检测任务,当超过超时时间后,对该次prepare重新处理。处理的方式相对比较简单,就是重新执行一次prepare。
最后把这一次propose的内容首先由自己处理,然后再发送给其他节点。即设置的RunSelf_First标志所起的作用。
当节点收到prepare消息后的处理逻辑如代码:
public int onPrepare(PaxosMsg paxosMsg) {
PaxosMsg replyPaxosMsg = new PaxosMsg();
replyPaxosMsg.setInstanceID(getInstanceID());
replyPaxosMsg.setNodeID(this.pConfig.getMyNodeID());
replyPaxosMsg.setProposalID(paxosMsg.getProposalID());
replyPaxosMsg.setMsgType(PaxosMsgType.paxosPrepareReply.getValue());
BallotNumber ballot = new BallotNumber(paxosMsg.getProposalID(), paxosMsg.getNodeID());
BallotNumber pbn = this.acceptorState.getPromiseBallot();
if(ballot.ge(pbn)) {
int ret = updateAcceptorState4Prepare(replyPaxosMsg, ballot);
if(ret != 0) return ret;
} else {
replyPaxosMsg.setRejectByPromiseID(this.acceptorState.getPromiseBallot().getProposalID());
}
long replyNodeId = paxosMsg.getNodeID();
sendMessage(replyNodeId, replyPaxosMsg);
return 0;
}
可以看到这里就是paxos算法保证一致性的关键点,即只有收到的提议编号大于自身已经接受的提议编号,才会更新自身已经接受的提议编号,否则拒绝这一次提议,并告知自己已经接受的提议编号。然后回复消息给发起提议的节点。
接下来看一下收到回复后的处理逻辑,如代码:
public void onPrepareReply(PaxosMsg paxosMsg) {
if(!this.isPreparing) {
return ;
}
if(paxosMsg.getProposalID() != this.proposerState.getProposalID()) {
return ;
}
this.msgCounter.addReceive(paxosMsg.getNodeID());
if(paxosMsg.getRejectByPromiseID() == 0) {
this.msgCounter.addPromiseOrAccept(paxosMsg.getNodeID());
this.proposerState.addPreAcceptValue(ballot, paxosMsg.getValue());
} else {
this.msgCounter.addReject(paxosMsg.getNodeID());
this.wasRejectBySomeone = true;
this.proposerState.setOtherProposalID(paxosMsg.getRejectByPromiseID());
}
if(this.msgCounter.isPassedOnThisRound()) {
int useTimeMs = this.timeStat.point();
this.canSkipPrepare = true;
accept();
} else if(this.msgCounter.isRejectedOnThisRound()
|| this.msgCounter.isAllReceiveOnThisRound()) {
addPrepareTimer(OtherUtils.fastRand() % 30 + 10);
}
}
如果当前节点已经不在prepare阶段了,则直接返回。因为只需要接收到超过半数节点的同意,就可以认为这次prepare成功,可以进入accept阶段了,所以返回慢的节点的消息就可以忽略了。如果返回的提议的编号和当前节点发起提议的编号不一致,也直接返回。如果接受到的是同意该提议,则统计同意的数量,并且要更新接受到的提议编号和提议值。如果是接受到的是拒绝该提议,则设置这次提议被其他节点拒绝过,更新拒绝节点所承诺的提议编号,目的是设置当这一次prepare被拒绝之后,再一次发起prepare时重新选择提议编号时的最大提议编号值。最后判断这一次prepare是否通过,即同意的节点数量是否超过半数,如果超过半数,则设置可以跳过prepare阶段的标识,然后进入accept阶段。如果被拒绝后,则在一段时间后重新发起prepare。
接下来看accept阶段,如下代码:
public void accept() {
exitPrepare();
this.isAccepting = true;
PaxosMsg paxosMsg = new PaxosMsg();
paxosMsg.setMsgType(PaxosMsgType.paxosAccept.getValue());
paxosMsg.setInstanceID(getInstanceID());
paxosMsg.setNodeID(this.pConfig.getMyNodeID());
paxosMsg.setProposalID(this.proposerState.getProposalID());
paxosMsg.setValue(this.proposerState.getValue());
paxosMsg.setLastChecksum(getLastChecksum());
this.msgCounter.startNewRound();
addAcceptTimer(0);
int runSelfFirst = BroadcastMessageType.BroadcastMessage_Type_RunSelf_Final.getType();
int sendType = MessageSendType.UDP.getValue();
paxosMsg.setTimestamp(System.currentTimeMillis());
broadcastMessage(paxosMsg, runSelfFirst, sendType);
}
可见逻辑是一样的,首先是退出prepare阶段,并设置当前节点处于accept阶段,同样设置accept过期任务,因为accept同样也会因为网络等原因,导致无法结束或很长时间无法结束,所以增加一个过期任务,当超过时间阈值后,重新进入prepare阶段,而不再是accept阶段。接着发送消息到所有的节点上。注意这里的消息内容与prepare阶段不太一样,在prepare阶段的消息并没有携带消息内容,而accept阶段携带了消息内容,与前面原理内容一致。
当节点接收到accept消息后,逻辑与收到prepare几乎一样,也是判断当前承诺的提议编号是否小于接收到的提议编号,则保存这一次提议,这里保存消息就涉及到了前面所说的消息存储,这里会保存实际的消息内容以及索引文件,为了提高写的效率,这里写消息内容是顺序追加写,即使这一次提议最终没有成功,也不会做修改。但是写索引却是根据instanceId值计算好待写入的位置,可以覆盖已写的内容。否则回复拒绝。然后回复消息,当接受到回复的消息后,处理逻辑如代码:
public void onAcceptReply(PaxosMsg paxosMsg) {
if(!this.isAccepting) {
return ;
}
if(paxosMsg.getProposalID() != this.proposerState.getProposalID()) {
return ;
}
this.msgCounter.addReceive(paxosMsg.getNodeID());
if(paxosMsg.getRejectByPromiseID() == 0) {
this.msgCounter.addPromiseOrAccept(paxosMsg.getNodeID());
} else {
this.msgCounter.addReject(paxosMsg.getNodeID());
this.wasRejectBySomeone = true;
this.proposerState.setOtherProposalID(paxosMsg.getRejectByPromiseID());
}
if(this.msgCounter.isPassedOnThisRound()) {
int useTimeMs = this.timeStat.point();
exitAccept();
this.learner.proposerSendSuccess(getInstanceID(), this.proposerState.getProposalID());
} else if(this.msgCounter.isRejectedOnThisRound()
|| this.msgCounter.isAllReceiveOnThisRound()) {
addAcceptTimer(OtherUtils.fastRand() % 30 +10);
}
}
同样也是先判断节点状态和提议编号是否相同,如果接收到同意,则统计同意的节点数量。如果接收到拒绝,同样设置当前节点被拒绝状态,然后设置被拒绝的提议编号。当超过半数节点拒绝的时候,在一定时间后重新发起prepare。当超过半数节点同意后,发起propose成功的操作proposerSendSuccess。当每个节点收到SendSuccess消息后,会更新内存中的值,不需要再写文件,因为在accept阶段已经写过。然后再执行状态机。全部执行完后,增加instanceId,重置所有阶段承诺的提议编号。
这里可能会有疑问,如果发起提议的节点状态机执行失败了还怎么保证消息的一致性。其实可以看到这里发起proposerSendSuccess的时候,首先是发送给了本节点,只有当本节点执行成功后,才会再发送给其他节点,这就保证了发送提议的节点一定会执行成功。当其他节点执行失败时,又怎么办?这就解涉及到了Learn的内容,即当其他节点执行失败时,对应的instanceId不会增加,具体可以看收到proposerSendSuccess的操作,如代码:
public void onProposerSendSuccess(PaxosMsg paxosMsg) {
if(paxosMsg.getInstanceID() != getInstanceID()) {
return ;
}
if(this.acceptor.getAcceptorState().getAcceptedBallot().getProposalID() == 0) {
return ;
}
BallotNumber ballot = new BallotNumber(paxosMsg.getProposalID(), paxosMsg.getNodeID());
BallotNumber thisBallot = this.acceptor.getAcceptorState().getAcceptedBallot();
if(thisBallot.getProposalID() != ballot.getProposalID()
|| thisBallot.getNodeId() != ballot.getNodeId()) {
return ;
}
this.learnerState.learnValueWithoutWrite(paxosMsg.getInstanceID(),
this.acceptor.getAcceptorState().getAcceptedValue(), this.acceptor.getAcceptorState().getCheckSum());
transmitToFollower();
}
可以看到,因为执行失败的时候,instanceId不会增加,在代码第二行会比较,如果当前的instanceId不等于消息的instanceId,则会直接返回,这就保证了所有节点的instanceId会保证一致。那么如果节点落后了,落后节点就不会在参与到提议过程中了,此时会通过Learn机制学习落后的instanceId,当追齐后重新参与到提议过程中。
在启动paxos,会启动定时任务,定时的发送learn请求到每个节点,当收到learn请求之后处理逻辑如代码:
public void onAskforLearn(PaxosMsg paxosMsg) {
setSeenInstanceID(paxosMsg.getInstanceID(), paxosMsg.getNodeID());
if(paxosMsg.getProposalNodeID() == this.pConfig.getMyNodeID()) {
this.pConfig.addFollowerNode(paxosMsg.getNodeID());
}
if(paxosMsg.getInstanceID() >= getInstanceID()) return;
if(paxosMsg.getInstanceID() >= this.checkpointMgr.getMinChosenInstanceID()) {
if(!this.learnerSender.prepare(paxosMsg.getInstanceID(), paxosMsg.getNodeID())) {
if(paxosMsg.getInstanceID() == (getInstanceID() - 1)) {
//send one value
AcceptorStateData state = new AcceptorStateData();
int ret = this.paxosLog.readState(this.pConfig.getMyGroupIdx(), paxosMsg.getInstanceID(), state);
if(ret == 0) {
BallotNumber ballot = new BallotNumber(state.getPromiseID(), state.getAcceptedNodeID());
sendLearnValue(paxosMsg.getNodeID(), paxosMsg.getInstanceID(), ballot,
state.getAcceptedValue(), 0, false);
}
}
return ;
}
}
sendNowInstanceID(paxosMsg.getInstanceID(), paxosMsg.getNodeID());
}
首先是设置可以看见的最大的instanceId,这个值在处理消息收到的prepare和accept消息时,如果消息的instanceId大于自身的的instanceId,并且大于这里设置的值,则会把这个消息放到重试队列中。
接着是如果自身没有可以学习的内容,就直接返回。如果有学习的数据,即请求的instanceId大于节点最小的instanceId,则判断是否已经在处理其他节点的learn请求,如果没有,则通过learnSender发送一些信息给对方节点。如果只差了一个instanceId,则直接把差的这个intanceId内容发送给请求节点即可,不需要再走learnSender。
接下来具体看发送了什么内容,如代码:
public void sendNowInstanceID(long instanceID, long sendNodeId) {
PaxosMsg msg = new PaxosMsg();
msg.setInstanceID(instanceID);
msg.setNodeID(this.pConfig.getMyNodeID());
msg.setMsgType(PaxosMsgType.paxosLearnerSendNowInstanceID.getValue());
msg.setNowInstanceID(getInstanceID());
msg.setMinchosenInstanceID(this.checkpointMgr.getMinChosenInstanceID());
if(getInstanceID() - instanceID > 50) {
//instanceid too close not need to send vsm/master checkpoint.
byte[] systemVariablesCPBuffer = this.pConfig.getSystemVSM().getCheckpointBuffer();
msg.setSystemVariables(systemVariablesCPBuffer);
if(this.pConfig.getMasterSM() != null) {
byte[] masterVariableCPBuffer = this.pConfig.getMasterSM().getCheckpointBuffer();
msg.setMasterVariables(masterVariableCPBuffer);
}
}
sendMessage(sendNodeId, msg);
}
发送了自身节点信息,现在的instanceId,最小的instanceId,和请求的instanceId。如果instanceId相差超过50,则认为两个节点信息不一致,携带当前节点的master信息和成员信息给请求节点。
当请求节点收到回复信息后,处理逻辑如代码:
public void onSendNowInstanceID(PaxosMsg paxosMsg) {
long receiveNowInstanceId = paxosMsg.getNowInstanceID();
long currInstanceId = this.getInstanceID();
setSeenInstanceID(receiveNowInstanceId, paxosMsg.getNodeID());
UpdateCpRet updateCpRet = this.pConfig.getSystemVSM().updateByCheckpoint(paxosMsg.getSystemVariables());
int ret = updateCpRet.getRet();
boolean isChanged = updateCpRet.isChange();
if(ret == 0 && isChanged) {
return ;
}
if(this.pConfig.getMasterSM() != null) {
UpdateCpRet masterUpdateCpRet = this.pConfig.getMasterSM().updateByCheckpoint(paxosMsg.getMasterVariables());
ret = masterUpdateCpRet.getRet();
boolean isMasterChanged = masterUpdateCpRet.isChange();
if(ret == 0 && isMasterChanged) {
logger.warn("MasterVariables changed!");
}
}
if(paxosMsg.getInstanceID() != getInstanceID()) {
return ;
}
if(paxosMsg.getNowInstanceID() <= getInstanceID()) {
return ;
}
if(paxosMsg.getMinChosenInstanceID() > getInstanceID()) {
askforCheckpoint(paxosMsg.getNodeID());
} else if(!this.isIMLearning) {
comfirmAskForLearn(paxosMsg.getNodeID());
}
}
首先同样是更新seenInstanceId,然后根据接收到的信息更新master与成员信息,如果与本地不一致,则直接返回。在判断当前的instanceId与发起learn请求时发送的instanceId是否还相同,如果不相同,则认为已经从别的节点学习了,也直接返回。如果其他节点的最小instanceId都要大于自己的instanceId了,那么就进入checkpoint模式。否则回复节点开始学习。当节点收到确认信息之后,正式启动learnSender开始发送内容。如代码:
public void sendLearnedValue(long beginInstanceID, long sendToNodeID) {
long sendInstanceId = beginInstanceID;
int ret = 0;
//control send speed to avoid affecting the network too much.
int sendQps = InsideOptions.getInstance().getLearnerSenderSendQps();
int sleepMs = sendQps > 1000 ? 1 : 1000 / sendQps;
int sendInterval = sendQps > 1000 ? sendQps / 1000 + 1 : 1;
int sendCount = 0;
JavaOriTypeWrapper<Integer> lastCheckSumWrap = new JavaOriTypeWrapper<Integer>(0);
while(sendInstanceId < this.learner.getInstanceID()) {
ret = sendOne(sendInstanceId, sendToNodeID, lastCheckSumWrap);
if(ret != 0) {
return ;
}
if(!checkAck(sendInstanceId)) return ;
sendCount ++;
sendInstanceId ++;
releshSending();
if(sendCount >= sendInterval) {
sendCount = 0;
Time.sleep(sleepMs);
}
}
//succ send, reset ack lead.
this.ackLead = InsideOptions.getInstance().getLearnerSenderAckLead();
}
逻辑很简单,做了一下限速,防止发送速度过快占用网络资源,影响正常的通信。这里注意看,只发送到当前节点instanceId的前一条,这是因为当前这个instanceId可能处于提案的过程中,已经被选定,但是并没有完成。
发送学习内容如代码:
public int sendOne(long sendInstanceID, long sendToNodeID, JavaOriTypeWrapper<Integer> lastChecksum) {
AcceptorStateData state = new AcceptorStateData();
int ret = this.paxosLog.readState(this.config.getMyGroupIdx(), sendInstanceID, state);
if(ret != 0) {
return ret;
}
byte[] sendValue = state.getAcceptedValue();
if(sendValue != null && sendValue.length > 0) {
limiter.acquire(sendValue.length);
}
BallotNumber ballot = new BallotNumber(state.getPromiseID(), state.getAcceptedNodeID());
ret = this.learner.sendLearnValue(sendToNodeID, sendInstanceID, ballot, state.getAcceptedValue(), lastChecksum.getValue(), true);
lastChecksum.setValue(state.getCheckSum());
return ret;
}
根据instanceId读取内容,然后发送给学习节点。发送完后,节点暂停learnSender。
学习过程讲述完毕,前面还留了一个内容checkpoint模式。Checkpoint是paxos中重要的一个组成部分,为了防止数据文件无限的增长,肯定要定期的清理无用的数据内容。那么哪些数据可以被删除就是有checkpoint所决定。Checkpoint有三个重要的数据,minChosenInstanceId,maxChosenInstanceId,checkpointInstanceId。
minChosenInstanceId表示当前group的最小的instanceId,maxChosenInstanceId表示当前选定的最大的instanceId,checkpointInstanceId表示已经归档完毕的最大instanceId,checkpointInstanceId前面的数据都可以删除。当节点重启恢复的时候,则从checkpointInstanceId开始恢复即可。
讲述checkPoint之前还需要理解paxos中一个重要的概念——状态机。状态机可以看成是paxos数据的消费逻辑,当每次议案通过并保存成功后,都会交给状态机来执行。具体的执行逻辑由使用方自行定义。Paoxs中也定义了一些默认的状态机。paxos算法中会有多种类型的状态机同时存在,但是一份paxos数据只会被一种状态机执行,并且每个节点执行状态机后状态及数据要一致。状态机具体的方法如代码:
public interface StateMachine {
public int getSMID();
public boolean execute(int groupIdx, long instanceID, byte[] paxosValue, SMCtx smCtx);
public boolean executeForCheckpoint(int groupIdx, long instanceID, byte[] paxosValue);
public long getCheckpointInstanceID(int groupIdx);
public int lockCheckpointState();
public int getCheckpointState(int groupIdx, JavaOriTypeWrapper<String> dirPath, List<String> fileList);
public void unLockCheckpointState();
public int loadCheckpointState(int groupIdx, String checkpointTmpFileDirPath, List<String> fileList, long checkpointInstanceID);
public byte[] beforePropose(int groupIdx, byte[] sValue);
public boolean needCallBeforePropose();
public void fixCheckpointByMinChosenInstanceId(long minChosenInstanceID);
}
依次看每个方法的作用:
getSMID:返回每个状态机唯一标识,每次提交议案的时候都会携带一个SMID,表示最终由那个状态机来执行。
execute:执行状态机的方法
executeForCheckpoint:在执行checkpoint时的方法,会面讲述checkpoint时会用到
getCheckpointInstanceIdD:返回当前状态机的checkpointInstanceID,在后面讲述checkpoint会用到,在发送learn数据和、Replayer和Cleaner时会频繁调用
lockCheckpointState和unLockCheckpointState:在checkpoing模式下,发送checkpoint文件会使用,锁定checkpoint文件与解锁。
getCheckpointState和loadCheckpointState:在checkpint模式下,发送方获取checkpoint文件与接收方加载恢复checkpoint文件
beforeProcess和needCallBeforeProcess:状态机器的前置执行方法
fixCheckpointByMinchosenInstanceID:根据minChosenInstanceID修复checkpointInstanceID
接着看checkpoint,整个checkpoint有两个关键线程,Replayer和Cleaner,Replayer是用来做checkpoint执行的,只有被Replayer执行过的instanceId才可以被删除。Cleaner就是清除数据的。
Replayer代码:
public void run() {
long instanceId = this.smFac.getCheckpointInstanceID(this.config.getMyGroupIdx()) + 1;
while(true) {
try {
if(this.isEnd) {
logger.debug("Checkpoint.Replayer [END]");
return ;
}
if(!this.canRun) {
this.isPaused = true;
Time.sleep(1000);
continue ;
}
if(instanceId >= this.checkpointMgr.getMaxChosenInstanceID()) {
Time.sleep(1000);
continue ;
}
boolean playRet = playOne(instanceId);
if(playRet) {
logger.info("Play one done, instanceid [" + instanceId + "]");
instanceId ++;
} else {
logger.info("Play one fail, instanceid [" + instanceId + "]");
Time.sleep(500);
}
} catch (Exception e) {
logger.error("run throws exception", e);
}
}
}
首先通过状态机获取到当前的checkpoint位置,满足条件后对每个instanceId执行checkpoint。如代码:
public boolean playOne(long instanceID) {
AcceptorStateData state = new AcceptorStateData();
int ret = this.paxosLog.readState(this.config.getMyGroupIdx(), instanceID, state);
if(ret != 0) return false;
boolean executeRet = this.smFac.executeForCheckpoint(this.config.getMyGroupIdx(), instanceID, state.getAcceptedValue());
if(!executeRet) {
logger.error("Checkpoint sm excute fail, instanceid [" + instanceID + "]");
}
return executeRet;
}
根据instanceId读取内容,然后让对应的状态机执行checkpoint。
Cleaner如代码:
public void run() {
this.isStart = true;
toContinue();
int deleteQps = InsideOptions.getInstance().getCleanerDeleteQps();
int sleepMs = deleteQps > 1000 ? 1 : 1000 / deleteQps;
int deleteInterval = deleteQps > 1000 ? deleteQps / 1000 + 1 : 1;
while(true) {
try {
if(this.isEnd) {
logger.info("Checkpoint.Cleaner [END]");
return ;
}
if(!this.canRun) {
this.isPaused = true;
Time.sleep(1000);
continue ;
}
long instanceId = this.checkpointMgr.getMinChosenInstanceID();
long cpInstanceId = this.smFac.getCheckpointInstanceID(this.config.getMyGroupIdx()) + 1;
long maxChosenInstanceId = this.checkpointMgr.getMaxChosenInstanceID();
int deleteCount = 0;
while((instanceId + this.holdCount < cpInstanceId) &&
(instanceId + this.holdCount < maxChosenInstanceId)) {
boolean deleteRet = deleteOne(instanceId);
if(deleteRet) {
instanceId ++;
deleteCount ++;
if(deleteCount >= deleteInterval) {
deleteCount = 0;
Time.sleep(sleepMs);
}
} else {
logger.warn("delete system fail, instanceid [" + instanceId + "]");
break;
}
}
persistMinChosenInstanceID(instanceId);
Time.sleep(OtherUtils.fastRand() % 500 + 500);
} catch (Exception e) {
logger.error("run throws exception", e);
}
}
}
这里有一个holdCount,只有当前的instanceId+holdCount<checkpoint,才可以删除,这是为了前面所讲Learn所用,防止有节点落后时,可以快速学习,而不用进入checkpoint模式。这里删除之后,更新minInstanceId。那么整个过程串联起来就是,每次执行提议后,更新maxInstanceId,由Replayer定时校验当前的checkpoint是否小于maxInstanceId,如果小於则执行状态记得checkpoint操作,更新checkpoint。Cleaner定时清理已经执行过checkpoint操作的instanceId,定时检测minInstanceId+holdCount是否小于checkpoint,如果小於则删除instanceId,并更新minInstanceId。
Checkpoint描述完毕,接着看前面Lean阶段遗漏的checkpoint模式是做什么的。当发现自己的instanceId已经小于其他节点minInstanceId了,则进入checkpoint模式,发送askForCheckpoint到所有的节点上,当节点接收到该请求后,如果没有处于方checkpoint模式中,则开启CheckpointSender线程,如代码:
public void run() {
try {
this.isStarted = true;
this.absLastAckTime = Time.getSteadyClockMS();
//pause checkpoint replayer
boolean needContinue = false;
while(!this.checkpointMgr.getRelayer().isPaused()) {
if(this.isEnd) {
this.isEnded = true;
return ;
}
needContinue = true;
this.checkpointMgr.getRelayer().pause();
logger.debug("wait replayer paused.");
Time.sleep(100);
}
int ret = lockCheckpoint();
if(ret == 0) {
try {
sendCheckpoint();
} finally {
unlockCheckpoint();
}
}
//continue checkpoint replayer
if(needContinue) {
this.checkpointMgr.getRelayer().toContinue();
}
logger.info("Checkpoint.Sender [END]");
} catch (Exception e) {
logger.error("CheckpointSender run error", e);
} finally {
this.isEnded = true;
}
}
首先是关闭replayer线程,防止发送checkpoint时又对其进行更新。然后对checkpoint进行锁定,这里会执行每个状态机的lockCheckpoint方法,锁定checkpoint文件。发送checkpoint文件分为三个状态,begin、ing、end。节点收到begin时处理逻辑为清除当前节点的所有日志文件,包括索引,设置minInstanceId为接收到的节点的minInstanceId,设置学习节点为目标节点。发送ing逻辑如代码:
public int sendCheckpointFaSM(StateMachine sm) {
JavaOriTypeWrapper<String> dirPath = new JavaOriTypeWrapper<String>();
List<String> fileList = new ArrayList<String>();
int ret = sm.getCheckpointState(this.config.getMyGroupIdx(), dirPath, fileList);
if(ret != 0) {
logger.error("GetCheckpointState fail ret [" + ret +"], smid [" + sm.getSMID() + "]");
return -1;
}
String oriDirPath = dirPath.getValue();
if(oriDirPath == null || oriDirPath.length() == 0) {
logger.info("No Checkpoint, smid [" + sm.getSMID() + "]");
return 0;
}
if(!oriDirPath.endsWith("/")) {
oriDirPath += "/";
}
for(String filePath : fileList) {
if(filePath == null || "".equals(filePath)) continue;
ret = sendFile(sm, oriDirPath, filePath);
if(ret != 0) {
logger.error("SendFile fail, ret " + ret + " smid " + sm.getSMID());
return -1;
}
}
logger.info("END, send ok, smid [" + sm.getSMID() + "] filelistcount [" + fileList.size() + "]");
return 0;
}
会通过状态的getCheckpointState方法获取到checkpoint文件的位置,然后发送文件给学习节点。当学习节点收到ing状态时,处理逻辑代码:
public int receiveCheckpoint(CheckpointMsg checkpointMsg) {
if(checkpointMsg.getNodeID() != this.sendNodeID || checkpointMsg.getUuid() != this.uuid) {
return -2;
}
if(checkpointMsg.getSequenceID() == this.sequenceID) {
return 0;
}
if(checkpointMsg.getSequenceID() != this.sequenceID + 1) {
return -2;
}
String fileDir = getTmpDirPath(checkpointMsg.getSmID());
JavaOriTypeWrapper<String> ffpWrap = new JavaOriTypeWrapper<String>();
initFilePath(fileDir, ffpWrap);
String filePath = fileDir + "/" + checkpointMsg.getFilePath();
File file = new File(filePath);
FileOutputStream out = null;
try {
if(!file.exists()) {
file.createNewFile();
}
file.setReadable(true);
file.setWritable(true);
if(file.length() != checkpointMsg.getOffset()) {
return -2;
}
out = new FileOutputStream(file, true);
out.write(checkpointMsg.getBuffer());
out.flush();
this.sequenceID ++;
} catch (Exception e) {
logger.error("receiveCheckpoint error", e);
return -2;
} finally {
if(out != null) {
try {
out.close();
} catch (Exception e) {
logger.error("receiveCheckpoint close file error", e);
}
}
}
return 0;
}
为了防止占用网络,每次只会发一部分文件,所以每次发送都会携带一个递增的序号。当接受到消息后,会进行一系列判断,包括收到的节点是否是前面记录的节点,序号是否是已经收序号加1。都满足条件后,则把文件保存到本地的一个临时路径中。
当所有文件发送完后,会再发送一个end消息,告知学习节点已经发送完毕。当接收到end消息后,会对每个状态机执行lockCheckpointState操作,然后设置minInstanceId、maxInstanceId等,然后退出checkpoint模式
总结一下整个checkpoint学习过程,首先当进入checkpoint模式后,发送学习请求给所有节点,当收到某个节点的checkpoint start信息后,就只接受该节点的checkpoint信息。被学习的节点会开启sender线程,通过每个状态机的getCheckpointState方法获取所有状态机的checkpoint文件,把文件发送给学习节点。当发送完毕后,学习节点根据保存的所有文件,执行loadCheckpointState方法恢复数据。
以上就是WPaxos中一次完整的paxos流程,只是简单的顺序撸了一遍代码,具体还有很多细节,每一部分的细节后面会再单独详细解析。