MIT 6.824 Distributed Systems 分布式系统课程系列2 Lab2-Raft

MIT 6.824 Distributed Systems 分布式系统课程系列2 Lab2-Raft


1 介绍

raft的大致背景

一致性算法,跑在一批机器上,同时维护多个副本,保证所有副本数据一致。
可承受一部分机器失效。是大型网络软件的基石。
著名的前辈paxos,最近十年基本统治了这一领域。但是比较难懂、难实现。大家深受其苦,所以发明了raft。

raft的一个主要目的就是易懂。算法有效很重要,能理解它为什么有效也同样重要。

与众不同之处:

  1. Strong leader
    强化的leader。比如log只从leader发往其他节点。

  2. leader选举
    用随机timer选leader。只在常用的心跳机制上稍作改变即可实现。
    可简单快速地选出leader。

  3. 归属变更
    用joint consensus机制进行归属变更。使得配置的更改不影响集群的运行。

raft的发明者认为raft比paxos等算法更好,更适合教学,更易于实现,更有效。已被各界广泛应用。


2 Replicated state machines 状态机副本

集群中有多个状态机副本,每个机器一个,核心是log副本。
对于同一个log序列,执行的结果一定是相同的,那么如果能保证所有log副本是一致的,即使某个节点出错了,也能通过log副本恢复到一致的状态。
比如一个节点运行某个子模块出了异常丢了数据,而这时它的log副本是好的,那么可以重新处理log副本来还原到正确的状态。

一致性算法的主要性质

  1. 在非拜占庭状态下(只包含网络延迟、数据包丢失、数据包重复、数据包乱序之类)(不包含恶意数据、攻击操作)是安全的。
  2. 只要多数(超过半数)server能互联并能接入(从客户端),整个系统就能正常运转。
    比如5个server的集群可以最多失效2个还能工作。主动停止server也算作失效,失效的server之后可以再次加入集群并恢复到正常状态
  3. 不依赖时序(或者说时间戳之类的东西)来保证log的一致性。坏的系统时钟和非常严重的消息延迟可能导致系统失效。
  4. 通常情况,一旦多于半数server回应了一个rpc,那这个rpc就算是完成了。少于半数的慢的server不应影响整体系统的响应速度。

3 Paxos的问题

十年来Paxos基本成为了一致性算法的标准。地位不需多说。

缺点

  1. 非常难懂。raft作者团队自己费劲搞了一年才懂。
  2. 并没有提供一个好的开发平台,很难在其基础上进一步开发。
    很多基于paxos的系统会慢慢发现有坑,然后逐渐发展出一套自己的架构。
    Chubby(一个paxos的实现)就有一段描述(大意):paxos算法与现实情况有相当的出入。最终的系统可能是基于一个未被证明的协议。

4 为了易于理解的设计

raft的一些目标:

最难的目标:

raft的一些哲学:


5 Raft一致性算法

请打开论文 Raft论文链接

算法大致流程

从leader这个模式生出几个子问题:

  1. leader选举
  2. log复制
  3. 安全性。如果某个server处理一个log项,那么绝不可能有其他server在同个index(可理解为log项的流水号)上处理了一个不同的log项。

重要性质

  1. Election Safety
    一个term里最多只能选出一个leader。

  2. Leader Append-Only
    leader绝不会删除或改写自己的log。leader只会追加自己的log。

  3. Log Matching
    如果两份log有两个log项,他们的index和term都相等,那么这两份log在这个index之前的内容完全相同。

  4. Leader Completeness
    如果一个log项在某个term里是已提交状态,那么以后如果有了更加新的term,这个log项在新term的leader的log中必然存在。

  5. State Machine Safety
    如果一个server在某个index执行了一个log项,那么不可能有其他server在同一个index上执行一个不同的log项。

5.1 raft基础

server间用rpc通信。只有两个:

  1. RequestVote rpc(RV)
    由candidate在选举时发起(5.2)

  2. AppendEntries rpc(AE)
    leader用来发起log项的复制,同时作为一个心跳(5.3)。

发rpc是异步的,阻塞的话碰到网络故障等情况效率会很差。

5.2 leader选举

1.自己赢得选举
2.其他某个server赢得选举。
3.一直没选出来,直到超时。

5.3 log复制

5.4会讨论因leader切换而产生的刁钻问题,并证明算法的正确性。
leader会保存commit状态的最高的index,并通过AE发给其他server。
当一个follower知道某个log项是commit状态后,就会把这个log项给本地的状态机执行。

1.如果两个不同的log中的log项有同样的index和term,那么他们存的是同样的命令。
2.如果两个不同的log中的log项有同样的index和term,那么此index之前的log项完全一致。
。。。。

5.4 安全性

5.4.1 选举限制

5.4.2 前一个term的log项的提交情况

5.4.3 安全性讨论

Leader Completeness相关。我没看。。。

5.5 follower和candidate崩溃

5.6 时序和可用性

6 Cluster membership changes 集群成员变更

本课程不做要求

7 log compaction(log 压缩)

本次实验用不到这部分内容。放到下一次实验详细分析。

8 客户端交互

下一次实验讨论

9 实现和评估

讨论一些raft的现实表现

10 相关工作

介绍一些一致性算法的相关研究和对比。

11 结论

raft好。一顿吹。

12 鸣谢


开始看代码

go相关:

代码文件:


测试流程

2a

