3.1 选主流程
流程详解
流程概览
节点在选举超时时间(
election_timeout_ms)内未收到任何心跳而触发选举向所有节点广播
PreVote请求,若收到大多数赞成票则进行正式选举,否则重新等待选举超时将自身角色转变为 Candidate,并将自身
term加一,向所有节点广播RequestVote请求在投票超时时间(
vote_timeout_ms)内若收到足够多的选票则成为 Leader;若有收到更高term的响应则转变为 Follower 并重复步骤 1;否则等待投票超时后转变为 Follower 并重复步骤 2成为 Leader
5.1 将自身角色转变为 Leader
5.2 为所有 Follower 创建
Replicator,并对所有 Follower 定期广播心跳5.3 通过发送空的
AppendEntries请求来确定各 Follower 的nextIndex5.4 将之前任期的日志全部复制给 Follower(只复制不提交,不更新 commitIndex)
5.5 通过复制并提交一条本任期的配置日志来提交之前任期的日志,提交后日志就可以 apply了
5.6 调用用户状态机的
on_apply来回放之前任期的日志,以恢复状态机5.7 待日志全部回放完后,调用用户状态机的
on_configuration_committed来应用上述配置5.8 调用用户状态机的
on_leader_start
至此,Leader 可以正式对外服务
上述流程可分为 PreVote (1-2)、RequestVote (3-4)、成为 Leader(5-6)这三个阶段
投票规则
相关 RPC:
在同一任期内,节点发出的 PreVote 请求和 RequestVote 请求的内容是一样的,区别在于:
PreVote请求中的term为自身的term加上 1而发送
RequestVote请求前会先将自身的term加 1,再将其作为请求中的term
节点对于 RequestVote 请求投赞成票需要同时满足以下 3 个条件,而 PreVote 只需满足前 2 个条件:
term:候选人的
term要大于等于自己的termlastLog:候选人的最后一条日志要和自己的一样新或者新于自己
votedFor:自己的
votedFor为空或者等于候选人的 ID
PreVote 与 RequestVote 的差异:
处理
RequestVote请求时会记录votedFor,确保在同一个任期内只会给一个候选人投票;而PreVote则可以同时投票给多个候选人,只要其满足上述的 2 个条件处理
RequestVote请求时若发现请求中的term比自身的大,会step_down成 Follower,而PreVote则不会,这点可以确保不会在PreVote阶段打断当前 Leader
从以上差异可以看出,PreVote 更像是一次预检,检测其连通性和合法性,并没有实际的动作。
日志新旧比较
日志由
term和index组成,对于 2 条日志a和b来说:
若其
term和index都一样,则 2 条日志一样新若
(a.term > b.term) || (a.term == b.term && a.index > b.index),则日志a新于日志b
votedFor
每个节点都会记录当前 term 内投给了谁(即 votedFor),如果在相同 term 内已经投过票了,就不会再投票给其他人,这是确保在同一任期内只会产生一个 Leader 的关键。
除此之外,Raft 会将 term 与 votedFor 进行持久化,防止在投给某一个候选人后节点 Crash,重新启动后又投给了另一候选人。 在具体实现方面,节点会先持久化 term 与 votedFor,再向候选人返回赞成响应。
幽灵日志

上述选举流程中提到,Leader 并不是通过 Quorum 机制来提交之前任期的日志,而是通过提交本任期的一条日志,顺带提交上一任期的日志。这主要是为了解决 Raft 论文在 5.4 Safety 一节中提到的幽灵日志问题,因为该问题会破坏系统的线性一致性,正如上图所示:
(a):
S1当选term 2的 Leader,并将日志复制给S2,之后 Crash(b):
S5被S3,S4,S5选为term 3的 Leader,在本地写入一条日志后 Crash(c):
S1被S1,S2,S3选为term 4的 Leader,并将index=2的日志复制给S3,达到Quorum并应用到状态机;在本地写入一条日志,然后 Crash(d1):
S5被S2,S3,S4,S5选为term 5的 Leader,并将index=2的日志复制给所有节点,从而覆盖了原来的日志。
从上面流程可以看到,在 (c) 时刻 index=2 的日志即使被提交了,但在 (d1) 时又被覆盖了。如果我们在 (c) 时刻去 Leader S1 读取 x 的值,得到的将是 1,之后我们又在 (d1) 时刻去 Leader S5 读取 x 的值,得到的将是 0,这明显违背了线性一致性。
所以论文里提出不能通过 Quorum 机制提交之前任期的日志,而是需要通过提交本任期的一条日志,顺带提交上一任期的日志,正如 (d2) 所示。一般 Raft 实现会在节点当选 Leader 后提交一条本任期的 no-op 日志,而 braft 中提交的是本任期的配置日志,这主要是在实现上和节点配置变更的特性结合到一起了,但其起到的作用是一样的,只要是本任期内的日志都能解决幽灵日志问题,具体实现见以下提交 no-op 日志。
特别需要注意的是,以上的读操作是指除
Raft Log Read之外的其他读取方式,因为对于Raft Log Read来说,其读操作就是一条本任期的日志。
相关接口
一些会在选举过程中调用的状态机接口:
阶段一:PreVote

