文章目录
一、代码入手——读lab说明
1.根据课程lab指引上的说明,从raft/raft.go文件入手。
官方lab说明
2.代码需要实现下面的接口
3.其中的Make方法是返回了一个raft服务实例
peers参数是一个raft实例的网络id数组(包括当前的实例)。me参数是当前peer在peers中的下标。
4.Start(command)是要求raft启动一个进程来将coomand命令加入到副本日志中去。
Start方法应该立刻返回,不需要等待日志添加完成。
5.服务需要实现对于每一个新提交的log entry发送一个ApplyMsg给Make()的参数applych通道。
6.raft.go包括一个发送RPC的例子sendReduestVote(), 和处理一个传入RPC的例子RequestVote().
7.Raft实例必须通过RPC交互,不允许通过共享Go变量或文件。
8.lab2A
(1)2A部分的实验是实现了raft的领导选举和发送心跳信息(没有log entry的AppendEntries)
(2)2A的目的是选举出一个leader,在这个leader没有出现错误的情况下保持它的leader身份,和旧leader出错后新的leader接管。
(3)运行go test -run 2A来测试2A部分代码
(4)2A部分主要是论文Figure2的内容,发送和接收RequestVote RPC, 服务器和选举有关的规则,和领导选举相关的状态。
(5)在raft.go的Raft struct中增加Figure2中领导选举的状态。也需要定义一个结构体来保持每一个log entry的信息。
(6)填写RequestVoteArgs和RequestVoteReply结构体,修改Make()方法来创建一个后台协程,在它一段时间内没有收到另一个raft实例的消息时,就周期性的发送RequestVote RPC消息进行leader选举。通过这种方式,一个raft实例可以知道谁是leader,或者自己成为leader。
实现RequestVote() RPC处理方法,使得服务器能够为另一个投票
(7)为了实现心跳,定义一个AppendEntries RPC结构体,让leader周期性的发送它们。
写一个AppendEntries RPC处理方法,可以重置选举超时,这样当一个server已经被选举为leader时,其他的就不会当选了。
(8)要确定不同实例的选举超时不会同一时间发起,否则每个实例都会投票给自己,将永远不会有实例被当选为leader
(9)测试需要leader发送心跳RPC消息不会大于每秒十次。
(10)测试需要raft在旧leader失败5s内选出新的leader。
(11)超时时间虽然论文中是150ms-300ms,因为测试要求每秒最多10个心跳信息,所以超时时间需要比300ms大一点,但是不能太大,否则5s内无法完成新leader的选举。
(12)go中的rand是非常有用的
(13)对于一些需要周期性或者延迟一些时间运行的方法,可以使用time.Sleep()。不要用time.Timer或者time.Ticker。
(14)lock的参考
a. 当一个数据被多个协程使用,至少一个需要修改时,使用lock
b.当一个系列的修改不能让其他协程看到中间状态时,使用lock
c.当代码进行一系列读操作,可能会由其他协程读写时造成错误结果,使用lock
raft RPC处理方法会从头到尾持有一个lock
d.不能在做任何事的时候都加锁,一是效率低,二是会死锁。
e.要注意在释放锁和再次获得锁的地方。例子中在等待锁的期间,变量可能会有变化,导致执行错误。
(15)struct的参考
一个raft实例有两个时间驱动活动:leader发送心跳,和如果长时间未收到leader的信息跟随者发起选举。最好是这些活动各自有一个专用的长时间的协程,而不是都写到一个单独的协程。
管理选举超时,最简单的方式就是在raft结构体中设置一个变量来保留raft实例从leader获取信息的最后的时间。并且有一个选举超时的协程周期性的检查这些时间。一个简单的方式是使用time.Sleep()方法来驱动这些周期性的检查。
发送已提交log到applyCh需要使用一个单独的长时间运行的协程,因为发送到applyCh通道会阻塞,所以一定要是个单独的协程。一定要使用一个协程是因为否则会很难确认发送的log entries在日志中的顺序。推进coomitIndex的代码需要启动apply协程,最简单的方法是使用一个条件变量sync.Cond
RPC响应进程应该与发送进程在同一个Go协程中,而不是通过一个channel发送响应消息。
记住网络会延迟RPC及其响应消息。当发送同步RPC时,网络会对请求和回应重新排序。Figure 2 的RPC响应进程不详尽。leader必须小心处理响应进程;它必须检查term从发送RPC后有无改变,并且必须对给相同follower发送同步RPC响应时改变了leader的状态负责任。
(16)通不过测试返回读论文的Figure 2
(17)不要忘记实现GetState()方法
(18)测试会调用rf.Kill()方法来永久的关闭一个实例。你可以使用rf.killed()方法来检查是否调用过rf.Kill(). 在所有的循环中都应该调用这个,来避免死去的raft实例打印出混乱消息。
(19)一个好的debug方式是在实例发送或接收消息时插入打印语句,并且将这些使用go test -run 2A > out输出到一个文件中。util.go中的DPrintf()方法在debug不同问题时作为打印开关很有用。
(20)Go RPC只发送名字大写字母开头的结构体变量。labgob包会提醒这些。
(21)用go test -race检查代码,并修复它报告的任何冲突。
二、lab2A实现
1.完善raft结构体
代码中读取term、currentTerm、votedFor等可以读写的变量时要加锁,否则出现不一致。
//
// A Go object implementing a single Raft peer.
//
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[]
dead int32 // set by Kill()
// Your data here (2A, 2B, 2C).
// Look at the paper's Figure 2 for a description of what
// state a Raft server must maintain.
currentTerm int //当前任期
heartBeatInterval time.Duration //心跳时间间隔
state RaftState // 0表示为跟随者 1为候选者 2为leader
votedFor int // 为哪个实例投票
log []LogEntry // 同步的日志
commitIndex int // 提交日志的index
lastApplied int // 已应用日志的index
nextIndex []int // 所有实例下一个需要同步的日志
matchIndex []int // 所有实例匹配的日志
//lastResponse time.Time // 上一次收到leader响应的时间(用于管理选举超时)
electionTime time.Time //选举超时时间,是一个时间点
}
2.Make方法,返回raft实例
func Make(peers []*labrpc.ClientEnd, me int,
persister *Persister, applyCh chan ApplyMsg) *Raft {
rf := &Raft{}
rf.peers = peers
rf.persister = persister
rf.me = me
// Your initialization code here (2A, 2B, 2C).
rf.state = Follower
rf.currentTerm = 0
rf.votedFor = -1
rf.resetElectionTimer()
rf.heartBeatInterval = 50 * time.Millisecond
n := len(peers)
rf.nextIndex = make([]int, n)
rf.log = make([]LogEntry, 0)
rf.log = append(rf.log, LogEntry{0, 0, -1}) //增加第一条日志,此处是为了防止发送请求投票的RPC请求时,传入的最后一个日志下标不合法
rf.matchIndex = make([]int, n)
// initialize from state persisted before a crash
rf.readPersist(persister.ReadRaftState())
// start ticker goroutine to start elections
go rf.ticker()
return rf
}
3.ticker方法及相关实现
创建raft实例时调用的ticker方法:
周期性的判断是否应该进行选举(400ms内没有收到心跳信息)
下面的代码有两个坑:①不要在循环中写defer解锁,不然可能会死锁。②不要这里在加锁的代码中写RPC请求,不然可能会一直发送。具体分析参考另外一篇博客lab2A调试过程中错误记录
// The ticker go routine starts a new election if this peer hasn't received
// heartsbeats recently.
//在该协程中周期性的检查是否大于electionTime时间点,如果超时,触发选举
func (rf *Raft) ticker() {
for rf.killed() == false {
//间隔心跳周期检查状态
time.Sleep(rf.heartBeatInterval)
rf.mu.Lock()
// Your code here to check if a leader election should
// be started and to randomize sleeping time using
// time.Sleep().
if rf.state == Leader { //如果当前实例是Leader,发送心跳信息
rf.appendEntry(false)
}
//timeNoResponse := time.Now().Sub(rf.lastResponse)
如果当前实例间隔electionTimeout ms以内没有收到心跳,则开始选举流程(这里每个实例都选择的不同值,防止同时发起选举)
//var electionTimeout int64 = (int64)(rand.Intn(100) + 300)
if time.Now().After(rf.electionTime) {
rf.leaderElection()
}
rf.mu.Unlock()
}
}
(1)ticker方法中appendEntry的实现
心跳信息和发送日志信息用的是同一个方法appendEntry,参数表示是否有日志,如果日志为空参数为false,表示该RPC消息是心跳信息。
//appendEntry Leader用该方法发送日志或心跳信息,hasLogs为true表示是日志
func (rf *Raft) appendEntry(hasLogs bool) {
for peer, _ := range rf.peers {
if peer == rf.me {
rf.resetElectionTimer()
continue
}
if !hasLogs {
args := RequestAppendEntryArgs{
Term: rf.currentTerm,
LeaderId: rf.me,
}
reply := RequestAppendEntryReply{}
go rf.leaderSendEntries(peer, &args, &reply)
}
}
}
(a)因为该段代码有用到锁mu,所以发送RPC的消息封装到一个协程中单独执行leaderSendEntries
//leader发送心跳,按照lab说明,使用一个单独长时间运行的协程(心跳信息不会大于每秒10次)
func (rf *Raft) leaderSendEntries(serverId int, args *RequestAppendEntryArgs, reply *RequestAppendEntryReply) {
ok := rf.sendRequestAppendEntry(serverId, args, reply)
if !ok {
DPrintf("[leaderSendEntries]sendRequestAppendEntry failed, leader id is : %d, term is : %d, target server id is : %d\n", rf.me, rf.currentTerm, serverId)
return
}
rf.mu.Lock()
defer rf.mu.Unlock()
if reply.Term > rf.currentTerm {
rf.currentTerm = reply.Term
return
}
}
(b)sendRequestAppendEntry封装了发送心跳的RPC请求
func (rf *Raft) sendRequestAppendEntry(serverId int, args *RequestAppendEntryArgs, reply *RequestAppendEntryReply) bool { // server是发送投票请求的目的实例id
ok := rf.peers[serverId].Call("Raft.RequestAppendEntry", args, reply)
return ok
}
RPC请求的实现(暂时只实现了接收心跳信息,还未实现接收日志的部分):
type RequestAppendEntryArgs struct {
Term int
LeaderId int
PrevLogIndex int
PrevLogTerm int
Entries []LogEntry
LeaderCommit int
}
type RequestAppendEntryReply struct {
Term int
Success bool
}
func (rf *Raft) RequestAppendEntry(args *RequestAppendEntryArgs, reply *RequestAppendEntryReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
DPrintf("[%d]: (term %d) follower 收到 [%v] RequestAppendEntry %v, prevIndex %v, prevTerm %v", rf.me, rf.currentTerm, args.LeaderId, args.Entries, args.PrevLogIndex, args.PrevLogTerm)
reply.Success = false
reply.Term = rf.currentTerm
if args.Term < rf.currentTerm {
return
}
rf.resetElectionTimer()
//if rf.state == Candidate {
rf.state = Follower
//}
if args.Entries == nil { //说明收到的是心跳信息,直接返回即可
reply.Success = true
reply.Term = rf.currentTerm
return
} else { // 收到的是日志同步信息
DPrintf("收到日志信息")
}
}
(2)ticker中的leader选举leaderElection()方法
这里使用了sync.Once类型的变量保证成为leader的方法只执行一次。当前的实例作为候选人。
func (rf *Raft) leaderElection() {
DPrintf("%d服务器开始成为候选者\n", rf.me)
rf.state = Candidate
rf.currentTerm += 1 //新的term
args := RequestVoteArgs{
Term: rf.currentTerm,
CandidateId: rf.me,
LastLogIndex: len(rf.log) - 1,
LastLogTerm: rf.log[len(rf.log)-1].LogTerm,
}
//先给自己投一票
okSum := 1
rf.votedFor = rf.me
rf.persist() //投票信息要持久化,持久化方法暂时未实现
rf.resetElectionTimer()
var becomeLeader sync.Once //用sync.Once类型的变量保证切换为leader的方法只执行一次
// 给其他的服务器发送投票RPC
for i := 0; i < len(rf.nextIndex); i++ {
if i == rf.me {
continue
}
go rf.candidateRequestVote(i, &args, &okSum, &becomeLeader)
}
}
(a) 封装了candidateRequestVote进行请求投票的RPC消息和返回值处理
func (rf *Raft) candidateRequestVote(serverId int, args *RequestVoteArgs, okSum *int, becomeLeader *sync.Once) {
reply := RequestVoteReply{}
ok := rf.sendRequestVote(serverId, args, &reply)
if !ok {
return
}
rf.mu.Lock() //接下来要比较返回的term和当前实例的term所以要加锁
defer rf.mu.Unlock()
//如果返回的term比实例本身的term还要大,说明当前leader已经过时
if reply.Term > args.Term {
DPrintf("[%d]: %d 在新的term,更新term,结束\n", rf.me, serverId)
rf.setNewTerm(reply.Term) //这里面要把Leader状态改为Follower
return
}
if reply.Term < args.Term {
DPrintf("[%d]: %d 的term %d 已经失效,结束\n", rf.me, serverId, reply.Term)
return
}
if !reply.VoteGranted {
DPrintf("[%d]: %d 没有投给me,结束\n", rf.me, serverId)
return
}
DPrintf("[%d]: from %d term一致,且投给%d\n", rf.me, serverId, rf.me)
*okSum++
if *okSum > len(rf.nextIndex)/2 && rf.currentTerm == args.Term && rf.state == Candidate {
DPrintf(" [%d]: 当选为leader, term为%d\n", rf.me, args.Term)
becomeLeader.Do(func() {
rf.state = Leader
//初始化两个数组
lastLogIndex := args.LastLogIndex
for i, _ := range rf.peers {
rf.nextIndex[i] = lastLogIndex + 1
rf.matchIndex[i] = 0
}
DPrintf("[%d]: leader - nextIndex %#v", rf.me, rf.nextIndex)
//发送心跳
rf.appendEntry(false)
})
}
}
(b)实现发送投票的RPC方法
func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool { // server是发送投票请求的目的实例id
ok := rf.peers[server].Call("Raft.RequestVote", args, reply)
return ok
}
下面方法是投票实现的重点,要注意一定避免同一个实例在同一个term内同时给两个候选者投票,这里使用了判断votedFor的值来避免这种情况,除非候选者的term较大,否则已经投票过的实例不会再次投票。
//
// example RequestVote RPC arguments structure.
// field names must start with capital letters!
//
type RequestVoteArgs struct { //申请投票的RPC请求
// Your data here (2A, 2B).
Term int //候选者任期
CandidateId int
LastLogIndex int // 候选者的最后一条日志索引
LastLogTerm int // 候选者的最后一条日志任期
}
//
// example RequestVote RPC reply structure.
// field names must start with capital letters!
//
type RequestVoteReply struct {
// Your data here (2A).
Term int // 处理请求节点的任期号,用于候选者更新自己的任期
VoteGranted bool // 候选者获得选票为true,否则为false
}
//
// example RequestVote RPC handler.
//
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
// Your code here (2A, 2B).
rf.mu.Lock()
defer rf.mu.Unlock()
voteTerm := args.Term
candidateId := args.CandidateId
reply.Term = rf.currentTerm
reply.VoteGranted = false
if voteTerm < rf.currentTerm {
return
}
// 收到的请求比当前实例的任期大,更新自己的currentTerm,并设为非领导。但此时并不代表就会投票给候选者,还需要判断日志
if voteTerm > rf.currentTerm {
rf.currentTerm = voteTerm
rf.votedFor = -1
}
// 在任期没问题的情况下,进一步判断日志来决定是否投票给候选者
if rf.votedFor == -1 || rf.votedFor == candidateId {
lastIndex := len(rf.log) - 1 //当前实例log日志最后一条的下标
// 比较当前实例rf日志最后一条log entry和候选者的,如果rf的任期大或任期相同但rf的index大,则返回,不投票给候选者
if rf.log[lastIndex].LogTerm > args.LastLogTerm || (rf.log[lastIndex].LogTerm == args.LastLogTerm && rf.log[lastIndex].Index > args.LastLogIndex) {
return
}
rf.votedFor = candidateId
rf.state = Follower
reply.VoteGranted = true
reply.Term = rf.currentTerm
rf.resetElectionTimer()
rf.persist()
DPrintf("当前server:%d 投票给%d,任期为:%d\n", rf.me, candidateId, voteTerm)
}
return
}