发RV的架构问题

  1. 最开始是for循环里每次等ElectionTimeout,然后依次给其他人发rv。这样阻塞肯定是不好的。

  2. 再改为并发发rv,给所有人同时发出一组rv,阻塞等结果。
    如果能快速得到超过半数的票,那么没问题。如果有半数以上的人卡了,怎么办?还是会等很长时间比如上面的6秒,才能发下次rv。

  3. 如果改成每组rv之间不阻塞(每次超时都起一个goroutine发一组rv),能不能解决。
    至少对于目前的测试程序是可以解决的。
    假设第一组rv卡了,如果阻塞等一组的结果,如果超过半数的人卡了,比如6秒,那么下一次发rv至少就要6秒。
    如果不阻塞,那么过几百ms就能再发一组rv,能加速投票。
    这里跟测试程序有关,因为它的rn.longDelays事先就决定了rpc的返回时间,而不是一旦联通就能迅速把积压的rpc返回,它一旦积压就注定要积压,所以短时间内再发一组rv是可能显著提高投票速度的。

  4. 最终形态就是每ElectionTimeout时间强制发一组rv。
    因为每次发rv时term都会+1,可以很方便做成忽略老的rv结果,只看最新一组rv。

那么就是完全异步了,那么同一时刻可能有多个发给同个server的rpc在飘,而且先后不一定。需要处理一系列相关问题。

改成异步后选举速度明显加快,测试也ok。之前时不时会有错,只有通过改它的测试代码才能过。

https://pdos.csail.mit.edu/6.824/labs/raft-structure.txt
有讨论


2b

    type ApplyMsg struct {
        CommandValid bool
        Command      interface{}
        CommandIndex int
    }

 正常情况会这样更新 cfg.logs[server][CommandIndex] = Command

TestFailNoAgree2B

里面起了5个server。测试过后可能导致打印混乱。有点奇怪,我没有仔细研究,在我封装的打印函数里判断一下killed()来解决。

TestFailAgree2B

输入106命令之前。s1断开后由于收不到ae,会发起rv,同时term+1,这样等他重连以后,leader给s1发ae时term比s1小1。这样造成s1不接受ae。
就是说s1可能比leader落后很多log,但是term却比leader大。然后反而s1要变成leader。
怎么处理。
一个可能的方法:

  1. ae的success问题,其他操作是否要关联。图2AE第4点,s1收ae时不管返回结果,只要有新的log,都存下来。
  2. 图2的RV第2点,只有发rv的人的log至少跟自己持平(5.4.1最后),才可能投它的票。这样就能保证s1至少把缺失的log补上之后才能发起投票成功。
    If votedFor is null or candidateId这句有点耐人寻味,是不是说votedFor同个term最多更新一次?也就是一个term只投一个人的票。

这里后来看到官方说法,要按规则的顺序来,一旦false,就返回,不要进行后续的操作。

TestRejoin2B

情况如下,就有点复杂。

| | | | log index->| 1|2|3|4|5|
| — | — | — | — | — | — | — | — | — | — |
| 0| leader1 | 重连| term1| 101(commit)| 102 | 103 | 104 | 104 (new) |
| 1| leader2| 断开 | term2| 101 | 103(commit)|
| 2| | 正常 | term2| 101 | 103(commit) |

1.如果term不同。term大的新。
2.如果term相同。index大的新。

再看图2收rv的处理

  1. 第一条Reply false if term < currentTerm (§5.1)。这一点因为0和2在选举中会不断升高term,并且如果对方term高的话会更新自己的term,所以是个比较随机的过程,相互攀升,2的term按理一定有机会大于等于0的term。
    这里的问题:

  2. 我开始的代码是把每个server的electionTimeout定死,导致有概率走到一个时间模式里,导致长时间2的term小于0,导致选不出leader。后来改成每次发RV都重置一下这个超时时间。仍有概率失败。

  3. 收rv时如果我的log比候选人新,会返回false。这时如果候选人的term比我新,我也应该更新自己的term等状态。否则仍有可能2只能靠自己升term,永远追不上其他人,导致选举出不了结果。

  4. 第二条规定候选人的log必须至少跟我一样新,才可能投它的票。
    0的term永远是101的term,而2是重新选举过的,肯定在101的term基础上变大,然后又执行了103命令,所以103命令的term一定大于101的term,所以2的log一定比0新。

matchIndex的问题

go相关


2c

TestFigure82C

论文图8的测试。

5.4.2的标题Committing entries from previous terms,也很容易误导。
实际它是要求leader不能去commit不是当前term的数据。因为这种情况下去提交term2,之后会被term3的数据给覆盖,造成数据不一致。
他只是为了说明图2最后log[N].term == currentTerm的必要性!

1.s5活了,成为leader,因为它log更加新,在term3上。s5会用3覆盖其他server的index2。变成(d)的状态。
2.s1继续收命令,并在term4上复制成功,那么才可以把commitIndex更新为3(跳过了index2)。这时根据Log Matching性质,之前term2的log一定已经是得到成功复制的。

过了图8的测试后,我代码实际上基本是对的了。但莫名有时超时。

一个超时问题

另一个超时问题


测试情况


总结


参考

https://pdos.csail.mit.edu/6.824/labs/lab-raft.html
https://thesquareplanet.com/blog/students-guide-to-raft/
https://thesquareplanet.com/blog/raft-qa/
https://www.zhihu.com/question/29597104

上次更新 2021-01-28