文章目录
前言
这是6.824系统的raft算法的分布式选举部分,通过go语言进行实现。
来自MIT该系统的课程lab2
一、raft 算法分布式选举原理
Raft内部有一种心跳机制,如果存在leader,那么它就会周期性地向所有follower发送心跳,来维持自己的地位。如果follower-一段时间没有收到心跳,那么他就会认为系统中没有可用的leader了,然后开始进行选举。开始一个选举过程后,follower先增加自己的当前任期号,并转换到candidate.状态。然后投票给自己,并且并行地向集群中的其他服务器节点发送投票请求(RequestVote RPC)
投票的分裂处理
投票分裂 同时超时 并且得票一样 只要是candidate会不停地向没有回复的发起投票 一直投票 导致 在这个过程中 有其他已经投过票的 超时了
那么那个后面超时的 term++ term已经比前面两个 竞选者大了 小于新的竞选者 所以都会吧赞成票给新的竞选者 成为leader
另一个情况 都平票 那么就继续等待超时 然后继续选举
二、结构体说明与初始化
1、节点状态与投票状态
自身节点的状态只有三个 跟随者,领导者,竞选者,
type Status int
const (
Follower Status =iota
Candidate
Leader
)
投票状态有正常投票,节点终止,投票国企,还有已经投过票了四种状态。
type VoteState int
const (
Normal VoteState = iota //投票过程正常
Killed //Raft节点已终止
Expire //投票(消息\竞选者)过期
Voted //本Term内已经投过票
)
2、日志结构体
日志结构体应该包含日志内容以及日志所在的任期两个陈媛
type LogEntry struct {
Term int
Command interface{
}
}
3、raft节点
对于节点信息,应该包含所有的节点,以及自身节点的index,记录当前的任期,记录当前的任期把票投给了谁,日志条目数组,包含了状态机要执行的指令集,以及收到领导时的任期号。
还应该保存 节点是什么角色 以及超时时间以及计时器。
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 //记录单签的任期
votedFor int //记录当前的任期投票给谁了
logs []LogEntry //日志条目数组
// 所有的servers经常修改的:
// 正常情况下commitIndex与lastApplied应该是一样的,但是如果有一个新的提交,并且还未应用的话last应该要更小些
commitIndex int // 状态机中已知的被提交的日志条目的索引值(初始化为0,持续递增)
lastApplied int // 最后一个被追加到状态机日志的索引值
// leader拥有的可见变量,用来管理他的follower(leader经常修改的)
// nextIndex与matchIndex初始化长度应该为len(peers),Leader对于每个Follower都记录他的nextIndex和matchIndex
// nextIndex指的是下一个的appendEntries要从哪里开始
// matchIndex指的是已知的某follower的log与leader的log最大匹配到第几个Index,已经apply
nextIndex []int // 对于每一个server,需要发送给他下一个日志条目的索引值(初始化为leader日志index+1,那么范围就对标len)
matchIndex []int // 对于每一个server,已经复制给该server的最后日志条目下标
// 由自己追加的:
status Status // 该节点是什么角色(状态)
overtime time.Duration // 设置超时时间,200-400ms 固定的
timer *time.Ticker // 每个节点中的计时器
applyChan chan ApplyMsg // 日志都是存在这里client取(2B)
}
4、投票rpc
该rpc的请求结构体是竞选者发送的给其他节点的,应当包含以下内容,自身的任期,竞选者的index。竞选人日志条目最后索引,候选人最后日志条目的任期号。
而回复结构体 包含投票方的任期 以及 是否投票给你,还有就是投票的状态,是否投过票这些。
type RequestVoteArgs struct {
// Your data here (2A, 2B).
Term int // 需要竞选的人的任期
CandidateId int // 需要竞选的人的Id
LastLogIndex int // 竞选人日志条目最后索引
LastLogTerm int // 候选人最后日志条目的任期号
}
type RequestVoteReply struct {
// Your data here (2A).
Term int // 投票方的term,如果竞选者比自己还低就改为这个
VoteGranted bool // 是否投票给了该竞选人
VoteState VoteState // 投票状态
}
5、心跳rpc
首先设置一个全局的心跳超时时间
var HeartBeatTimeout = 120 * time.Millisecond
其发起的rpc
type AppendEntriesArgs struct {
Term int // leader的任期
LeaderId int // leader自身的ID
PrevLogIndex int // 预计要从哪里追加的index,因此每次要比当前的len(logs)多1 args初始化为:rf.nextIndex[i] - 1
PrevLogTerm int // 追加新的日志的任期号(这边传的应该都是当前leader的任期号 args初始化为:rf.currentTerm
Entries []LogEntry // 预计存储的日志(为空时就是心跳连接)
LeaderCommit int // leader的commit index指的是最后一个被大多数机器都复制的日志Index
}
type AppendEntriesReply struct {
Term int // leader的term可能是过时的,此时收到的Term用于更新他自己
Success bool // 如果follower与Args中的PreLogIndex/PreLogTerm都匹配才会接过去新的日志(追加),不匹配直接返回false
AppState AppendEntriesState // 追加状态
}
三、整体流程
1、整体
大体的流程如下,如果存在leader,那么它就会周期性地向所有follower发送心跳,来维持自己的地位。如果follower-一段时间没有收到心跳,那么他就会认为系统中没有可用的leader了,然后开始进行选举。
2、初始化
初始化每个raft包括写入所有的同行,自己的index,以及初始化自身的属性,比如说自己的状态一开始都是follower。设置随机产生的选举超时时间,一般设置在150-350ms。初始化完毕之后开一个协程进行控制这个raft
func Make(peers [