phxpaxos状态机的管理
一、phxpaxos内置的日志系统
为了应对系统故障、进程重启之类的工程问题,phxpaxos需要对系统的状态进行持久化存储。从最简单的情况来说,系统必须要保证所有proposer提议的实例ID是连续递增的,而这个信息本身就要求对系统的状态进行持久化存储。
但是,实例号的连续只是一个基础要求。考虑到paxos算法本身就是为了不稳定系统的强一致性问题,那么允许系统中的节点出现故障然后恢复的场景就不可避免。当一个节点重新启动之后,它如果获取整个系统的当前状态,同样是工程实现的一个重要问题。
为了满足第二个需求,每个节点可以保存自己所有已经执行过的日志,它的作用感觉是相当于一个“我为人人、人人为我”的保险互助功能:由于每个节点都存储了所有已经完成的协商信息,那么当少数节点故障重启之后,就可以从系统的其它节点中同步历史信息,从而进行追赶。
同样是在工程实现中,如果节点保留所有的历史信息,那么这个数据量是不可控制的,所以此时引入checkpoint机制。所有的协商结果都是用于改变状态机的状态,如果能够把状态机的状态保存起来,那么就可以把状态机之前的状态删除,这就相当于是一个同步点功能。举一个简单的例子,系统协商的是一个商品的价格,那么不同的提议可以设置不同的值(例如加10,减5等操作),每轮提议都可以修改该值,但是无论如何,最终决定出来的都是一个数值。当系统中有其它节点加入系统之后,它就可以首先学习这个值,之后就可以在这个状态的基础上操作。再用一个更常见的例子来说明,当数据库主备迁移的时候,通常把当前的数据库状态拷贝到新机器,之后两者同时在接收并处理请求即可。
二、phxpaxos的日志实现
算法设计的目的是为了保证一个值被“接受”之后不再变化,所以问题的关键在于对acceptor状态的持久化。acceptor的状态包括它promise的值以及接受的值,所以在acceptor执行这些关键操作的时候就需要将这些信息进行持久化,系统中写入日志的场景包括下面三个:
1、promise时
int Acceptor :: OnPrepare(const PaxosMsg & oPaxosMsg)
{
……
int ret = m_oAcceptorState.Persist(GetInstanceID(), GetLastChecksum());
……
}
2、accept时:
void Acceptor :: OnAccept(const PaxosMsg & oPaxosMsg)
{
……
int ret = m_oAcceptorState.Persist(GetInstanceID(), GetLastChecksum());
……
}
3、learn时
int LearnerState :: LearnValue(const uint64_t llInstanceID, const BallotNumber & oLearnedBallot,
const std::string & sValue, const uint32_t iLastChecksum)
{
……
int ret = m_oPaxosLog.WriteState(oWriteOptions, m_poConfig->GetMyGroupIdx(), llInstanceID, oState);
……
}
三、系统初始化时对InstanceID的处理
启动时会从本地存储中读取最大的InstanceID数值。由于leveldb支持这种获取最大键值的形式,所以这个操作没有问题。
int Acceptor :: Init()
{
uint64_t llInstanceID = 0;
int ret = m_oAcceptorState.Load(llInstanceID);
if (ret != 0)
{
NLErr("Load State fail, ret %d", ret);
return ret;
}
if (llInstanceID == 0)
{
PLGImp("Empty database");
}
SetInstanceID(llInstanceID);
PLGImp("OK");
return 0;
}
四、给其它节点发送系统acceptor状态
const bool LearnerSender :: Comfirm(const uint64_t llBeginInstanceID, const nodeid_t iSendToNodeID)
{
m_oLock.Lock();
bool bComfirmRet = false;
if (IsIMSending() && (!m_bIsComfirmed))
{
if (m_llBeginInstanceID == llBeginInstanceID && m_iSendToNodeID == iSendToNodeID)
{
bComfirmRet = true;
m_bIsComfirmed = true;
m_oLock.Interupt();
}
}
m_oLock.UnLock();
return bComfirmRet;
}
五、prepare、accept、learn对同一个InstanceID的操作
一个InstanceID的chosen可能要陆续经过这三次持久化,但是由于这个InstanceID是唯一的,所以之后的更新并不会产生新的实例,只是更新实例的状态。
六、当其它节点学习的时候,如何避免把prepare和accept状态下的未完成节点同步过去
发送的上限是learner中保存的已经学习到的实例,prepare和accept状态下的InstanceID大于m_poLearner->GetInstanceID()
void LearnerSender :: SendLearnedValue(const uint64_t llBeginInstanceID, const nodeid_t iSendToNodeID)
{
……
int iSendCount = 0;
while (llSendInstanceID < m_poLearner->GetInstanceID())
{
……
}