一、Raft选主流程
- 当新集群启动的时候,所有的机器A、B、C的默认状态是Follower,所有的机器地址endpoint作为初始化参数传入进程。
- 如果收到心跳,则作为Follower开始工作,选主结束。如果超过一段随机选举超时时间后(在一定范围且大于心跳时间), 开始发起Election。随机的目的是为了保证不要同时发起Election,在少数情况下可能会发生同时发起选举情况。
- 集群初始化时没有Leader,所以开始所以的机器没有收到心跳,机器A开始发起选举。首先机器A把自己的身份置为Candidate,把Term自增1,并且自己投票给自己。
- 当其他机器B接受到这个vote投票请求,如果发现Candidate的Term小于自己,则否决,并返回自己的Term。 - 如果机器B发现Candidate的Term大于自己,就更新Term,并且重置投给谁votedFor为A。 - 如果机器B发现当前Term已经给别人投票过,就否决。 - 如果 Candidate的日志不是最新,就否决。 - 赞成
- 机器A在发送vote请求后,如果发现大多数赞成,则跳到步骤7,成为Leader。
- 如果发现返回的Term大于自己,就放弃本次Election,更新自己的term到返回的term,重置自己的状态为Follower,重新开始步骤3。
- 如果机器A在投票结束,没有收到大多数赞成票,则返回步骤3。
- 机器A选主成功, 调整自身状态为Leader, 并且立即发送一次心跳(避免其他机器超时进入选主状态)。
- 周期性发送心跳, 保证Leader地位。
跟随者只响应来自其他服务器的请求。如果跟随者接收不到消息,那么他就会变成候选人并发起一次选举。获得集群中大多数选票的候选人将成为领导者。领导人一直都会是领导人直到自己宕机了。
二、代码实现
- 根据论文填充Raft结构字段。
- 添加实现Raft的接口,必须支持下面的接口,这些接口会在测试例子和你们最终的key/value服务器中使用。
// create a new Raft server instance:
rf := Make(peers, me, persister, applyCh)
// start agreement on a new log entry:
rf.Start(command interface{}) (index, term, isleader)
// ask a Raft for its current term, and whether it thinks it is leader
rf.GetState() (term, isLeader)
// each time a new entry is committed to the log, each Raft peer
// should send an ApplyMsg to the service (or tester).
type ApplyMsg
一个服务通过调用Make(peers,me,…)创建一个Raft端点。peers参数是通往其他Raft端点处于连接状态下的RPC连接。me参数是自己在端点数组中的索引。
- 实现领导选举和心跳(empty AppendEntries calls). 这应该是足够一个领导人当选,并在出错的情况下保持领导者。
- 实现RequestVoteArgs和RequestVoteReply结构体,然后修改Make()函数创建一个后台的goroutine,当长时间接收不到其他节点的信息时开始选举(通过对外发送RequestVote请求)。为了能让选举工作,你们需要实现RequestVote()请求的处理函数,这样服务器们就可以给其他服务器投票。
- 为了实现心跳,你们将会定义一个AppendEntries结构(虽然你们可能用不到全部的从参数),有领导人定期发送出来。你们同时也需要实现AppendEntries请求的处理函数,重置选举超时,当有领导选举产生的时候其他服务器就不会想成为领导。
- 确保定时器在不同的Raft端点没有同步。尤其是确保选举的超时不是同时触发的,否则全部的端点都会要求会自己投票,然后没有服务器能够成为领导。
- 当我们的代码可以完成领导选举之后,我们想要使用Raft保存一致,复杂日志操作。为了做到这些,我们需要通过Start()让服务器接受客户端的操作,然后将操作插入到日志中。在Raft中,只有领导者被允许追加日志,然后通过AppendEntries调用通过其他服务器增加新条目。
1、 Raft结构字段填充
首先根据论文来实现Raft结构体,应该包含如下信息:
所有服务器上持久存在的状态
参数 | 含义 |
---|---|
currentTerm | 服务器最后一次知道的任期号(初始化为 0,持续递增) |
votedFor | 在当前获得选票的候选人的 Id |
log[] | 日志条目集;每一个条目包含一个用户状态机执行的指令和收到时的任期号 |
所有服务器上经常变的状态
字段 | 含义 |
---|---|
commitIndex | 已知的最大的已经被提交的日志条目的索引值 |
lastApplied | 最后被应用到状态机的日志条目索引值(初始化为 0,持续递增 |
在领导人里经常改变的状态 (选举后重新初始化)
字段 | 含义 |
---|---|
nextIndex[] | 对于每一个服务器,需要发送给他的下一个日志条目的索引值(初始化为领导人最后索引值加一) |
matchIndex[] | 对于每一个服务器,已经复制给他的日志的最高索引值 |
Raft结构
type Raft struct {
mu sync.Mutex
peers []*labrpc.ClientEnd
persister *Persister
me int // index into peers[]
// Your data here.
// Look at the paper's Figure 2 for a description of what
// state a Raft server must maintain.
currentTerm int
votedFor int
log []logEntries
commitIndex int
lastApplied int
nextIndex []int
matchIndx []int
state Role
leader int
appendEntriesCh chan bool
voteGrantedCh chan bool
leaderCh chan bool
applyMsgCh chan ApplyMsg
heatbeatTimeout time.Duration
electionTimeout time.Duration
}
通过Make函数初始化创建一个Raft对象
一个服务通过调用Make(peers,me,…)创建一个Raft端点。peers参数是通往其他Raft端点处于连接状态下的RPC连接,me参数是自己在端点数组中的索引。
状态机中状态转移图:
所有服务器:
如果接收到的 RPC 请求中,任期号T > currentTerm,那么就令 currentTerm 等于 T,并切换状态为跟随者。
跟随者(Follow):
- 响应来自候选人和领导者的请求。
- 如果在超过选举超时时间的情况之前都没有收到领导人的心跳,或者是候选人请求投票的,就自己变成候选人。
候选人(Candidate):
在转变成候选人后就立即开始选举过程
- 自增当前的任期号(currentTerm)。
- 给自己投票。
- 重置选举超时计时器。
- 发送请求投票的 RPC 给其他所有服务器。
- 如果接收到大多数服务器的选票,那么就变成领导人。
- 如果接收到来自新的领导人的附加日志 RPC,转变成跟随者。
- 如果选举过程超时,再次发起一轮选举。
领导人(Leader):
- 一旦成为领导人:发送空的附加日志 RPC(心跳)给其他所有的服务器;在一定的空余时间之后不停的重复发送,以阻止跟随者超时。
// create a new Raft server instance:
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.
rf.currentTerm = STARTTERM
rf.votedFor = VOTENULL
rf.log = make([]logEntries,0)
rf.log = append(rf.log,logEntries{0,0})
rf.commitIndex = 0
rf.lastApplied = 0
rf.state = Follow
rf.nextIndex = make([]int,len(rf.peers))
rf.matchIndx = make([]int,len(rf.peers))
rf.appendEntriesCh = make(chan bool,1)
rf.voteGrantedCh = make(chan bool,1)
rf.leaderCh = make(chan bool,1)
rf.applyMsgCh = applyCh
rf.heatbeatTimeout = time.Duration(HEATBEATTIMEOUT) * time.Millisecond
// initialize from state persisted before a crash
rf.readPersist(persister.ReadRaftState())
go func(){
for {
rf.mu.Lock()
state := rf.state
rf.mu.Unlock()
electionTimeout := HEATBEATTIMEOUT * 2 + rand.Intn(HEATBEATTIMEOUT)
rf.mu.Lock()
rf.electionTimeout = time.Duration(electionTimeout) * time.Millisecond
rf.mu.Unlock()
switch state {
case Follow:
select {
/*
如果在超过选举超时时间的情况之前都没有收到领导人的心跳,或者是候选人请求投票的,就自己变成候选人
*/
case <- rf.appendEntriesCh:
case <- rf.voteGrantedCh:
case <- time.After(rf.electionTimeout):
rf.coverToCandidate()
}
case Candidate:
go rf.leaderElection()
select {
case <- rf.appendEntriesCh:
case <- rf.voteGrantedCh:
case <- rf.leaderCh:
case <- time.After(rf.electionTimeout):
rf.coverToCandidate()
}
case Leader:
go rf.broadcastHeartbeat()
time.Sleep(rf.heatbeatTimeout)
}
}
}()
return rf
}
2、领导选举
请求投票 RPC和返回参数
要开始一次选举过程,跟随者先要增加自己的当前任期号并且转换到候选人状态。然后他会并行的向集群中的其他服务器节点发送请求投票的 RPCs 来给自己投票。所以先要实现RequestVoteArgs和RequestVoteReply结构体,即请求投票RPC的参数和反馈结果。
请求投票RPC参数RequestVoteArgs
由候选人负责调用用来征集选票
参数 | 含义 |
---|---|
term | 候选人的任期号 |
candidateId | 请求选票的候选人的 Id |
lastLogIndex | 候选人的最后日志条目的索引值 |
lastLogTerm | 候选人最后日志条目的任期号 |
type RequestVoteArgs struct {
// Your data here.
Term int
CandidateId int
LastLogIndex int
LastLogTerm int
}
请求投票RPC参数返回值RequestVoteReply
参数 | 含义 |
---|---|
term | 当前任期号,以便于候选人去更新自己的任期号 |
voteGranted | 候选人赢得了此张选票时为真 |
type RequestVoteReply struct {
// Your data here.
Term int
VoteGranted bool
}
发起投票实现
为了提高性能,需要并行发送RPC。可以迭代peers,为每一个peer单独创建一个goroutine发送RPC。Raft Structure Adivce建议:
在同一个goroutine里进行RPC回复(reply)处理是最简单的,而不是通过(over)channel发送回复消息。
所以,为每个peer创建一个gorotuine同步发送RPC并进行RPC回复处理。另外,为了保证由于RPC发送阻塞而阻塞的goroutine不会阻塞RequestVote RPC的投票统计,需要在每个发送RequestVote RPC的goroutine中实时统计获得的选票数,达到多数后就立即切换为Leader状态,并立即发送一次心跳,阻止其他peer因选举超时而发起新的选举。而不能在等待所有发送goroutine处理结束后再统计票数,这样阻塞的goroutine,会阻塞领导者的产生,且在阻塞的过程中,容易使得达到了选举超时时间,会进入新的一个周期再次进行发出投票请求。
func (rf *Raft) leaderElection() {
rf.mu.Lock()
if rf.state != Candidate {
rf.mu.Unlock()
return
}
rf.mu.Unlock()
rf.mu.Lock()
args := RequestVoteArgs{
Term:rf.currentTerm,
CandidateId:rf.me,
LastLogIndex:rf.getLastIndex(),
LastLogTerm:rf.getLastTerm(),
}
rf.mu.Unlock()
winThreshold := int64(len(rf.peers)/2 + 1)
voteCount := int64(1)
for i := 0; i < len(rf.peers); i++ {
if i == rf.me {
continue
}
go func(index int,args RequestVoteArgs) {
reply := &RequestVoteReply{}
ok := rf.sendRequestVote(index,args,reply)
if !ok {
rf.mu.Lock()
defer rf.mu.Unlock()
DPrintf("sendRequestVote fail,request term:%d,candidate id: %d,",args.Term,args.CandidateId)
return
}
rf.mu.Lock()
if args.Term != rf.currentTerm {
rf.mu.Unlock()
return
}
rf.mu.Unlock()
if reply.VoteGranted == false {
if reply.Term > rf.currentTerm {
rf.coverToFollow(reply.Term)
}
}else{
atomic.AddInt64(&voteCount,1)
if atomic.LoadInt64(&voteCount) >= winThreshold && rf.state == Candidate {
DPrintf("server %d win the vote",rf.me)
rf.coverToLeader()
chanSet(rf.leaderCh)
}
}
}(i,args)
}
}
接收者实现:
如果term < currentTerm返回 false。
如果 votedFor 为空或者就是 candidateId,并且候选人的日志也自己一样新,那么就投票给他。
func (rf *Raft)RequestVote(args RequestVoteArgs, reply *RequestVoteReply) {
// Your code here.
voteGranted := false
/*
如果term < currentTerm返回 false
*/
rf.mu.Lock()
if rf.currentTerm > args.Term {
reply.Term = rf.currentTerm
reply.VoteGranted = voteGranted
rf.mu.Unlock()
return
}
rf.mu.Unlock()
/*
如果接收到的 RPC 请求中,任期号T > currentTerm,那么就令 currentTerm 等于 T,并切换状态为跟随者
T > currentTerm时,当此服务器状态为Candidate时,在发起选举时给自己投票,会将voteFor设置为自己的id,切换到Follow时,重置voteFor。
T > currentTerm时,当次服务器状态为Follow时,则此时服务器没有给其他节点投过票,如果投过票则currentTerm更新为最新的,此时重置voteFor也没问题。
*/
if rf.currentTerm < args.Term {
rf.coverToFollow(args.Term)
}
/*
如果 votedFor 为空或者就是 candidateId,并且候选人的日志也自己一样新,那么就投票给他
*/
rf.mu.Lock()
if (rf.votedFor == VOTENULL || rf.votedFor == args.CandidateId) && ((rf.currentTerm <= args.Term) || ((rf.getLastIndex() <= args.LastLogIndex) && (rf.currentTerm== args.Term)) ) {
voteGranted = true
rf.votedFor = args.CandidateId
rf.state = Follow
rf.leader = args.CandidateId
chanSet(rf.voteGrantedCh)
}
reply.VoteGranted = voteGranted
reply.Term = rf.currentTerm
rf.mu.Unlock()
}
3、心跳实现
- 发送请求
当状态转变为Leader后,马上给其他所有的节点发送心跳请求。收到请求的回复,如果回复的Term > currentTerm,说明存在一个更大的任期,则转换成Follow状态,重置voteFor,更新周期。 - 接收者实现
当接收到的Term < currentTerm时,则返回失败。否则返回成功,接收到的节点将自己状态转换为Follow。