1 引言
大约用了20多天的时间完成了6.824的lab2,期间穿插了毕业预答辩,改论文,准备外审等等事情,最终磕磕绊绊的完成了Lab2,感觉算是自己写的程序中比较具有挑战性的了,因为在实验过程中需要认真的考虑并发、加锁、死锁等问题,并且实际RAFT论文中省去了很多细节,而且为了尽量通过测试与优化性能,本人也对与RAFT论文中很多细节进行更改,因此该Lab的完成具有一定挑战。
实验结果
开门见山,先放结果:
对于2A部分,运行1000次,都可以全部PASS,取一组结果:
Test (2A): initial election ...
... Passed -- 3.5 3 106 29540 0
Test (2A): election after network failure ...
... Passed -- 5.5 3 158 29218 0
PASS
ok _/home/hetianfang/gocodes/DS6.824/src/raft-2C 9.028s
对于2B部分,运行1000次,也可以全部PASS,取一组结果:
Test (2B): basic agreement ...
... Passed -- 1.1 3 16 4686 3
Test (2B): RPC byte count ...
... Passed -- 2.0 3 52 116026 11
Test (2B): agreement despite follower disconnection ...
... Passed -- 5.3 3 152 38842 7
Test (2B): no agreement if too many followers disconnect ...
... Passed -- 4.4 5 228 41540 3
Test (2B): concurrent Start()s ...
... Passed -- 1.2 3 24 7126 6
Test (2B): rejoin of partitioned leader ...
... Passed -- 4.0 3 100 23143 4
Test (2B): leader backs up quickly over incorrect follower logs ...
... Passed -- 26.4 5 2132 673111 102
Test (2B): RPC counts aren't too high ...
... Passed -- 2.7 3 82 24838 12
PASS
ok _/home/hetianfang/gocodes/6.824_htf/src/raft-2B-success 48.052s
对于2C部分,运行100次,除Figure 8 (unreliable)
外所有测试均可顺利通过,但唯独Figure 8 (unreliable)
有过半的可能性无法通过,经过本人分析,该测试项目为模拟网络在长时间混乱后能够顺利进行提交,该测试项目中使用cfg.setlongreordering(true)
模拟在RPC回复时出现延迟(sometimes delay replies a long time),从而模拟长时间网络长时间的混乱,然而在模拟网络混乱后并没有恢复网络状态,本人在cfg.one(9999, servers, true)
前采用cfg.setlongreordering(false)
恢复网络秩序,最终使得程序可以顺利通过各项测试,取一组实验结果:
Test (2C): basic persistence ...
... Passed -- 6.3 3 142 37280 6
Test (2C): more persistence ...
... Passed -- 25.7 5 1692 265019 17
Test (2C): partitioned leader and one follower crash, leader restarts ...
... Passed -- 2.8 3 52 12907 4
Test (2C): Figure 8 ...
... Passed -- 34.1 5 680 125859 7
Test (2C): unreliable agreement ...
... Passed -- 40.8 5 1484 412291 301
Test (2C): Figure 8 (unreliable) htf ...
... Passed -- 30.6 5 2584 359827 20
Test (2C): churn ...
... Passed -- 16.7 5 828 187978 103
Test (2C): unreliable churn ...
... Passed -- 16.7 5 828 171317 64
PASS
ok _/home/hetianfang/gocodes/DS6.824/src/raft-2C 173.823s
工程源码
[源码](CountingStars/6.824 (gitee.com))置于Gitee中,为了省事许多实验过程中撰写的代码带有很多乱七八糟的注释以及打印,只对于最终版本的简洁性进行优化,Lab2实验中,最终经优化的代码路径为./src/raft/raft-2C-simplify
2 实现方式
Lab2分为ABC三部分,三部分内容的如下:
- A:RAFT的leader选举与心跳机制(Raft leader election and heartbeats)
- B:日志复制(Append new log entries)
- C:状态持久化(Keep persistent state that survives a reboot)
三部分功能隔离,但是三部分的实现具有交叉,例如日志复制和心跳机制实际上是一体的,而状态持久化与RPC的发起与响应相耦合,因此对于实现方式的描述不会按照三部分内容展开,而会是围绕程序逻辑。
主要类
程序主要实现基本都位于raft.go
文件之中,除此之外,本人在time.go
文件中实现了一个自定义的定时器以及一个具有限时功能的WaitGroup
,整个程序基本都围绕着Raft
这个类(Golang没有严格的对象概念,不过这样说着方便),该类主要包括自带的变量以及论文Figue2中所提及的变量,除此之外增加了applyCh
用于记录Raft
对象初始化时提供的用于提交apply新的的chan,以及记录本人声明的其他用于记录新的变量。Raft
所包含多个成员函数,其中比较重要的成员函数有:
func RaftConstructer(peers []*labrpc.ClientEnd, me int, persister *Persister, applyCh chan ApplyMsg) *Raft
func (recv *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply)
func (recv *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply)
func (recv *Raft) startElection()
func (recv *Raft) broadcastAppendEntries()
func (recv *Raft) mainLoop()
func (recv *Raft) applyLoop()
func (recv *Raft) Start(command interface{}) (index int, term int, isLeader bool)
除Raft
类外,还有ApplyMsg
,LogEntry
,RequestVoteArgs
,RequestVoteReply
等类,作用显而易见无需描述,以及一些用于表示时间设置、状态的预定义常量。
整体运行逻辑
单个节点的运行开始于其构造函数RaftConstructer
(本人对Make
内部又封装了一个构造函数),构造函数中对于各个变量进行初始化,并且启动两个新的协程mainLoop
和applyLoop
,构造函数基本流程为:
- 初始化各个变量
- 尝试从
persister
中读取历史运行信息 - 分别启动
mainLoop
和applyLoop
整个程序的运行就是基于mainLoop
和applyLoop
,是这个程序的核心,mainLoop
主要用于监测心跳是否超时以及发起选举;applyLoop
用于将日志进行提交,其中mainLoop
基本逻辑为:
- while自己这个Raft节点未被杀死:
- 如果已经心跳超时:
- 切换为candidate状态
- 等待一个随机的时间
- 通过
startElection
发起选举
- 如果自己是leader:
- 通过
broadcastAppendEntries
发起一次Append Entries - 等待一小段时间,防止心跳发送过于频繁
- 通过
- 如果已经心跳超时:
通过以上逻辑可以实现对于心跳超时的监测,以及在竞选成功后立即发送心跳包,其中所调用的startElection
和broadcastAppendEntries
两个函数具有重要作用。
而applyLoop
的基本逻辑为:
- while自己这个Raft节点未被杀死
- 提交
recv.log
中所有index小于recv.commitIndex
的日志
- 提交
applyLoop
的功能很简单,也没调用其他函数,因此程序整体运行逻辑的重点就是mainLoop
,以及其包含的startElection
与broadcastAppendEntries
。
选举与心跳
如前所述,startElection
与broadcastAppendEntries
是程序的重点,分别对应选举和日志复制两大主要功能,整个Raft实现中也只有两种RPC即选举与日志复制。
在startElection
中通过RPC调用RequestVote
,在broadcastAppendEntries
中调用AppendEntries
,因此startElection
与RequestVote
,broadcastAppendEntries
与AppendEntries
是对应的!
startElection
和broadcastAppendEntries
均用于某个节点向其他节点广播,RequestVote
与AppendEntries
则分别为接收广播的节点处理请求的回调函数。
选举广播
startElection
用于发起一次选举,其流程如下(该流程与论文有不一致):
- 自增自身的任期Term
- 向各个其他节点在规定时间内并发执行以下操作:
- 通过RPC调用
RequestVote
,请求投票 - 如果RPC请求成功并且获得了
VoteGranted
:- 增加一个自身获得的选票
- 如果RPC请求成功并且被索票节点的Term大于自身Term:
- 更新自身Term与被索票节点一致
- 更新自身State为follower
- 通过RPC调用
- 如果加上自己一票数量超过了节点数量一半,并且自己的State仍然是Candidate:
- 给自己投一票
- 相应的更新
lastVoteTerm
和votedFor
两部分有关投票信息 - 设定自身state为leader
- leader初始化(matchIndex、nextIndex数组的初始化)
- 状态持久化(如果需持久化状态有变动的话)
本人程序中与论文的差异主要在于:
- 选举过程中不存在刷新选举定时器 :目的在于减少选举失败的间隔,加速选举进程
- 选举过程中先索要其他节点的票:如果先索要自己的票,将使得在长时间的RPC过程中持有自己这一票这个资源,这更容易产生平票
- 仅在其他节点票数量满足要求时给自己投票:防止自己这一票被浪费掉
以上处理均为了加速选举进程,尽量减少资源竞争与平票
选举RPC回调函数
RequestVote
为处理投票请求的回调函数,其基本流程如下:
- 如果索票candidate的Term小于自身Term:
- 拒绝投票,返回自身任期
- return
- 如果索票的candidate日志比自身更up to date,且自己没有比当前索票的candidate更新的任期投过票:
- 更新自身state为follower
- 更新自身投票记录
- 更新自身任期Term
- 为索票的候选人投票,返回自身任期
- return
- defer中,状态持久化(如果需持久化状态有变动的话)
日志添加广播
broadcastAppendEntries
用于leader向其他节点进行日志复制与发送心跳,其基本流程如下:
设定所需添加日志结尾位置,初始化
successCnt
,followerCnt
,finishCnt
三个变量均为1-
向各个其他节点在规定时间内并发执行以下操作:
- 通过RPC调用
AppendEntries
,尝试添加日志 - 如果RPC调用成功,并且成功添加日志:
-
finishCnt
,followerCnt
,successCnt
均加一
-
- 如果RPC调用成功,未成功添加日志,但该节点仍然认同自己是leader:
-
finishCnt
,followerCnt
加一 - 更新该节点对应的
nextIndex
-
- 如果RPC调用成功,未成功添加日志,且该节点不认同自己是leaer:
-
finishCnt
加一 - 设定自身state为follower
-
- 通过RPC调用
-
如果followerCnt大于节点数量一半:
- 刷新选举定时器
- 如果successCnt大于节点数量一半:
- 更新commitIndex
状态持久化(如果需持久化状态有变动的话)
其中successCnt
, followerCnt
, finishCnt
三个变量分别表示:成功添加日志的节点数量,成功认同自身为leader的节点数量,成功完成的RPC数量,其中finishCnt
仅用于监测运行状态,实际对于程序逻辑没有影响。
日志复制RPC回调函数
AppendEntries
为处理日志复制请求的回调函数,其基本流程如下:
-
如果发送日志复制请求的节点任期小于自身任期:
- 拒绝复制,回传自身的任期
- return
刷新选举计时器
设定自身state为follower
更新自身term
-
如果请求参数中
PrevLogIndex
和PrevLogTerm
不符合自身日志:- 拒绝复制,回传自身任期,并给出一个推荐的Index即
RecommendPrevLogIndex
- return
- 拒绝复制,回传自身任期,并给出一个推荐的Index即
-
如果请求中的日志中存在自己未包含的日志:
- 截断
PrevLogIndex
之后的日志 - 添加新的日志
- 截断
更新自身
commitIndex
defer中,状态持久化(如果需持久化状态有变动的话)