一、实验说明
https://pdos.csail.mit.edu/6.824/labs/lab-raft.html
二、梳理
在本实验中,需要实现Raft。
Raft 是一种为了管理复制日志的一致性算法。复制服务通过将其状态(即数据)的完整副本存储在多个副本服务器上来实现容错功能。即使服务的某些服务器出现故障(崩溃,网络故障或不稳定),复制也可以使服务继续运行。挑战在于,故障可能导致副本持有不同的数据副本。
Raft管理服务的状态副本,特别是它可以帮助服务找出故障后正确的状态。Raft实现了一个复制状态机。它将客户请求组织成一个序列,称为日志,并确保所有副本都同意日志的内容。每个副本都按照它们在日志中出现的顺序执行日志中的客户端请求,并将这些请求应用于服务状态的副本本地副本。由于所有活动复本都看到相同的日志内容,因此它们都以相同的顺序执行相同的请求,因此继续具有相同的服务状态。如果服务器出现故障但后来又恢复了,Raft会确保其日志为最新状态。只要至少大多数服务器都处于活动状态并且可以相互通信,Raft将继续运行。
Raft特性:
- 强领导者:和其他一致性算法相比,Raft 使用一种更强的领导能力形式。比如,日志条目只从领导者发送给其他的服务器。这种方式简化了对复制日志的管理并且使得 Raft 算法更加易于理解。
- 领导选举:Raft 算法使用一个随机计时器来选举领导者。这种方式只是在任何一致性算法都必须实现的心跳机制上增加了一点机制。在解决冲突的时候会更加简单快捷。
- 成员关系调整:Raft 使用一种共同一致的方法来处理集群成员变换的问题,在这种方法下,处于调整过程中的两种不同的配置集群中大多数机器会有重叠,这就使得集群在成员变换的时候依然可以继续工作。
Raft定义
Raft三种状态,分别为
Follower
- 接收并处理心跳包
- 接收并处理选举
- 超时未收到心跳包,变为Candidate
Candidate
- 接收并处理心跳包
- 接收并处理选举
- 处理超时未收到心跳包
- 发起投票,成功变为Leader,变为Follower
Leader
- 接收并处理心跳包
- 接收并处理选举
- 发送心跳包,失败变为Follower
基于以上,需要实现两个周期性的方法:
- 作为Candidate时,发起投票的 startElection(定义
electionTimer,收到心跳包从新计时,到期发起投票
) - 作为Leader时,发送心跳包broadcastHeartbeat(定义
heartbeatTimer,,到期发送心跳包
)
实现两个方法:
- 对应投票的
RequestVote
- 对应心跳包的
AppendEntries
。
Part 2A
状态和timer
const (
Follower int32 = 0
Candidate int32 = 1
Leader int32 = 2
HeartbeatInterval = time.Duration(120) * time.Millisecond
ElectionTimeoutMin = time.Duration(300) * time.Millisecond
ElectionTimeoutMax = time.Duration(400) * time.Millisecond
)
func randTimeDuration(lower, upper time.Duration) time.Duration {
num := rand.Int63n(upper.Nanoseconds()-lower.Nanoseconds()) + lower.Nanoseconds()
return time.Duration(num) * time.Nanosecond
}
状态转换方法
func (rf *Raft) convertTo(state int32) {
if state == rf.state {
return
}
rf.state = state
switch state {
case Follower:
rf.heartbeatTimer.Stop()
rf.electionTimer.Reset(randTimeDuration(ElectionTimeoutMin, ElectionTimeoutMax))
rf.votedFor = -1
case Candidate:
rf.startElection()
case Leader:
rf.electionTimer.Stop()
rf.broadcastHeartbeat()
rf.heartbeatTimer.Reset(HeartbeatInterval)
}
}
Raft
currentTerm 当前版本
votedFor 竞选对象
electionTimer 竞选timer
heartbeatTimer 心跳包timer
state 状态
type Raft struct {
mu sync.Mutex // Lock to protect shared access to this peer's state
peers []*labrpc.ClientEnd // RPC end points of all peers
persister *Persister // Object to hold this peer's persisted state
me int // this peer's index into peers[]
currentTerm int32
votedFor int
electionTimer *time.Timer
heartbeatTimer *time.Timer
state int32
}
添加goroutine处理electionTimer和heartbeatTimer
rf.votedFor = -1
rf.currentTerm = 0
rf.heartbeatTimer = time.NewTimer(HeartbeatInterval)
rf.electionTimer = time.NewTimer(randTimeDuration(ElectionTimeoutMin, ElectionTimeoutMax))
rf.state = Follower
go func(node *Raft) {
for {
select {
case <-rf.electionTimer.C:
rf.mu.Lock()
if rf.state == Follower {
rf.convertTo(Candidate)
} else {
rf.startElection()
}
rf.mu.Unlock()
case <-rf.heartbeatTimer.C:
rf.mu.Lock()
if rf.state == Leader {
rf.broadcastHeartbeat()
rf.heartbeatTimer.Reset(HeartbeatInterval)
}
rf.mu.Unlock()
}
}
}(rf)
两个周期性的方法
注意点
- 处理返回结果时,一定要加锁后处理数据
- 需要考虑是否是当前节点发起的请求
- goroutine并发问题
发起选举startElection
发起投票,如果返回成功,计数加一,如果同意方多于总数的一半,则状态改为Leader
如果发现其他节点当前版本大于发起选举的节点,则状态改为Follower
func (rf *Raft) startElection() {
rf.currentTerm += 1
rf.electionTimer.Reset(randTimeDuration(ElectionTimeoutMin, ElectionTimeoutMax))
args := RequestVoteArgs{
Term: rf.currentTerm,
CandidateId: rf.me,
}
var voteCount int32
for i := range rf.peers {
if i == rf.me {
rf.votedFor = rf.me
atomic.AddInt32(&voteCount, 1)
continue
}
go func(server int) {
var reply RequestVoteReply
if rf.sendRequestVote(server, &args, &reply) {
rf.mu.Lock()
if reply.VoteGranted && rf.state == Candidate {
atomic.AddInt32(&voteCount, 1)
if atomic.LoadInt32(&voteCount) > int32(len(rf.peers)/2) {
rf.convertTo(Leader)
}
} else {
if reply.Term > rf.currentTerm {
rf.currentTerm = reply.Term
rf.convertTo(Follower)
}
}
rf.mu.Unlock()
}
}(i)
}
}
Leader发送心跳包
如果发现其他节点当前版本大于发送心跳包的节点(可能存在网络分割等情况,导致有多个leader),则状态改为Follower
func (rf *Raft) broadcastHeartbeat() {
for i := range rf.peers {
if i == rf.me {
continue
}
go func(server int) {
if rf.state != Leader {
rf.mu.Unlock()
return
}
args := AppendEntriesArgs{
Term: rf.currentTerm,
LeaderId: rf.me,
}
var reply AppendEntriesReply
if rf.sendAppendEntries(server, &args, &reply) {
rf.mu.Lock()
if reply.Term > rf.currentTerm {
rf.currentTerm = reply.Term
rf.convertTo(Follower)
}
rf.mu.Unlock()
}
}(i)
}
}
实现两个方法
对应投票的 RequestVote
如果当前节点的版本大于传入的版本,或者当前节点版本等于当前版本但是当前节点已经选举其他节点(同时有其他节点在发起选举),则返回失败
若成功需要Reset electionTimer(重新开始计时)
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
if args.Term < rf.currentTerm || (args.Term == rf.currentTerm && rf.votedFor != -1 && rf.votedFor != args.CandidateId) {
reply.Term = rf.currentTerm
reply.VoteGranted = false
return
}
if args.Term > rf.currentTerm {
rf.currentTerm = args.Term
rf.convertTo(Follower)
}
rf.electionTimer.Reset(randTimeDuration(ElectionTimeoutMin, ElectionTimeoutMax))
rf.votedFor = args.CandidateId
reply.VoteGranted = true
}
对应心跳包的 AppendEntries
如果
当前节点的版本大于传入的版本,则返回失败
若成功需要Reset electionTimer(重新开始计时)
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
if args.Term < rf.currentTerm {
reply.IsSuccess = false
reply.Term = rf.currentTerm
return
}
if args.Term > rf.currentTerm {
rf.currentTerm = args.Term
rf.convertTo(Follower)
}
rf.electionTimer.Reset(randTimeDuration(ElectionTimeoutMin, ElectionTimeoutMax))
reply.Success = true
}