C++实现Raft算法之选Leader

目录

electionTimeOutTicker

doElection

sendRequestVote

RequestVote


这篇blog是我自己学习之后的总结,记录了学习过程中的思考,不足之处请指出,文中不介绍Raft类的成员变量,有需要请移步其他blog,前置概念在同专栏下有补充

选举涉及4个重要函数,函数之间关系如下:

electionTimeOutTicker

只做一件事,就是判断超时没超时,更严谨的说是负责查看是否发起选举,如果发起选举就执行doElection,这里先只关注electionTimeOutTrick。

如何判断超时没呢?

会有一个getRandomizedElectionTimeout()函数返回一个随机的选举超时时间,

getRandomizedElectionTimeout() + m_lastResetElectionTime - nowTime得到一个合适的睡眠时间,如果睡眠时间大于1ms,就进入睡眠,如果小于1ms,可以直接判断超时与否。

对于以上判断超时我需要补充,比如说为什么要随机函数,这个的目的是为了防止选举冲突,如果getRandomizedElectionTimeout()节点都一样的话,那么超时发起选举的这个行为可能很多节点同时发起并从follower变成candidate。

代码如下:

auto nowTime = now(); //睡眠前记录时间
auto suitableSleepTime = getRandomizedElectionTimeout() + m_lastResetElectionTime - nowTime;

这个计算合适睡眠时间的行为必须单线程,所以用lock保护,给以上代码加上mtx.lock(),最后一行加上mtx.unlock()

设置好了选举超时时间,接下来就判断了,是否超时,m_lastResetElectionTime是最后一次选举的时间,如果m_lastResetElectionTime - nowTime的时差大于0,意味着m_lastResetElectionTime比nowTime更晚,意味着选举计时器被重置了,这种情况下没有超时发生,将继续睡眠。

如果m_lastResetElectionTime - nowTime的时差没有大于0,执行doElection()函数。

void Raft::electionTimeOutTicker() {
    while (true) {
        m_mtx.lock();
        auto nowTime = now(); //睡眠前记录时间
        auto suitableSleepTime = getRandomizedElectionTimeout() + m_lastResetElectionTime - nowTime;
        m_mtx.unlock();
        if (suitableSleepTime.count() > 1) {
            std::this_thread::sleep_for(suitableSleepTime);
        }

        if ((m_lastResetElectionTime - nowTime).count() > 0) {  //说明睡眠的这段时间有重置定时器,那么就没有超时,再次睡眠
            continue;
        }
        doElection();
    }
}

doElection

doElection实现了选举逻辑,注意是选举逻辑,不是选举的具体过程,也不是实现了投票机制

首先使用lock_guard保护锁,为什么不使用mtx.lock和mtx.unlock呢?因为lock_guard可以当执行出现异常或者达到作用域末尾时,锁自动释放而无需手动。

接下来,检查节点的状态,因为这里是发起选举,如果是leader,就别发了

不是leader的话,第一步是把follower变成candidate,接下来还要改变什么?

还要改变任期号!因为 '我' 发起选举,比其他还没参与的节点多一轮,增加当前的任期号,表示开始新一轮的选举!

此外 '我' 还得给自己投一票,为啥给自己投,其实就相当于一个 '初始化' 的过程,保证了自己至少有一票, 给自己投票之后,再号召大家都给 '我' 投,接下来检查票数是否过过半

以上逻辑实现代码如下:

