MIT 6.824 分布式课程Lab2 2A Raft领导者选举和心跳机制

一、Raft选主流程

  1. 当新集群启动的时候,所有的机器A、B、C的默认状态是Follower,所有的机器地址endpoint作为初始化参数传入进程。
  2. 如果收到心跳,则作为Follower开始工作,选主结束。如果超过一段随机选举超时时间后(在一定范围且大于心跳时间), 开始发起Election。随机的目的是为了保证不要同时发起Election,在少数情况下可能会发生同时发起选举情况。
  3. 集群初始化时没有Leader,所以开始所以的机器没有收到心跳,机器A开始发起选举。首先机器A把自己的身份置为Candidate,把Term自增1,并且自己投票给自己。
    - 当其他机器B接受到这个vote投票请求,如果发现Candidate的Term小于自己,则否决,并返回自己的Term。
    - 如果机器B发现Candidate的Term大于自己,就更新Term,并且重置投给谁votedFor为A。
    - 如果机器B发现当前Term已经给别人投票过,就否决。
    - 如果 Candidate的日志不是最新,就否决。
    - 赞成
    
  4. 机器A在发送vote请求后,如果发现大多数赞成,则跳到步骤7,成为Leader。
  5. 如果发现返回的Term大于自己,就放弃本次Election,更新自己的term到返回的term,重置自己的状态为Follower,重新开始步骤3。
  6. 如果机器A在投票结束,没有收到大多数赞成票,则返回步骤3。
  7. 机器A选主成功, 调整自身状态为Leader, 并且立即发送一次心跳(避免其他机器超时进入选主状态)。
  8. 周期性发送心跳, 保证Leader地位。

跟随者只响应来自其他服务器的请求。如果跟随者接收不到消息,那么他就会变成候选人并发起一次选举。获得集群中大多数选票的候选人将成为领导者。领导人一直都会是领导人直到自己宕机了。

二、代码实现

  1. 根据论文填充Raft结构字段。
  2. 添加实现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参数是自己在端点数组中的索引。
  1. 实现领导选举和心跳(empty AppendEntries calls). 这应该是足够一个领导人当选,并在出错的情况下保持领导者。
    • 实现RequestVoteArgs和RequestVoteReply结构体,然后修改Make()函数创建一个后台的goroutine,当长时间接收不到其他节点的信息时开始选举(通过对外发送RequestVote请求)。为了能让选举工作,你们需要实现RequestVote()请求的处理函数,这样服务器们就可以给其他服务器投票。
    • 为了实现心跳,你们将会定义一个AppendEntries结构(虽然你们可能用不到全部的从参数),有领导人定期发送出来。你们同时也需要实现AppendEntries请求的处理函数,重置选举超时,当有领导选举产生的时候其他服务器就不会想成为领导。
    • 确保定时器在不同的Raft端点没有同步。尤其是确保选举的超时不是同时触发的,否则全部的端点都会要求会自己投票,然后没有服务器能够成为领导。
    • 当我们的代码可以完成领导选举之后,我们想要使用Raft保存一致,复杂日志操作。为了做到这些,我们需要通过Start()让服务器接受客户端的操作,然后将操作插入到日志中。在Raft中,只有领导者被允许追加日志,然后通过AppendEntries调用通过其他服务器增加新条目。

1、 Raft结构字段填充

首先根据论文来实现Raft结构体,应该包含如下信息:
MIT 6.824 分布式课程Lab2 2A Raft领导者选举和心跳机制_第1张图片
所有服务器上持久存在的状态

参数 含义
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参数是自己在端点数组中的索引。

状态机中状态转移图:
MIT 6.824 分布式课程Lab2 2A Raft领导者选举和心跳机制_第2张图片

所有服务器:
如果接收到的 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的参数和反馈结果。

MIT 6.824 分布式课程Lab2 2A Raft领导者选举和心跳机制_第3张图片
请求投票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。

你可能感兴趣的:(分布式系统,Raft,Mit6.824,一致性算法)