触发投票
节点在启动时就会启动选举定时器:
待定时器超时后就会调用 pre_vote 进行 PreVote:
发送请求
在 pre_vote 函数中会向所有节点发送 PreVote 请求,并设置 RPC 响应的回调函数为 OnPreVoteRPCDone,最后调用 grant_slef 给自己投一票后,就等待其他节点的 PreVote 响应:
处理请求
其他节点在收到 PreVote 请求后会调用 handle_pre_vote_request 处理请求:
处理响应
在收到其他节点的 PreVote 响应后,会调用之前设置的回调函数 OnPreVoteRPCDone->Run(),在回调函数中会调用 handle_pre_vote_response 处理 PreVote 响应:
处理响应,如果在处理响应后发现收到的选票数已达到 Quorum,则调用 elect_self 进行正式选举:
阶段二:RequestVote

发送请求
当 PreVote 阶段获得大多数节点的支持后,将调用 elect_self 正式进 RequestVote 阶段。在 elect_self 会将角色转变为 Candidte,并加自身的 term 加一,向所有的节点发送 RequestVote 请求,最后给自己投一票后,就等待其他节点的 RequestVote 响应:
request_peers_to_vote 负责向所有节点发送 RequestVote 请求:
处理请求
节点在收到 RequestVote 请求后,会调用 handle_request_vote_request 处理 RequestVote 请求:
处理响应
候选人在收到其他节点的 RequestVote 响应后,会调用之前设置的回调函数 OnRequestVoteRPCDone->Run(),在回调函数中会调用 handle_request_vote_response 处理 RequestVote 响应:
处理响应,如果在处理响应后发现收到的选票数已达到 Quorum,则调用 become_leader 成为 Leader:
投票超时
VoteTimer 在节点开始正式投票(即调用 elect_self)就开始计时了。若在投票超时时间内未收到足够多的选票,VoteTimer 就会调用 handle_vote_timeout 将当前节点 step_down 并进行重新 PreVote;反之在收到足够多选票成为 Leader 后将会停止该 Timer:
阶段三:成为 Leader

成为 Leader
节点在 RequestVote 阶段收到足够多的选票后,会调用 become_leader 正式成为 Leader,在该函数中主要执行成为 Leader 前的准备工作,特别需要注意的是,只有当这些工作全部完成后才会回调用户状态机的 on_leader_start,每一项工作将在下面的小节中逐一介绍:
创建 Replicator
Leader 会为每个 Follower 创建对应 Replicator,并将其启动。每个 Replicator 都是单独的 bthread,它主要有以下 3 个作用:
记录 Follower 的一些状态,比如
nextIndex、flyingAppendEntriesSize等作为 RPC Client,所有从 Leader 发往 Follower 的 RPC 请求都会通过它,包括心跳、
AppendEntriesRequest、InstallSnapshotRequest同步日志:
Replicator会不断地向 Follower 同步日志,直到 Follower 成功复制了 Leader 的所有日志后,其会在后台等待新日志的到来
调用 Replicator::start 来创建 Replicator,并将其启动:
定时发送心跳
Replicator 调用 _start_heartbeat_timer 启动心跳定时器,其每隔一段时间会发送 ETIMEDOUT 状态码,而 Replicator 收到该状态码后,会调用 _send_heartbeat 发送心跳。
调用 _start_heartbeat_timer 启动心跳定时器,心跳间隔时间在节点初始化时通过 heartbeat_timeout 函数算得:
定时器会每隔一段时间向 Replicator 发送 ETIMEDOUT 状态码:
Replicator 收到该状态码后,会调用 _send_heartbeat 发送心跳:
确定 nextIndex
Leader 通过发送空的 AppendEntries 请求来探测 Follower 的 nextIndex, 只有确定了 nextIndex 才能正式向 Follower 发送日志。这里忽略了很多细节,关于 nextIndex 的作用和匹配算法,以及相关实现可参考 4.1 日志复制中的相关内容:
复制之前任期日志
上述我们已经为每一个 Follower 创建了 Replicator,并且确认了每个 Follower 的 nextIndex,这时候 Replicator 通过 nextIndex 判断 Follower 日志还落后于 Leader,将自动向 Follower 同步日志,直至与 Leader 对齐为止。
注意,这些日志只复制并不提交。通常情况下,Leader 每向一个 Follower 成功复制日志后,都会调用 BallotBox::commit_at 将对应日志的投票数加一,当投票数达到 Quorum 时,Leader 就会更新 commitIndex,并应用这些日志。
节点在刚成为 Leader 时通过调用以下函数将第一条可以提交的 logIndex (即 _pending_index)设为了 Leader 的 lastLogIndex+1:
在调用 commit_at 函数时,只有 logIndex>=_pending_index 的日志才能被提交:
这里可能忽略了一些日志复制的细节,你可以参考 4.1 日志复制中的相关内容。
提交 no-op 日志
一般 Raft 实现会在节点当选 Leader 后提交一条本任期的 no-op 日志,而 braft 中提交的是本任期的配置日志。在节点刚成为 Leader 时会调用 ConfigurationCtx::flush 复制并提交配置日志:
ConfigurationCtx::flush 会调用 unsafe_apply_configuration 函数来做以下几件事:
on_apply
on_configuration_committed
上面已经提到将配置日志交给 LogManager 进行复制,待其复制达到 Quorum 后,才会更新 commitIndex,并会调用 FSMCaller::do_committed 开始应用日志,参见上述的 BallotBox::commit_at 函数。
当然应用的日志包括之前任期的日志和本任期的配置日志。如果日志类型是配置,则调用状态机的 on_configuration_committed,否则回调 on_apply:
on_leader_start
在配置日志被应用(即调用 on_configuration_committed)后,其会调用其回调函数 ConfigurationChangeDone::Run(),在该函数中会调用状态机的 on_leader_start 开启 Leader 任期:
Last updated