6.824 Lab2 RAFT总结

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类外,还有ApplyMsgLogEntry,RequestVoteArgsRequestVoteReply等类,作用显而易见无需描述,以及一些用于表示时间设置、状态的预定义常量。

整体运行逻辑

单个节点的运行开始于其构造函数RaftConstructer(本人对Make内部又封装了一个构造函数),构造函数中对于各个变量进行初始化,并且启动两个新的协程mainLoopapplyLoop,构造函数基本流程为:

  • 初始化各个变量
  • 尝试从persister中读取历史运行信息
  • 分别启动mainLoopapplyLoop

整个程序的运行就是基于mainLoopapplyLoop,是这个程序的核心,mainLoop主要用于监测心跳是否超时以及发起选举;applyLoop用于将日志进行提交,其中mainLoop基本逻辑为:

  • while自己这个Raft节点未被杀死:
    • 如果已经心跳超时:
      • 切换为candidate状态
      • 等待一个随机的时间
      • 通过startElection发起选举
    • 如果自己是leader:
      • 通过broadcastAppendEntries发起一次Append Entries
      • 等待一小段时间,防止心跳发送过于频繁

通过以上逻辑可以实现对于心跳超时的监测,以及在竞选成功后立即发送心跳包,其中所调用的startElectionbroadcastAppendEntries两个函数具有重要作用。

applyLoop的基本逻辑为:

  • while自己这个Raft节点未被杀死
    • 提交recv.log中所有index小于recv.commitIndex的日志

applyLoop的功能很简单,也没调用其他函数,因此程序整体运行逻辑的重点就是mainLoop以及其包含的startElectionbroadcastAppendEntries

选举与心跳

如前所述,startElectionbroadcastAppendEntries是程序的重点,分别对应选举和日志复制两大主要功能,整个Raft实现中也只有两种RPC即选举与日志复制

startElection中通过RPC调用RequestVote,在broadcastAppendEntries中调用AppendEntries,因此startElectionRequestVotebroadcastAppendEntriesAppendEntries是对应的!

startElectionbroadcastAppendEntries均用于某个节点向其他节点广播,RequestVoteAppendEntries则分别为接收广播的节点处理请求的回调函数。

选举广播

startElection用于发起一次选举,其流程如下(该流程与论文有不一致):

  • 自增自身的任期Term
  • 向各个其他节点在规定时间并发执行以下操作:
    • 通过RPC调用RequestVote,请求投票
    • 如果RPC请求成功并且获得了VoteGranted
      • 增加一个自身获得的选票
    • 如果RPC请求成功并且被索票节点的Term大于自身Term:
      • 更新自身Term与被索票节点一致
      • 更新自身State为follower
  • 如果加上自己一票数量超过了节点数量一半,并且自己的State仍然是Candidate:
    • 给自己投一票
    • 相应的更新lastVoteTermvotedFor两部分有关投票信息
    • 设定自身state为leader
    • leader初始化(matchIndex、nextIndex数组的初始化)
  • 状态持久化(如果需持久化状态有变动的话)

本人程序中与论文的差异主要在于:

  1. 选举过程中不存在刷新选举定时器 :目的在于减少选举失败的间隔,加速选举进程
  2. 选举过程中先索要其他节点的票:如果先索要自己的票,将使得在长时间的RPC过程中持有自己这一票这个资源,这更容易产生平票
  3. 仅在其他节点票数量满足要求时给自己投票:防止自己这一票被浪费掉

以上处理均为了加速选举进程,尽量减少资源竞争与平票

选举RPC回调函数

RequestVote为处理投票请求的回调函数,其基本流程如下:

  • 如果索票candidate的Term小于自身Term:
    • 拒绝投票,返回自身任期
    • return
  • 如果索票的candidate日志比自身更up to date,且自己没有比当前索票的candidate更新的任期投过票:
    • 更新自身state为follower
    • 更新自身投票记录
    • 更新自身任期Term
    • 为索票的候选人投票,返回自身任期
    • return
  • defer中,状态持久化(如果需持久化状态有变动的话)

日志添加广播

broadcastAppendEntries用于leader向其他节点进行日志复制与发送心跳,其基本流程如下:

  • 设定所需添加日志结尾位置,初始化successCntfollowerCntfinishCnt三个变量均为1

  • 向各个其他节点在规定时间并发执行以下操作:

    • 通过RPC调用AppendEntries,尝试添加日志
    • 如果RPC调用成功,并且成功添加日志:
      • finishCntfollowerCntsuccessCnt均加一
    • 如果RPC调用成功,未成功添加日志,但该节点仍然认同自己是leader:
      • finishCntfollowerCnt加一
      • 更新该节点对应的nextIndex
    • 如果RPC调用成功,未成功添加日志,且该节点不认同自己是leaer:
      • finishCnt加一
      • 设定自身state为follower
  • 如果followerCnt大于节点数量一半:

    • 刷新选举定时器
    • 如果successCnt大于节点数量一半:
      • 更新commitIndex
  • 状态持久化(如果需持久化状态有变动的话)

其中successCntfollowerCntfinishCnt三个变量分别表示:成功添加日志的节点数量,成功认同自身为leader的节点数量,成功完成的RPC数量,其中finishCnt仅用于监测运行状态,实际对于程序逻辑没有影响。

日志复制RPC回调函数

AppendEntries为处理日志复制请求的回调函数,其基本流程如下:

  • 如果发送日志复制请求的节点任期小于自身任期:

    • 拒绝复制,回传自身的任期
    • return
  • 刷新选举计时器

  • 设定自身state为follower

  • 更新自身term

  • 如果请求参数中PrevLogIndexPrevLogTerm不符合自身日志:

    • 拒绝复制,回传自身任期,并给出一个推荐的Index即RecommendPrevLogIndex
    • return
  • 如果请求中的日志中存在自己未包含的日志:

    • 截断PrevLogIndex之后的日志
    • 添加新的日志
  • 更新自身commitIndex

  • defer中,状态持久化(如果需持久化状态有变动的话)

你可能感兴趣的:(6.824 Lab2 RAFT总结)