4. PhxPaxos源码分析之Learner

目录
1. PhxPaxos源码分析之关于PhxPaxos
2. PhxPaxos分析之网络基础部件
3. PhxPaxos源码分析之Proposer、Acceptor
4. PhxPaxos源码分析之Learner
5. PhxPaxos源码分析之状态机
6. PhxPaxos源码分析之归档机制
7. PhxPaxos源码分析之整体架构


4.1 基本概念

在Paxos中Learner角色负责向其他节点学习选中的提案值。具体包括如下两种场景:

  • Learner所在节点参与了提案选举,Learner需要知道其接受(accept)的提案值是否被选中(chosen)。
  • Learner所在节点已落后于其他节点,Learner需要选择合适的策略快速完成追赶,并重新参与到提案选举当中。

4.2 选中(chosen)通知

当本节点Proposer的某个提案被选中(chosen)时,通过(MsgType_PaxosLearner_ProposerSendSuccess)消息通知到各个节点,代码如下:

void Learner::OnProposerSendSuccess(const PaxosMsg &oPaxosMsg)
{
    BP->GetLearnerBP()->OnProposerSendSuccess();

    PLGHead("START Msg.InstanceID %lu Now.InstanceID %lu Msg.ProposalID %lu State.AcceptedID %lu "
            "State.AcceptedNodeID %lu, Msg.from_nodeid %lu",
            oPaxosMsg.instanceid(), GetInstanceID(), oPaxosMsg.proposalid(),
            m_poAcceptor->GetAcceptorState()->GetAcceptedBallot().m_llProposalID,
            m_poAcceptor->GetAcceptorState()->GetAcceptedBallot().m_llNodeID,
            oPaxosMsg.nodeid());

    if (oPaxosMsg.instanceid() != GetInstanceID())
    {
        //Instance id not same, that means not in the same instance, ignord.
        PLGDebug("InstanceID not same, skip msg");
        return;
    }

    if (m_poAcceptor->GetAcceptorState()->GetAcceptedBallot().isnull())
    {
        //Not accept any yet.
        BP->GetLearnerBP()->OnProposerSendSuccessNotAcceptYet();
        PLGDebug("I haven't accpeted any proposal");
        return;
    }

    BallotNumber oBallot(oPaxosMsg.proposalid(), oPaxosMsg.nodeid());

    if (m_poAcceptor->GetAcceptorState()->GetAcceptedBallot() != oBallot)
    {
        //Proposalid not same, this accept value maybe not chosen value.
        PLGDebug("ProposalBallot not same to AcceptedBallot");
        BP->GetLearnerBP()->OnProposerSendSuccessBallotNotSame();
        return;
    }

    //learn value.
    m_oLearnerState.LearnValueWithoutWrite(
        oPaxosMsg.instanceid(),
        m_poAcceptor->GetAcceptorState()->GetAcceptedValue(),
        m_poAcceptor->GetAcceptorState()->GetChecksum());

    BP->GetLearnerBP()->OnProposerSendSuccessSuccessLearn();

    PLGHead("END Learn value OK, value %zu", m_poAcceptor->GetAcceptorState()->GetAcceptedValue().size());

    TransmitToFollower();
}

正常情况下,所有节点处于online状态,共同参与paxos选举。因此,各个节点的instance id一致。为了避免冲突,paxos建议只由主节点的proposer发起提案,这样保证接受提案和习得提案编号一致。

此时,Learn习得的提案值实际上就是本节点Accept的数据,因此learner只更新内存状态即可,无需再次落盘(acceptor已落盘)。最后,如果存在follower节点,数据同步到follower(follower节点不参与paxos算法,相当于某个paxos节点的同步备)。

4.3 提案值追赶

一旦节点处于落后状态,它无法再参与到paxos提案选举中来。这时需要由learner发起主动学习完成追赶。