lock_guard<mutex> g(m_mtx);
if (m_status != Leader) {        
    m_status = Candidate;
    m_currentTerm += 1;
    m_votedFor = m_me; 
    persist();
    std::shared_ptr<int> votedNum = std::make_shared<int>(1); 

persist是持久化当前的状态到存储系统,这是为了在发生故障时能够从最后一次已知的状态恢复

votedNum是一个投票计数器,这里表示初始化为1,意味着自己给自己投了一票,考虑到线程安全,必须设置为智能指针的形式,在doElection函数中,Raft候选人需要向集群中其他节点并行发送请求投票的RPCs,每个这样的请求可能由不同的线程处理,并且这些线程需要更新和访问共同的votedNum计数器来追踪已收到的投票数,而使用的shared_ptr可以安全地在多个线程之间共享votedNum,而无需担心对象生命周期管理问题。

初始化结束之后,下面就要遍历所有节点了,每一个节点都要号召其他节点给自己投票!

在此之前,先重置一下m_lastResetElectionTime = nowTime,更新一轮新的选举

接下来,遍历每一个m_peers,如果是自身节点就跳过,此外最最最最重要的是构建RequestVoteArgs和RequestVoteReply,RequestVoteArgs封装了数个参数,比如term,candidate的id,LastLogIndex和LastLogTerm。而RequestVoteReply用于接收每个节点对投票请求的响应,这些响应包含信息,如:是否同意投票(votedGranted)以及响应的任期号(term),用于候选人检查是否需要更新自己的任期。

以上逻辑是是doElection的全部,完整代码如下:

void Raft::doElection() {
    lock_guard<mutex> g(m_mtx);
    if (m_status != Leader) {
        m_status = Candidate;
        m_currentTerm += 1;
        m_votedFor = m_me; 
        persist();
        std::shared_ptr<int> votedNum = std::make_shared<int>(1); 

        m_lastResetElectionTime = now();

        for (int i = 0; i < m_peers.size(); i++) {
            if (i == m_me) {
                continue;
            }
            int lastLogIndex = -1, lastLogTerm = -1;
            getLastLogIndexAndTerm(&lastLogIndex, &lastLogTerm);//获取最后一个log的term和下标
            std::shared_ptr<mprrpc::RequestVoteArgs> requestVoteArgs = std::make_shared<mprrpc::RequestVoteArgs>();
            requestVoteArgs->set_term(m_currentTerm);
            requestVoteArgs->set_candidateid(m_me);
            requestVoteArgs->set_lastlogindex(lastLogIndex);
            requestVoteArgs->set_lastlogterm(lastLogTerm);
            std::shared_ptr<mprrpc::RequestVoteReply> requestVoteReply = std::make_shared<mprrpc::RequestVoteReply>();

            std::thread t(&Raft::sendRequestVote, this, i, requestVoteArgs, requestVoteReply,
                          votedNum); 
            t.detach();
        }
    }
}

Q:RequestVoteReply是如何得到term和votedGranted的?

A:好问题!有点复杂,听我步步拆解

doElection函数其实进行了RequestVote的调用(不知道RequestVote什么意思的去看开头图片),但没有直接调用,而是先调用sendRequestVote,在sendRequestVote中调用了RequestVote,RequestVote实现了具体的Reply。你先了解这个逻辑,接下来在慢慢讲sendRequestVote和RequestVote。

我先讲一下最后两行线程的创建与分离:

使用Raft::thread来异步执行投票请求是提高效率和响应性的常见做法

通常创建一个新的线程来执行sendRequestVote方法,并随后将这个线程分离,允许它独立于创建它的线程运行

sendRequestVote

负责选举中的投票逻辑!

首先需要调用RequestVote,这很重要,因为RequestVote实现的是 其他节点收到 '我' 的拉票之后,决定是否给我投票 这个逻辑,并且能得到响应的term和是否同意投票votedGranted

代码如下:

bool ok = m_peers[server]->RequestVote(args.get(),reply.get());
if (!ok) {
    return ok; // RPC通信失败就立即返回,避免资源消耗
}

ok显示的是RPC调用成功或者失败的状态,简单地说就是RequestVote调用成功与否的状态,如果因为网络问题或者节点不可达等问题导致调用失败,函数就需要提前返回

接下来是老规矩,lock_guard管理互斥锁,因为下面需要实现投票的逻辑了,必须保证安全。

投票首先比较任期号,任期号最大的才会是leader,任期号小的不可能,比较是基于RequestVote中收到的回复(reply)和当前节点任期号(m_currentTern)之间的。

假如收到的回复比当前任期号大,那 '我' 自然就不可能当leader了,那么我就连candidate都不当了,由candidate退为follower,同时任期号更新,确保与集群一致,votedFor改成-1,一开始votedFor '我' 投给了自己,现在自己不可能了,先置为-1,:

if(reply->term() > m_currentTerm){
    m_status = Follower;
    m_currentTerm = reply->term();
    m_votedFor = -1;
    persist();
    return true;
}

假如收到的回复比当前任期号小

那么 '我' 就有可能成为leader,但是也有可能集群中还有比 '我' 的任期号更大的节点,还不好说,所以一切先保持原样

else if (reply->term() < m_currentTerm) {
    return true;
}

假如收到的回复与当前任期号相等

那么到底能否获得投票呢?且看RequestVote函数返回的votegranted是否为true,别急马上就将votedgranted了,如果votedgranted为true,那么!reply->votegranted()就为false,那么就会执行votedNum+1的操作,如果votedgranted为false,那么!reply->votegranted()就为true,那么会return出去

if(!reply->votegranted()){
    return true;
}

*votedNum = *votedNum + 1;

现在三种任期号的情况都判断完了,接下来就要计算获得的票数是否过半了,如果过半的话,票数先清空,接着状态改成leader,然后就是日志同步,我在代码块里以问答的形式给出这部分的讲解,最后启动心跳线程,领导者通过定期发送心跳来维护其权威和防止选举超时。

if (*votedNum >=  m_peers.size()/2+1) {
    *votedNum = 0;
    m_status = Leader;
    int lastLogIndex =   getLastLogIndex();
    for (int i = 0; i <m_nextIndex.size()  ; i++) {
         m_nextIndex[i] = lastLogIndex + 1 ;
         m_matchIndex[i] = 0;     
    }
    std::thread t(&Raft::doHeartBeat, this); 
    t.detach();
    persist();
}

Q:nextIndex和matchIndex有什么作用?

A:他们用于管理领导者与每个跟随者之间的日志复制状态。帮助维护和跟踪日志条目的复制进度,并决定合适可以安全地提交日志条目。

nextIndex数组的每个元素nextIndex[i]表示领导者准备发送给第 i 个跟随者的下一个日志条目的索引。当领导者刚刚当选时,它假设所有的跟随者的日志都是最新的,所以初始化每个nextIndex[i]为领导者日志的最后索引+1,即getLastLogIndex()+1

matchIndex数组的每个元素matchIndex[i]表示第i个跟随者已经成功复制的最高日志条目索引。领导者通过检查matchIndex来确定是否有日志条目已被集群的大多数节点复制。如果存在某个索引n,使得大多数matchIndex的值大于或等于n,并且该索引条目的任期等于当前任期,领导者就可以提交该索引及之前所有的日志条目。

RequestVote

激动人心的RequestVote来了!RequestVote主要用来接收别人发来的选举请求,检验是否要给对方投票

第一步加锁和持久化是必须的,使用lock_guard加锁,Defer持久化

接下来,还是任期对比,和sendRequestVote很相似,但是不一样,先听我把区别讲了

  • RequestVote 函数:这个函数是在接收到其他节点的投票请求时被调用的。其核心目的是响应外部的请求,并作出是否授予投票的决定。

    • 若请求的任期 小于 当前任期:这表明发起请求的节点落后于当前节点。当前节点拒绝投票并通知请求者其任期已过时,以促使其更新状态。
    • 若请求的任期 大于 当前任期:这表明当前节点落后于集群的最新状态。当前节点需要更新自己的任期以匹配集群的状态,转变为跟随者,并重置自己的投票状态(因为新任期尚未投票)。
  • sendRequestVote 函数:这个函数处理的是发送到其他节点的投票请求。其核心目的是作为候选人寻求其他节点的支持以成为领导者。

    • 若响应的任期 大于 候选人的任期:这表明存在一个更高任期的节点,候选人必须立即停止选举活动,更新任期,转变为跟随者状态。这保证了节点遵循最新的领导者或更高任期的指令。
    • 若响应的任期 小于 或等于候选人的任期:候选人可以继续自己的选举过程。如果响应授予了投票,候选人将这一票计入总票数;否则,只要响应任期不大于当前任期,候选人的状态不改变。

其实区别显而易见...(那你bb这么多!)

总得来说,sendRequestVote任期对比目的就是一个,看能不能投票,大于或者小于都不会投票

而RequestVote任期对比也只有一个,能不能投票,能就true,为什么这里出现两个任期判断,因为,在处理 RPC 请求和响应时,必须首先检查 term(任期),因为不同的节点角色(Leader、Follower、Candidate)对 AppendEntries 请求的反应不同

如果请求中的任期小于当前节点的任期,那么说明请求的是一个过时的候选人,那么拒绝投票,将当前任期返回给请求者,特别是votedgranted设置为false,代表拒绝投票

如果请求中的任期大于当前节点的任期,那么说明存在一个比自己还要新的任期。当前节点应该更新其任期,转变为追随者,重置投票信息!能投给他吗?不能!!在sendRequestVote里面是一样的逻辑!你怎么知道还没有更加新的?!

lock_guard<mutex> lg(m_mtx);

Defer ec1([this]() -> void { //应该先持久化,再撤销lock
    this->persist();
});
if (args->term() < m_currentTerm) {
    reply->set_term(m_currentTerm);
    reply->set_votestate(Expire);
    reply->set_votegranted(false);
    return;
}
if (args->term() > m_currentTerm) {
    m_status = Follower;
    m_currentTerm = args->term();
    m_votedFor = -1;
}

如果请求中的任期等于当前节点的任期,那么开始检查日志!!!!!!!!!这才是完整的逻辑,全放在这儿了!!!!!!!!!!!!!!!

此时的节点任期都是相同的了,任期小的也已经更新到新的args的term了!

接下来检查log的term和index是不是匹配

如果candidate的日志的新的程度 >= 接受者的日志新的程度,就可以授票

int lastLogTerm = getLastLogIndex();
if (!UpToDate(args->lastlogindex(), args->lastlogterm())) {

    //日志太旧了,votestate为Voted表示已经投过了,votedgranted为false表示不给他投
    reply->set_term(m_currentTerm);
    reply->set_votestate(Voted);
    reply->set_votegranted(false);
    return;
}

如果网络质量不好的话就会重复发,因此需要避免重复发

如果m_votedFor不是-1,也不是自己,那么一定是已经投过票了

if (m_votedFor != -1 && m_votedFor != args->candidateid()) {
    reply->set_term(m_currentTerm);
    reply->set_votestate(Voted);
    reply->set_votegranted(false);
    return;
}

同意投票:

else {
    //同意投票
    m_votedFor = args->candidateid();
    m_lastResetElectionTime = now();//认为必须要在投出票的时候才重置定时器,
    reply->set_term(m_currentTerm);
    reply->set_votestate(Normal);
    reply->set_votegranted(true);
    return;
}

对于这篇blog大家如果有疑问,可以给我留言,如果有错误,也可以指出,感谢Thanks♪(・ω・)ノ

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值