目录
这篇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♪(・ω・)ノ