PhxPaxos启动时,启动learner定时器。learner定时发送learn请求到各个节点,发送请求携带本节点的Instance ID、Node ID信息。各节点收到该请求后,处理逻辑如下:

    //本节点无可学习数据,跳过本次请求
    if (oPaxosMsg.instanceid() >= GetInstanceID())
    {
        return;
    }
    //本节点有可学习数据,且本节点保存了此部分的paxos log
    if (oPaxosMsg.instanceid() >= m_poCheckpointMgr->GetMinChosenInstanceID())
    {
        //通知Learner Sender发送数据
        if (!m_oLearnerSender.Prepare(oPaxosMsg.instanceid(), oPaxosMsg.nodeid()))
        {
            BP->GetLearnerBP()->OnAskforLearnGetLockFail();

            PLGErr("LearnerSender working for others.");
            //如本节点正在处理其他节点的learn请求,但发起者只少了一条记录,绕过Learner Sender发送。
            if (oPaxosMsg.instanceid() == (GetInstanceID() - 1))
            {
                PLGImp("InstanceID only difference one, just send this value to other.");
                //send one value
                AcceptorStateData oState;
                int ret = m_oPaxosLog.ReadState(m_poConfig->GetMyGroupIdx(), oPaxosMsg.instanceid(), oState);
                if (ret == 0)
                {
                    BallotNumber oBallot(oState.acceptedid(), oState.acceptednodeid());
                    SendLearnValue(oPaxosMsg.nodeid(), oPaxosMsg.instanceid(), oBallot, oState.acceptedvalue(), 0, false);
                }
            }
            //如果Leaner Sender正在处理其他节点请求,跳过本次请求
            return;
        }
    }

    //本节点有可学习数据,并且本节点当前并未处理其他节点的Learn请求,发送本节点信息给发起者
    SendNowInstanceID(oPaxosMsg.instanceid(), oPaxosMsg.nodeid());

当接收节点有可学习数据,并且当前并未处理其他节点的Learn请求,发送如下信息包括:本节点的node id、当前的instance id、min chosen instance id、请求的instance id;除此之外,当paxos log差距超过50,那么两边的主节点信息、集群信息可能不同,发送此部分数据。发起者可能依次接收到多个节点请求,需要二次确认:

    //当初请求的instance id和本节点已不一致,说明已通过其他方式开始习得
    if (oPaxosMsg.instanceid() != GetInstanceID())
    {
        PLGErr("Lag msg, skip");
        return;
    }
    //本节点已经追赶甚至超过了接受节点的paxos log,跳过
    if (oPaxosMsg.nowinstanceid() <= GetInstanceID())
    {
        PLGErr("Lag msg, skip");
        return;
    }
    //本节点数据落后太多,接受节点已无完整的paxos log数据,尝试进入Checkpoint模式
    if (oPaxosMsg.minchoseninstanceid() > GetInstanceID())
    {
        BP->GetCheckpointBP()->NeedAskforCheckpoint();

        PLGHead("my instanceid %lu small than other's minchoseninstanceid %lu, other nodeid %lu",
                GetInstanceID(), oPaxosMsg.minchoseninstanceid(), oPaxosMsg.nodeid());

        AskforCheckpoint(oPaxosMsg.nodeid());
    }
    else if (!m_bIsIMLearning)
    {
        //接受节点有可习得的完整数据,向接受节点确认习得请求
        ComfirmAskForLearn(oPaxosMsg.nodeid());
    }

其实上面做了这么多,简单讲就是:如果本节点在请求这段时间内并未进行自主学习,并且也没有其他节点在此之前通知本节点开始学习,本节点发送确认请求到learn接收者,准备开始接受learn数据。learn接受者收到确认请求激活learner sender线程,发送paxos log数据。

4.4 Learner Sender

Learner Sender发送本节点数据到请求节点。为了避免learn操作造成网络拥塞,在数据发送前做了限流。

void LearnerSender :: SendLearnedValue(const uint64_t llBeginInstanceID, const nodeid_t iSendToNodeID)
{
    PLGHead("BeginInstanceID %lu SendToNodeID %lu", llBeginInstanceID, iSendToNodeID);

    uint64_t llSendInstanceID = llBeginInstanceID;
    int ret = 0;
    
    uint32_t iLastChecksum = 0;

    //control send speed to avoid affecting the network too much.
    int iSendQps = LearnerSender_SEND_QPS;
    int iSleepMs = iSendQps > 1000 ? 1 : 1000 / iSendQps;
    int iSendInterval = iSendQps > 1000 ? iSendQps / 1000 + 1 : 1; 

    PLGDebug("SendQps %d SleepMs %d SendInterval %d AckLead %d",
            iSendQps, iSleepMs, iSendInterval, m_iAckLead);

    int iSendCount = 0;
    while (llSendInstanceID < m_poLearner->GetInstanceID())
    {    
        ret = SendOne(llSendInstanceID, iSendToNodeID, iLastChecksum);
        if (ret != 0)
        {
            PLGErr("SendOne fail, SendInstanceID %lu SendToNodeID %lu ret %d",
                    llSendInstanceID, iSendToNodeID, ret);
            return;
        }

        if (!CheckAck(llSendInstanceID))
        {
            return;
        }

        iSendCount++;
        llSendInstanceID++;
        ReleshSending();

        if (iSendCount >= iSendInterval)
        {
            iSendCount = 0;
            Time::MsSleep(iSleepMs);
        }
    }

    //succ send, reset ack lead.
    m_iAckLead = LearnerSender_ACK_LEAD;
    PLGImp("SendDone, SendEndInstanceID %lu", llSendInstanceID);
}

