2C的持久化本身比较简单,就是把currentTerm、voteFor、logEntries存和取就行。需要注意的点的存数据的时间点应该在这三个值有变化的时候,准确来说是这两个地方:一个是在变为follower和candidate的时候,另一个是在commit data之后。另外尤其强调是在“之后”做持久化,否则会把未提交的数据保留下来。
但是实际跑测试的时候有一个测试怎么都通不过,试了很多次,然后试了很多日志终于发现不是持久化的问题。
GO111MODULE=off go test -run 2C
Test (2C): basic persistence ...
... Passed -- 5.6 3 100 24433 6
Test (2C): more persistence ...
... Passed -- 18.0 5 988 204042 16
Test (2C): partitioned leader and one follower crash, leader restarts ...
... Passed -- 2.4 3 34 8699 4
Test (2C): Figure 8 ...
... Passed -- 31.4 5 560 106887 8
Test (2C): unreliable agreement ...
... Passed -- 5.6 5 216 76193 246
Test (2C): Figure 8 (unreliable) ...
--- FAIL: TestFigure8Unreliable2C (45.35s)
config.go:478: one(8275) failed to reach agreement
Test (2C): churn ...
... Passed -- 16.3 5 608 284683 268
Test (2C): unreliable churn ...
... Passed -- 16.2 5 932 502715 249
FAIL
exit status 1
TestFigure8Unreliable2C是在测什么呢?对于普通的Figure 8 test,测试方法会不断地断线、宕机、重连服务,而加上Unreliable这个设定会让rpc请求随机失败或者延迟。通过Figure 8 test的核心点在于不要存未commit的数据(具体图见论文Figure 8)。
而TestFigure8Unreliable2C的失败结果初看非常奇怪。在跑了无数次TestFigure8Unreliable2C后发现错误的结果大概率是这样的:5个server,4个commit结果完成一致,而另一个则完全没有commit或者只commit了前面很少的一部分,甚至可能有全量的log data但是一条都没有提交。
以这次测试结果为例:
Test (2C): Figure 8 (unreliable) ...
*** server 1 成为 leader, currentTerm 1 ***
leader 1 更新commitIndex 1, 原commitIndex 0
*** server 4 成为 leader, currentTerm 3 ***
*** server 3 成为 leader, currentTerm 4 ***
*** server 3 成为 leader, currentTerm 7 ***
leader 3 更新commitIndex 17, 原commitIndex 0
leader 3 更新commitIndex 20, 原commitIndex 17
*** server 0 成为 leader, currentTerm 10 ***
*** server 2 成为 leader, currentTerm 11 ***
*** server 1 成为 leader, currentTerm 36 ***
*** server 0 成为 leader, currentTerm 49 ***
*** server 1 成为 leader, currentTerm 51 ***
leader 1 更新commitIndex 184, 原commitIndex 20
*** server 4 成为 leader, currentTerm 55 ***
*** server 4 成为 leader, currentTerm 64 ***
*** server 3 成为 leader, currentTerm 77 ***
*** server 3 成为 leader, currentTerm 93 ***
leader 3 更新commitIndex 327, 原commitIndex 184
leader 3 更新commitIndex 328, 原commitIndex 327
leader 3 更新commitIndex 329, 原commitIndex 328
leader 3 更新commitIndex 330, 原commitIndex 329
最后结果是server1-4的结果都是对的,只有server0的commit index停留在20。
为什么呢?再打日志发现,TestFigure8Unreliable2C最后会让服务全部连线上(但是rpc请求还是可能失败),此时leader不断地向原来被分割开的服务发送同样的请求,而返回也是同样的coflict term 和 conflict index。这两个值是server告诉leader,你传的数据不对,要从这个地方重新传。
看下原来leader处的处理:
// 有冲突的情况
if reply.ConflictTerm != -1 {
for i, v := range rf.logEntries {
if v.Term == reply.ConflictTerm {
rf.nextIndex[id] = i
break
}
}
} else {
rf.nextIndex[id] = reply.ConflictIndex
}
从表现上看,这里一直在根据coflict term决定下一次传的index id会陷入调用上的死循环。所以在此处,更应该注意follower自己发的conflict index。于是修改后加了一条逻辑:
if reply.ConflictTerm != -1 {
if rf.logEntries[reply.ConflictIndex].Term == reply.ConflictTerm {
for i, v := range rf.logEntries {
if v.Term == reply.ConflictTerm {
rf.nextIndex[id] = i
break
}
}
} else {
rf.nextIndex[id] = reply.ConflictIndex
}
} else {
rf.nextIndex[id] = reply.ConflictIndex
}
这样做就解决了问题,最后顺利通过全部测试。
GO111MODULE=off go test -run 2C
Test (2C): basic persistence ...
... Passed -- 4.2 3 114 29456 6
Test (2C): more persistence ...
... Passed -- 22.7 5 2125 472533 19
Test (2C): partitioned leader and one follower crash, leader restarts ...
... Passed -- 3.9 3 102 28521 4
Test (2C): Figure 8 ...
... Passed -- 35.1 5 676 119626 7
Test (2C): unreliable agreement ...
... Passed -- 3.0 5 216 75708 246
Test (2C): Figure 8 (unreliable) ...
... Passed -- 37.3 5 4612 7879256 172
Test (2C): churn ...
... Passed -- 16.3 5 1240 1145184 697
Test (2C): unreliable churn ...
... Passed -- 16.1 5 1220 879587 526
PASS
ok .../src/raft 138.617s
最后,我自己总结出的本项目的打日志方法,最主要的是这么几个地方:选取出leader处、leader提交数据处。其他地方也需要酌情打日志,但是很可能信息太多,淹没了有用的信息。
Lab的测试期望在4分钟内跑完,最后2A8秒,2B约38秒,2C约138秒,至少在我的电脑上达到标准了。