sendRequestVote() 是 Candidate 端处理单个 RequestVote RPC 的函数。它负责向指定节点发送投票请求,并根据响应更新本地任期、累计票数,必要时把自己提升为 Leader。
它不是接收方的 RequestVote():
Candidate Follower
doElection()
|
| 创建 args、reply、votedNum
v
sendRequestVote(server, ...)
|
|---- RequestVote RPC ------> RequestVote(args, reply)
| 检查任期、日志、投票记录
|<----------- reply ----------
|
v
处理回复、累计选票、可能成为 Leader
源码将每个目标节点的 sendRequestVote() 放进独立线程并行执行。
函数参数
可以抽象成:
bool sendRequestVote(
int server,
shared_ptr<RequestVoteArgs> args,
shared_ptr<RequestVoteReply> reply,
shared_ptr<int> votedNum
);
各参数含义:
server 目标节点在 m_peers 中的下标
args 本轮选举的请求快照
reply 目标节点填写的响应
votedNum 本轮选举的共享计票器
args 中包含:
term 发起选举时的任期
candidateId Candidate 的节点编号
lastLogIndex Candidate 最后一条日志的索引
lastLogTerm Candidate 最后一条日志的任期
reply 主要包含:
term 接收节点当前看到的任期
voteGranted 是否同意投票
这正是 Raft 的标准 RequestVote 请求和响应结构。
一、执行网络调用
核心调用是:
bool ok =
m_peers[server]->RequestVote(args.get(), reply.get());
这里:
args.get() 取得请求对象的裸指针
reply.get() 取得响应对象的裸指针
shared_ptr 仍然负责对象生命周期,所以 RPC 在线程中执行时,参数和响应对象不会因为 doElection() 返回而被销毁。
尤其要区分:
ok == true RPC 通信成功并收到响应
reply->votegranted() 对方是否真的投票
因此 ok == true 完全可能同时满足:
reply->votegranted() == false;
对方可能成功收到请求,但因为已经投过票、日志不够新或请求任期过期而拒绝。
二、 RPC 失败时直接返回
if (!ok) {
return false;
}
RPC 失败可能意味着:
目标节点宕机
网络分区
请求丢失
响应丢失
连接超时
这个函数不会在内部无限重试。若最终无法获得多数票,electionTimeOutTicker() 会再次超时,启动一个更高任期的新选举。
这不会影响安全性;只要 Candidate 得不到多数票,它就不能成为 Leader。Raft 的可用性依赖多数节点能够相互通信。
三、 为什么网络调用期间不持锁
网络 RPC 可能长时间阻塞。如果发送前就持有 m_mtx:
RPC 等待几百毫秒
→ Raft 主锁也被占用几百毫秒
→ 无法处理心跳
→ 无法处理其他投票请求
→ 无法更新任期
因此该函数先执行网络调用,收到响应后才加锁:
std::lock_guard<std::mutex> lg(m_mtx);
这是典型的并发结构:
锁内创建请求快照
→ 锁外执行慢速网络操作
→ 锁内验证响应并修改状态
不过,释放锁意味着等待 RPC 时本地状态可能已经发生变化,所以处理响应时必须重新验证任期和角色。
四、 响应任期更高
if (reply->term() > m_currentTerm) {
m_status = Follower;
m_currentTerm = reply->term();
m_votedFor = -1;
persist();
return true;
}
例如:
自己当前任期:8
对方响应任期:10
这说明本节点已经落后。无论当前是 Candidate 还是 Leader,都必须:
切换为 Follower
currentTerm 更新为 10
清空当前任期投票记录
持久化 currentTerm 和 votedFor
放弃处理这张选票
Raft 的通用规则是:任何 RPC 请求或响应中出现更高任期,都要更新本地任期并转为 Follower。
这里不能因为:
reply->votegranted() == true
就继续计票。任期已经变化,旧选举立即失效。
五、 响应任期更低
else if (reply->term() < m_currentTerm) {
return true;
}
例如:
请求发出时:第8任期
等待过程中:本节点已经进入第9任期
返回响应: 第8任期
这个响应属于过去的一轮选举,必须丢弃。
这说明 sendRequestVote() 对应的线程可能还活着,但它代表的选举已经失效。判断任期可以防止旧 RPC 响应污染新任期。
六、 任期相等但拒绝投票
经过前两个分支后:
reply->term() == m_currentTerm
源码先进行断言,然后检查:
if (!reply->votegranted()) {
return true;
}
同一任期拒绝投票通常有两个原因:
1. 对方本任期已经投给其他 Candidate
2. 当前 Candidate 的日志不够新
"日志足够新"的比较顺序是:
先比较 lastLogTerm
任期相同再比较 lastLogIndex
Candidate 的日志只有至少和接收方一样新,才有资格获得选票。(raft.github.io)
注意函数仍然返回 true,因为返回值表达的是 RPC 是否成功,不是是否获得选票。
七、获得一张赞成票
*votedNum = *votedNum + 1;
votedNum 在 doElection() 中初始化为 1,因为 Candidate 已经投给自己。
假设有 5 个节点:
初始:自己的一票,votedNum = 1
节点B同意: votedNum = 2
节点C同意: votedNum = 3
多数票计算为:
m_peers.size() / 2 + 1
对于不同规模:
3 个节点需要 2 票
5 个节点需要 3 票
7 个节点需要 4 票
虽然多个线程共享普通 int,但票数的读取和修改都发生在 m_mtx 的保护下,所以这里不会出现两个线程同时覆盖计票结果。
八、达到多数票后成为 Leader
if (*votedNum >= m_peers.size() / 2 + 1) {
*votedNum = 0;
m_status = Leader;
...
}
获得多数票后,这一轮选举已经成功。Raft 只要求多数节点同意,不需要等待所有节点响应。
源码把票数设为 0,目的是避免后续迟到的赞成票再次触发晋升逻辑。不过更清晰的设计通常是维护:
bool electionWon;
或者检查:
if (m_status != Candidate) {
return true;
}
九、 初始化 Leader 的复制状态
成为 Leader 后初始化:
m_nextIndex[i] = lastLogIndex + 1;
m_matchIndex[i] = 0;
含义是:
nextIndex[i]
下一次准备发送给节点 i 的日志索引
matchIndex[i]
已知节点 i 成功复制的最高日志索引
如果 Leader 最后一条日志索引是 10:
nextIndex[i] = 11
Leader 会先假设 Follower 已经拥有前面的日志,然后从索引 11 开始尝试;如果 AppendEntries 返回日志不匹配,再逐步回退。Raft 规定 Leader 当选后重新初始化这两个易失状态。
十、立即发送第一次心跳
源码创建一个新线程调用:
Raft::doHeartBeat()
新 Leader 不等待下一个心跳周期,而是立即广播 AppendEntries:
向其他节点宣布 Leader 身份
让其他 Candidate 退回 Follower
重置 Follower 的选举计时器
开始日志同步
线程创建时 sendRequestVote() 还持有 m_mtx,所以新线程进入 doHeartBeat() 后会暂时阻塞;当前函数释放锁后,它才能正式发送心跳。