注意,这里的while循环判定逻辑为while (llSendInstanceID < m_poLearner->GetInstanceID()),即只追赶到当前instance id的前一条。这是因为learner当前所处的instance id可能尚未完成提案,但这之前的提案是已选中(chosen)的。单条paxos log发送逻辑如下:

int LearnerSender :: SendOne(const uint64_t llSendInstanceID, const nodeid_t iSendToNodeID, uint32_t & iLastChecksum)
{
    BP->GetLearnerBP()->SenderSendOnePaxosLog();

    AcceptorStateData oState;
    int ret = m_poPaxosLog->ReadState(m_poConfig->GetMyGroupIdx(), llSendInstanceID, oState);
    if (ret != 0)
    {
        return ret;
    }

    BallotNumber oBallot(oState.acceptedid(), oState.acceptednodeid());

    ret = m_poLearner->SendLearnValue(iSendToNodeID, llSendInstanceID, oBallot, oState.acceptedvalue(), iLastChecksum);

    iLastChecksum = oState.checksum();

    return ret;
}

4.5 更快的对齐数据

下述文字截取自《微信自研生产级paxos类库PhxPaxos实现原理介绍》

上文说到当各台机器的当前运行实例编号不一致的时候,就需要Learner介入工作来对齐数据了。Learner通过其他机器拉取到当前实例的chosen value,从而跳转到下一编号的实例,如此反复最终将自己的实例编号更新到与其他机器一致。那么这里学习一个实例的网络延时代价是一个RTT。可能这个延迟看起来还不错,但是当新的数据仍然通过一个RTT的代价不断写入的时候,而落后的机器仍然以一个RTT来进行学习,这样会出现很难追上的情况。

这里需要改进,我们可以提前获取差距,批量打包进行学习,比如A机器Learner记录当前实例编号是x,B机器是y,而x < y,那么B机器通过通信获取这个差距,将(x,y]的chosen value一起打包发送给A机器,A机器进行批量的学习。这是一个很不错的方法。

但仍然不够快,当落后的数据极大,B机器发送数据需要的网络耗时也将变大,那么发送数据的过程中,A机器处于一种空闲状态,由于paxos另外一个瓶颈在于写盘,如果不能利用这段时间来进行写盘,那性能仍然堪忧。我们参考流式传输,采用类似的方法实现Learner的边发边学,B机器源源不断的往A机器输送数据,而A机器只需要收到一个实例最小单元的包体,即可立即解开进行学习并完成写盘。

具体的实现大概是先进行一对一的协商,建立一个Session通道,在Session通道里直接采用直塞的方式无脑发送数据。当然也不是完全的无脑,Session通过心跳机制进行维护,一旦Session断开即停止发送。

“我们参考流式传输,采用类似的方法实现Learner的边发边学,B机器源源不断的往A机器输送数据,而A机器只需要收到一个实例最小单元的包体,即可立即解开进行学习并完成写盘。”。这部分实际上封装在网络层,来看如何做到A机器接收到一个最小的实例单元:

int MessageEvent :: OnRead()
{
    if (m_iLeftReadLen > 0)
    {
        return ReadLeft();
    }
    
    int iReadLen = m_oSocket.receive(m_sReadHeadBuffer + m_iLastReadHeadPos, sizeof(int) - m_iLastReadHeadPos);
    if (iReadLen == 0)
    {
        BP->GetNetworkBP()->TcpOnReadMessageLenError();
           PLErr("read head fail, readlen %d, socket broken", iReadLen);
        return -1;
    }

    m_iLastReadHeadPos += iReadLen;
    if (m_iLastReadHeadPos < (int)sizeof(int))
    {
        PLImp("head read pos %d small than sizeof(int) %zu", m_iLastReadHeadPos, sizeof(int));
        return 0;
    }
    
    m_iLastReadHeadPos = 0;
    int niLen = 0;
    int iLen = 0;
    memcpy((char *)&niLen, m_sReadHeadBuffer, sizeof(int));
    iLen = ntohl(niLen) - 4;
    
    if (iLen < 0 || iLen > MAX_VALUE_SIZE)
    {
        PLErr("need to read len wrong %d", iLen);
        return -2; 
    }

    m_oReadCacheBuffer.Ready(iLen);

    m_iLeftReadLen = iLen;
    m_iLastReadPos = 0;
    
    //second read maybe no data read, so readlen == 0 is ok.
    bool bAgain = false;
    iReadLen = m_oSocket.receive(m_oReadCacheBuffer.GetPtr(), iLen, &bAgain);
    if (iReadLen == 0)
    {
        if (!bAgain)
        {
            PLErr("second read data fail, readlen %d, no again, socket broken", iReadLen);
            return -1;
        }
        else
        {
            PLErr("second read data, readlen %d need again", iReadLen);
            return 0;
        }
    }

    if (iReadLen == iLen)
    {
        ReadDone(m_oReadCacheBuffer, iLen);
        m_iLeftReadLen = 0;
        m_iLastReadPos = 0;
    }
    else if (iReadLen < iLen)
    {
        m_iLastReadPos = iReadLen;
        m_iLeftReadLen = iLen - iReadLen;

        PLImp("read buflen %d small than except len %d", iReadLen, iLen);
    }
    else
    {
        PLErr("read buflen %d large than except len %d", iReadLen, iLen);
        return -2;
    }

    return 0;
}

