终于进入了正题lab3,这个实验主要就是根据raft的 论文 ,实现raft的共识算法。raft在该实验中以go的对象类型实现为后续实验提供服务,该服务能够在多个不可靠的服务器之间以相同顺序安全地复制日志。
具体的raft细节可以看论文和b站up戌米的 视频
该lab把整个raft分为了四个部分,3A主要就是实现第一部分投票选举。写这个博客的时候我已经写到了3D(之前忘了写了),所以其中不可避免的会出现一些3A不需要的代码。
在实现的时候,非常重要的一件事就是看一下lab中助教写的 手册 ,可以避免很多bug。
具体的选举内容
这一部分主要是AI生成的,用来以后复习的时候看一下。Raft 算法的 Leader 选举过程是一个为了在分布式系统中选出唯一领导者,以保证数据一致性的核心机制。其设计目标是易于理解、安全可靠。整个过程主要依赖于任期(Term)、心跳超时(Election Timeout) 和 多数投票(Majority Vote) 三个关键概念。
以下是 Raft Leader 选举的详细步骤:
1. 节点角色与初始状态
- Follower (跟随者):被动角色,等待接收来自 Leader 的心跳或日志复制请求。所有节点启动时都处于此状态。
- Candidate (候选人):由 Follower 在特定条件下转变而来,主动发起选举,参与竞选 Leader。
- Leader (领导者):集群中的“主节点”,负责处理所有客户端写请求,并向所有 Follower 发送心跳和复制日志。
2. 选举触发条件
选举通常由以下情况触发:
- 初始启动:集群首次启动,没有 Leader。
- Leader 失效:当前 Leader 崩溃、网络分区或过载,导致无法正常工作。
- 心跳丢失:一个 Follower 在预设的 选举超时时间 (Election Timeout) 内没有收到来自 Leader 的任何消息(主要是心跳
AppendEntriesRPC)。
关键设计:每个 Follower 的选举超时时间是一个随机值(例如 150-300 毫秒)。这个随机化设计至关重要,它能有效避免多个 Follower 同时发现心跳丢失并同时转为 Candidate,从而大大降低“选票分裂”(Split Vote)的风险。
3. 选举过程步骤
当一个 Follower 因心跳超时而决定发起选举时,会经历以下流程:
- 自我提升为 Candidate:
- 该节点将自己的状态从
Follower切换为Candidate。 - 将自己的任期号 (Term) 加 1(
currentTerm++)。这标志着一个新选举轮次的开始。 - 给自己投一票(
voteFor = self)。
- 该节点将自己的状态从
- 发起投票请求 (RequestVote RPC):
- 新的 Candidate 会立即向集群中的所有其他节点并行发送
RequestVote远程调用(RPC)。 - 该请求包含以下信息:
- 自己的新任期号 (
term)。 - 自己最后一条日志的索引 (
lastLogIndex) 和任期号 (lastLogTerm)。
- 自己的新任期号 (
- 新的 Candidate 会立即向集群中的所有其他节点并行发送
- 其他节点的投票决策 (Follower 的响应):
- 收到
RequestVote请求的 Follower 会根据以下规则决定是否投票:- 规则 1:任期检查:如果请求中的
term小于自己当前的currentTerm,则拒绝投票。因为这意味着这是一个过期的请求。 - 规则 2:单次投票:如果该 Follower 在当前任期内已经给其他 Candidate 投过票了,则拒绝给新的请求者投票。一个 Follower 在一个任期内只能投一次票。
- 规则 3:日志完整性检查 (Log Integrity):这是确保数据安全的关键!只有当 Candidate 的日志“至少和自己一样新”时,才会投票。判断标准是:
- 比较双方
lastLogTerm,lastLogTerm更大的日志更新。 - 如果
lastLogTerm相同,则lastLogIndex更大的日志更新。 - 只有当 Candidate 的日志不比自己旧时,才允许投票。
- 比较双方
- 规则 1:任期检查:如果请求中的
- 收到
- 选举结果判定 (Candidate 的等待):
- Candidate 发出投票请求后,会等待其他节点的回复。
- 成功当选:如果 Candidate 收到了超过半数 (majority, 即 N/2 + 1) 节点的投票(包括自己的一票),那么它就赢得了选举,立即成为新的 Leader。
- 失败或平局:如果出现以下情况,本轮选举失败:
- 没有获得多数票(例如,在 5 节点集群中,两个 Candidate 各得 2 票,形成平局)。
- 在等待期间收到了另一个 Term 更高的节点发来的心跳(
AppendEntriesRPC),这表明已经有新的 Leader 产生。
- 当选举失败时,Candidate 会放弃本次竞选,将自身状态回退为 Follower,并等待下一次选举超时的到来。
- 新 Leader 上任:
- 一旦成为 Leader,它会立即向所有其他节点周期性地发送心跳消息(一种不包含日志条目的
AppendEntriesRPC)。 - 心跳的作用是:
- 向所有 Follower 宣告自己的“存活”状态。
- 阻止 Follower 因心跳超时而再次发起新的选举,从而维持自身的领导地位。
- 一旦成为 Leader,它会立即向所有其他节点周期性地发送心跳消息(一种不包含日志条目的
相关代码
选举所需的字段
|
|
根据论文的figure 2,我们的rpc结构体如下:
|
|
由于3A不需要具体的日志,所以当前可以LastLogIndex和LastLogTerm设为0即可。
选举代码
|
|
首先是ticker,虽然实验的说明里说可以用time.sleep来实现,我最初也是通过sleep和管道来实现的,如果接受到心跳就直接continue,没接收到sleep结束后就会开始选举。之后查了下timer的使用方法,感觉timer更灵活一点,可以每次切换状态的时候直接重置。
|
|
这个按照论文的要求来写即可,其中非常重要的一点就是在接收到reply的时候,一定要检查args.Term == rf.currentTerm,因为有可能因为网络延时等问题导致服务器收到了旧任期的rpc回复。这个在手册中有说明。
From experience, we have found that by far the simplest thing to do is to first record the term in the reply (it may be higher than your current term), and then to compare the current term with the term you sent in your original RPC.
|
|
这个比较容易出错的点在于投票后一定要立即重置超时选举。
一些思考
- 为什么要选举投票要比较lastLogIndex,而不是commitIndex? 因为其实commitIndex本质上是落后的,有可能Leader(为A,假如我们有三个节点,A,B,C)在成功将某一个日志复制到大多数(现在A,B都有了)但还没来得及提交时崩溃,之后有些没有获得该日志的节点(C)成为Leader(因为如果比较的是commitIndex,C没有最新的lastLogIndex,但commitIndex和A,B一样新),导致这条复制到大多数的最新的日志丢失。