開源框架WPaxos源碼淺析

最近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流程,只是簡單的順序擼了一遍代碼,具體還有很多細節,每一部分的細節後面會再單獨詳細解析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章