OnRead函数由以下两部分组成:

  • 首先读取数据包大小,这个过程可能需要分多次完成。
  • 读取指定数据包大小的数据,这部分也可能需要分多次完成。

当单个数据包读完,已获得完整的“最小实例单元”,通过ReadDone将数据包发往Node节点处理。

至于心跳,其实就是PhxPaxos中的Ack机制。在SendLearnedValue中,每发送一条记录需要执行一次CheckAck。如果检查失败,将终止发送。每条记录发送后,learner sender要求对端发送一个异步的ack请求,这个过程是异步的。CheckAck逻辑如下:

const bool LearnerSender :: CheckAck(const uint64_t llSendInstanceID)
{
    m_oLock.Lock();

    if (llSendInstanceID < m_llAckInstanceID)
    {
        m_iAckLead = LearnerSender_ACK_LEAD;
        PLGImp("Already catch up, ack instanceid %lu now send instanceid %lu", 
                m_llAckInstanceID, llSendInstanceID);
        m_oLock.UnLock();
        return false;
    }
    //如果当前发送的instance id和对端已确认的instance id差距超过了设定值
    while (llSendInstanceID > m_llAckInstanceID + m_iAckLead)
    {
        uint64_t llNowTime = Time::GetSteadyClockMS();
        uint64_t llPassTime = llNowTime > m_llAbsLastAckTime ? llNowTime - m_llAbsLastAckTime : 0;
         //同时,超过了设定的超时时间,提前终止
        if ((int)llPassTime >= LearnerSender_ACK_TIMEOUT)
        {
            BP->GetLearnerBP()->SenderAckTimeout();
            PLGErr("Ack timeout, last acktime %lu now send instanceid %lu", 
                    m_llAbsLastAckTime, llSendInstanceID);
            CutAckLead();
            m_oLock.UnLock();
            return false;
        }

        BP->GetLearnerBP()->SenderAckDelay();
        //PLGErr("Need sleep to slow down send speed, sendinstaceid %lu ackinstanceid %lu",
                //llSendInstanceID, m_llAckInstanceID);
        
        m_oLock.WaitTime(20);
    }

    m_oLock.UnLock();

    return true;
}

4.6 关于选中(chosen)

在整个讲解learner中,我们一直在强调选中(chosen)提案或者选中(chosen)值。这个过程提出以下疑问,并尝试解答。

问:learner如何知道某个提案是否被选中呢?
答:“4.2 选中通知”是通知各个learner值是否已被选中的一种常规方式。

问:如果某个值被选中后,提案发起节点异常,选中消息未发出会如何?
答:重新发起选举,新的被选中的提案编号不同,但提案值保持不变。

问:“4.2 选中通知”中,只更新了内存状态,在持久化的数据中,如何区分一个值的状态是accept还是chosen呢?
答: 如果当前的instance id为N,在N-1之前的所有提案值都是chosen的。当instance id为N的提案值可能是chosen状态、也可能是accept状态。

问:为何非chosen状态的数据也需要落盘
答:参见P2C的不变性,即Prepare和Accept阶段做过的承诺、接受过的值即便节点重启等异常情况下也需要保持不变。

问:我还有一个关于Instance类初始化的问题。
答:Instance类本章尚未涉及,留在第七章解答吧。

4.6 总结

经过第三、第四两章的讲解,paxos协议的算法实现已经分析完成。PhxPaxos并未对paxos做任何变种,甚至还做了一点简化。

下一节,我们将介绍paxos made simple中另外一个重要的概念:状态机。当然,这里提到的“简化”也会提及。


【转载请注明】随安居士. 4. PhxPaxos源码分析之Learner. 2017.11.15

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 198,082评论 5 464
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,231评论 2 375
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 145,047评论 0 327
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,977评论 1 268
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,893评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 47,014评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,976评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,605评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,888评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,906评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,732评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,513评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,980评论 3 301
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,132评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,447评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,027评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,232评论 2 339

推荐阅读更多精彩内容