资料
实验官网:http://nil.csail.mit.edu/6.824/2018/labs/lab-raft.html
论文翻译:https://www.infoq.cn/article/raft-paper/
视频讲解:https://www.bilibili.com/video/BV1TW411M7Fx?from=search&seid=6824636064276951860
动画模拟:https://raft.github.io/
实验参考:
- https://blog.csdn.net/Miracle_ma/article/details/80030610
- https://segmentfault.com/a/1190000020893300
- https://www.jianshu.com/p/01d4c1bd5c65
Part 2A
Implement leader election and heartbeats (AppendEntries
RPCs with no log entries).
重点阅读论文的 5.1 以及 5.2,结合 Figure 2 食用。
字段定义,Figure2都有。
最重要的就是实现三个状态的转换,我使用状态机来实现。发现使用select实现有限状态机真的很方便。打印log的时候记得精确到ms。
// 运行状态机, Figure 4
// 暂时没有考虑锁,但是通过了2A
func (rf *Raft)runFSM() {
for {
state := rf.state
switch state {
case Follower:
rf.electionTimer.Reset(randDuration())
select {
// 1 Follower -> Candidate:timeout, start election
case <- rf.electionTimer.C:
DPrintf("P%d time out", rf.me)
rf.state = Candidate
// leader的心跳
case term := <- rf.highTerm:
DPrintf("P%d get leader's heartbeat", rf.me)
rf.currentTerm = term
}
case Candidate:
DPrintf("P%d start election", rf.me)
rf.startElection()
select {
// 1 Candidate -> Leader : 获得了大多数票
case <- rf.becomeLeader:
rf.state = Leader
rf.electionTimer.Stop()
DPrintf("P%d become leader\n", rf.me)
// 2 Candidate -> Candidate: timeout, new election
case <- rf.electionTimer.C:
DPrintf("P%d election timeout\n", rf.me)
// TODO
// 3 Candidate -> Follower : 发现了新的leader或new term
case term := <-rf.highTerm:
DPrintf("P%d find a high term\n", rf.me)
rf.currentTerm = term
rf.votedFor = -1
rf.state = Follower
}
case Leader:
// 发送心跳
rf.broadcast()
DPrintf("P%d BH", rf.me)
rf.heartBeatTimer.Reset(heartBeatInterval)
// 1 Leader->Follower: 发现了更高的term
select {
case term := <- rf.highTerm:
rf.currentTerm = term
rf.votedFor = -1
rf.state = Follower
// 心跳到时
case <- rf.heartBeatTimer.C:
}
}
if state != rf.state {
DPrintf("P%d change state: %s -> %s\n", rf.me, stateName[state], stateName[rf.state])
}
}
}
Part2B
实现Raft.Start
函数,参考论文第 5.3 和 5.4.1 节。
参考:
- https://blog.csdn.net/guidao13/article/details/88070273
- https://www.cnblogs.com/mignet/p/6824_Lab_2_Raft_2B.html
matchIndex和nextIndex
- matchIndex:已经同步的位置,作用是用来更新commitIndex
- nextIndex:下一个需要复制的位置
matchIndex和nextIndex在什么时候发生变化?
答:当收到对方同步成功的reply后,需要更新matchIndex和nextIndex。两者的值相差1
什么时候两者的值不一样:
答:当新的Leader上任后,nextIndex是一个乐观估计(其他节点都有我的日志),初始化为rf.log.len
;matchIndex是保守估计(其他节点的日志是空的),初始化为0。
TestFailAgree2B
这个测试案列,调了好久。我一开始认为Follower掉线重连后,会继续追随原来的Leader。但是发现Follower的任期是远远大于Leader的,用动画模拟后,知道了Leader的任期也会增加,转变成Follower->Candidate。然后开始新的一轮的选举。
TestFailNoAgree2B
Raft认为任期和index都一样的日志,那么其内容也是一样的。
假设是P2,P3,P4掉线,重连后会有两种结果:
- Leader是P0或者P1,那么状态机就会有cmd=20这条命令
- Leader是P2 or P3 or P4,那么P0,P1就需要删除cmd=20这条命令
就是完善AppendEntries
的第3条。
1. 如果 leader 的任期小于自己的任期返回 false。 (5.1)
2. 如果自己不存在索引、任期和 prevLogIndex、 prevLogItem
匹配的日志返回 false。 (5.3)
3. 如果存在一条日志索引和 prevLogIndex 相等,
但是任期和 prevLogItem 不相同的日志,
需要删除这条日志及所有后继日志。(5.3)
4. 如果 leader 复制的日志本地没有,则直接追加存储。
5. 如果 leaderCommit>commitIndex,
设置本地 commitIndex 为 leaderCommit 和最新日志索引中
较小的一个。
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {}
需要测试多次,可以编写简单的测试脚本
export "GOPATH=/home/book/Develop/GOPATH/src/6.824"
for ((i=0;i<10;i++))
do
go test -run $1
done
分析下数据成员的安全性
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[]
// Your data here (2A, 2B, 2C).
// Look at the paper's Figure 2 for a description of what
// state a Raft server must maintain.
// 2A
currentTerm int // server 存储的最新任期
votedFor int // 当前任期接受到的选票的候选者 ID
state State // 节点的状态
electionTimer *time.Timer // 选举定时器
heartBeatTimer *time.Timer // use when it become Leader
highTerm chan int // 发现了更高的term
becomeLeader chan bool
// 2B
log []LogEntry
commitIndex int // 已知的提交的日志记录的索引(初值为 0 且单调递增)
// Leader的可变状态
nextIndex []int // 每台机器在数组占据一个元素,元素的值为下条发送到该机器的日志索引 (初始值为 leader 最新一条日志的索引 +1)
matchIndex []int // 每台机器在数组中占据一个元素,元素记录已经复制给该机器日志的索引的。
// for test
lastApplied int // 已经被提交到状态机的最后一个日志的索引(初值为0 且单调递增)
applyCh chan ApplyMsg
}
field | read | write | 结论 |
---|---|---|---|
currentTerm | GetState;RequestVote ;AppendEntries | FSM | + |
votedFor | RequestVote | FSM | + |
state | GetState … | FSM | + |
log | AppendEntries ; RequestVote | Leader:Start Follower:AppendEntries | + |
commitIndex | AppendEntries | Leader:每次请求后; Follower:AppendEntries | - |
nextIndex | heartBeat,Start; 访问的数据不一样 | - | |
matchIndex | heartBeat,Start | - | |
lastApplied | Leader在每次心跳请求后, 都会更新。 | + | |
TestRejoin2B
假设三任Leader分别为A,B,C,在第二任Leader断线、第一任Leader重连时,他们的日志为
// 我初始化有个空日志
A [{<nil> 0} {101 1} {102 1} {103 1} {104 1} {104 1}]
B [{<nil> 0} {101 1} {103 2}] // disconnet
c [{<nil> 0} {101 1} {103 2} {104 3}]
应该使A删除日志,与C同步,变成
// 我初始化有个空日志
A [{<nil> 0} {101 1} {103 2} {104 3}]
B [{<nil> 0} {101 1} {103 2}] // disconnet
c [{<nil> 0} {101 1} {103 2} {104 3}]
如果B重连后,因为C的Term > B,因此B会变成C的Follower。所有人的日志变成:
[{<nil> 0} {101 1} {103 2} {104 3} {105 3}]
TestBackup2B
这个测试最难通过的,为了方便理解测试场景,修改程序使得每次只加一条log而不是50条。
当执行完:
cfg.disconnect((leader1 + 0) % servers)
cfg.disconnect((leader1 + 1) % servers)
// allow other partition to recover
cfg.connect((leader1 + 2) % servers)
cfg.connect((leader1 + 3) % servers)
cfg.connect((leader1 + 4) % servers)
日志的变化情况如下。此时只有P0,P1,P3在线,因为P3的日志比其他两个更up-to-date,所以P3应该成为Leader。P3必须P0和P1的赞成,也就是要保证P3每次是第一个选举超时的。不然,P0可能投给P1,P1也有可能投给P0。选择在选Leader的时候需要花很多时间,有时候很快通过,有时候要花很久时间。
重写AppendEntries Handler
起初通不过测试是因为该函数有问题,主要是那个翻译和原文好像有歧义。
第2条和第3条应该翻译错了吧。我认为第2条:接受者的日志中没有args.prevLog,第3条:是为了提高性能,你当然选择删除args.PrevLogIndex后面的所有内容,然后再加入一样的东西。
我的部分代码:
// 2. Reply false if log doesn’t contain an entry at prevLogIndex
// whose term matches prevLogTerm (§5.3)
// 我的日志不包含args.prevLog, Leader will rf.nextIndex[]--
lastLogIndex := len(rf.log) - 1
if args.PrevLogIndex > lastLogIndex{
reply.Success = false
return
}
if rf.log[lastLogIndex].Term != args.PrevLogTerm {
reply.Success = false
return
}
// 3. If an existing entry conflicts with a new one (same index
// but different terms), delete the existing entry and all that
// follow it(5.3)
// 3.1 说明[args.PrevLogIndex+1, conflict)是重复的,可以保留不删除
// 3.2 当然也可以直接删除args.PrevLogIndex后面的所有内容
start := args.PrevLogIndex + 1
for i, newOne := range args.Entries {
idx := start + i
if idx < len(rf.log) {
if rf.log[idx].Term != newOne.Term {
rf.log = rf.log[:idx]
break
}
}
}