实验准备

  1. 实验代码:git://g.csail.mit.edu/6.824-golabs-2021/src/raft
  2. 如何测试:go test -run 2A -race
  3. 相关论文:Raft Extended Section 5.2
  4. 实验指导:6.824 Lab 2: Raft (mit.edu)

实验目标

实现Raft算法中Leader Election(RequestVote RPC)和Heartbeats(AppendEntries RPC)。确保只有一个Leader被选中,且若无错误该Leader会一直唯一存在,当该Leader下线或发生其他错误导致发出的数据无法被成功接收,则会产生新的Leader来替代。

一些提示

  1. 参考论文的Figure 2实现相应的结构体和函数。
  2. 通过Make()创建一个后台goroutine,用于一段时间(election timeout)没收到其他节点的消息时,通过RequestVote RPC发起选举。
  3. 尽量保证不同节点的election timeout不会让他们在同一时间发起选举,避免所有节点只为自己投票,可以通过设置随机的election timeout来实现。
  4. 测试要求Hearbeats频率每秒不高于10次。
  5. 测试要求New Leader在Old Leader下线后5秒内出现,考虑到一次换届多轮选举的情况(提示3的情况),election timeout应当足够短。
  6. 论文中对于election timeout设定在150ms - 300ms之间,前提是Heartbeat频率远远超过150ms一次。由于提示4的限制,实验中election timeout应该更大。
  7. 推荐使用time.Sleep()而不是time.Timertime.Ticker来实现定期或延迟行为。
  8. 不要忘记实现GetState()
  9. 使用rf.killed()判断测试是否关闭了该节点。
  10. RPC相关结构字段都应使用大写字母开头,这和Go语言的语法有关。

Raft简介

日志被理解为来自客户端的请求序列,在一个集群中,有唯一的一个节点用于接收客户端请求,称为"Leader Node",为了保证数据的安全性,"Leader Node"的日志应该复制给若干个节点用于备份,称为"Follower Node"。"Follower Node"的日志需要和"Leader Node"保持一致,Raft就是一种为了管理日志复制而产生的一致性算法。

领导者选举

Raft集群通常有奇数个节点,设为N,集群允许N/2个节点失效,在正常情况下,集群有1个Leader和N-1个Follower组成,当Leader失效时,会产生除了Leader和Follower外的第三种身份:Candidate。

Follower在election timeout后,身份转换为Candidate,在获取(N-1)/2个其他节点的选票后,身份转换为Leader。


主要结构

首先是Raft结构,具体的属性在论文的Figure 2中已经给出,此外还需要额外的两个属性。

  1. role:当前节点的身份。
  2. lastRecv:上一次收到其他节点消息的时间。

被注释的字段在Part A中可以忽略,且在本小节中,为了方便理解,请先忽略currentTerm字段。

  1. type Raft struct {
  2. mu sync.Mutex
  3. peers []*labrpc.ClientEnd // 集群中所有的节点
  4. // persister *Persister
  5. me int // 当前节点在peers中的索引
  6. dead int32 // 标记当前节点是否存活
  7. lastRecv time.Time
  8. role Role
  9. currentTerm int
  10. votedFor int
  11. // log []LogEntry
  12. // commitIndex int
  13. // lastApplied int
  14. // nextIndex []int
  15. // matchIndex []int
  16. }

超时选举

如果当前节点不是Leader,且超过election timeout未收到其他节点的消息,则发起选举。

此处设置election timeout在150ms - 300ms之间。

  1. func (rf *Raft) electionTimeout() time.Duration {
  2. return time.Duration(150 + rand.Int31n(150)) * time.Millisecond
  3. }

发起选举后,身份转换为Candidate,并通过RequestVote RPC获取其余节点的选票。在获取(N-1)/2个其他节点的选票后,身份转换为Leader。成为Leader后,需要立即向其余节点发送心跳,宣告自己的存在。

代码中的注释即论文Figure 2中的逻辑。

  1. func (rf *Raft) elect() {
  2. for !rf.killed() {
  3. if rf.role == Leader || time.Since(rf.lastRecv) < rf.electionTimeout() {
  4. return
  5. }
  6. /* On conversion to candidate, start election. */
  7. rf.role = Candidate
  8. /* Vote for self. */
  9. rf.voteFor = rf.me
  10. voteCount := 1
  11. /* Reset election timer. */
  12. rf.lastRecv = time.Now()
  13. /* Send RequestVote RPCs to all other servers. */
  14. for i, peer := range rf.peers {
  15. if i == rf.me {
  16. continue
  17. }
  18. reply := RequestVoteReply{}
  19. peer.Call("Raft.RequestVote", &RequestVoteArgs{
  20. CandidateId: rf.me,
  21. }, &reply)
  22. if reply.VoteGranted {
  23. voteCount++
  24. }
  25. }
  26. /* If votes received from majority of servers: become leader. */
  27. if voteCount > len(rf.peers)/2 {
  28. rf.role = Leader
  29. rf.votedFor = -1
  30. rf.heartbeat()
  31. }
  32. time.Sleep(10 * time.Millisecond)
  33. }
  34. }

Explain 1:如何理解rf.role == Leader

Follower和Candidate都可以参加选举,Candidate可以参加的原因在于,选出一个Leader可能不止一轮选举,假设非常不幸,所有节点都在同一时刻发起选举,他们都把自己的选票投给了自己,那么本轮选举将无法选出Leader。

这时候将开启第二轮选举,因此不能限制只有Follower可以参与选举。

发送心跳

不携带日志的日志复制即心跳,Leader通过心跳刷新其余节点的election timeout。Hint 4限制了心跳频率在每秒10次,因此这里让心跳一次后休眠100ms。

  1. func (rf *Raft) heartbeatInterval() {
  2. return 100 * time.Millisecond
  3. }
  4. func (rf *Raft) heartbeat() {
  5. for !rf.killed() {
  6. if rf.role != Leader {
  7. return
  8. }
  9. for i, peer := range rf.peers {
  10. if i == rf.me {
  11. continue
  12. }
  13. reply := AppendEntriesReply{}
  14. peer.Call("Raft.AppendEntries", &AppendEntriesArgs{}, &reply)
  15. }
  16. }
  17. time.Sleep(rf.heartbeatInterval())
  18. }

RPC全称Remote Procedure Call,即远程过程调用,通俗的讲就是调用其他节点上的函数。例如peer.Call("Raft.AppendEntries", &args, &reply),就是调用了对应节点的AppendEntries函数,参数是args,返回值保存在reply中。

heartbeat是主动,AppendEntries是被动。elect是主动,RequestVote是被动。

RequestVote

Candidate通过远程调用RequestVote,向其他节点索要选票。论文的Figure 2中也给出了RequestVoteArgs和RequestVoteReply的定义。在Part A中不需要关注LastLogIndex和LastLogTerm两个字段。同样的,为了方便理解,请先忽略Term的概念。

  1. type RequestVoteArgs struct {
  2. Term int
  3. CandidateId int
  4. // LastLogIndex int
  5. // LastLogTerm int
  6. }
  7. type RequestVoteReply struct {
  8. Term int
  9. VoteGranted bool
  10. }

在一轮投票中,每个节点只有一张选票,Candidate会投给自己,而Follower会投给第一个向他索要选票的Candidate。

代码中的注释即论文Figure 2中的逻辑。

RequestVote中需要刷新election timeout,一次换届多轮选举的情况。

  1. func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
  2. reply.VoteGranted = false
  3. rf.lastRecv = time.Now()
  4. /* If votedFor is null or candidateId, grant vote. */
  5. if rf.votedFor == -1 || rf.votedFor == args.CandidateId {
  6. rf.votedFor = args.CandidateId
  7. reply.VoteGranted = true
  8. }
  9. }

Explain 2:如何理解rf.votedFor == args.CandidateId

逻辑上来说这个条件是没必要的,去掉这个条件依旧能通过所有测试。我猜测这个条件是为了防止回复的网络包丢失,发送方重传,因此需要接收方再次投出选票。

AppendEntries

Leader通过AppendEntries远程调用,刷新其他节点的election timeout,保证在自己存活期间,不会有其他节点发起选举。论文的Figure 2中也给出了AppendEntriesArgs和AppendEntriesReply的定义。

被注释的字段在Part A中不需要关注,同样的,为了方便理解,请先忽略Term字段。

  1. type AppendEntriesArgs struct {
  2. Term int
  3. // LeaderId int
  4. // PrevLogIndex int
  5. // PrevLogTerm int
  6. // Entries []LogEntry
  7. // LeaderCommit int
  8. }
  9. type AppendEntriesReply struct {
  10. Term int
  11. Success bool
  12. }

Hint 2中,收到其他节点的消息,就刷新election timeout。因此RequestVote和AppendEntries中都需要更新rf.lastRecv

  1. func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
  2. reply.Success = true
  3. rf.lastRecv = time.Now()
  4. }

唯一的Leader

竞选成功的条件为voteCount > len(rf.peers)/2且每个节点只有一张选票,这保证了最多只有一个节点达到竞选成功条件,保证了Leader的唯一性。

现在考虑唯一的Leader因为某些网络问题导致Leader的心跳无法发出,那么剩余的N-1个节点将会选出新的Leader,剩余的N-1个节点可以继续提供正常的服务。那么如果Old Leader的网络问题因为某些原因恢复了,整个集群将同时出现两个Leader,这样整个集群的日志的一致性就不能保证。

引入任期

Term(任期)解决了可能出现多个Leader的问题。

Term是一个单调递增的整型值,所有节点的Term应保持一致,Term的自增只发生在从Follower到Candidate的转换中,即只有选举的时候,Term才会自增1。

再次考虑上面的问题,当Old Leader恢复后,由于剩余的N-1个节点又经历了至少一次选举,因此剩余的N-1个节点包括New Leader的Term都大于Old Leader的Term,Raft算法规定,任意节点感知到Term更高的节点,将转换为Follower;任意节点感知到Term更低的节点,将忽略对方的消息,并告知对方自己的Term。

这样,当Old Leader收到New Leader的更高Term的心跳时,会将自己的身份转换为Follower,保证了Leader的唯一性。

什么是感知到其他节点?

AppendEntries或RequestVote两个RPC的请求或回复中都包含Term,Old Leader感知到New Leader有两种途径。

  1. 收到New Leader的心跳,发现AppendEntriesArgs.Term更高。
  2. 向New Leader或其余节点发送心跳,发现AppendEntriesReply.Term更高。

实验总结

上面的图片就是本文多次提及的论文的Figure 2,我用绿色的线框选了Part A需要实现的部分。

引入Term后的代码本文就不再给出了,参照图片补充剩余的实现就可以,需要注意的是,本文为了简洁代码,省略了数据同步问题,-race可以暴露出你代码的data race问题,记得为临界资源上锁。

最后,为了证明我不是在乱写,附上我的测试结果。

MIT 6.824 Lab2A Raft之领导者选举的更多相关文章

  1. MIT 6.824 Llab2B Raft之日志复制

    书接上文Raft Part A | MIT 6.824 Lab2A Leader Election. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021 ...

  2. MIT 6.824 Lab2C Raft之持久化

    书接上文Raft Part B | MIT 6.824 Lab2B Log Replication. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021 ...

  3. 图解Raft之领导者选举

    图解Raft领导者选举,这里通过五张图来解答Raft选举的全过程: Raft集群各个节点之间是通过RPC通讯传递消息的,每个节点都包含一个RPC服务端与客户端,初始时启动RPC服务端.状态设置为Fol ...

  4. MIT 6.824 Lab2D Raft之日志压缩

    书接上文Raft Part C | MIT 6.824 Lab2C Persistence. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021/src ...

  5. MIT 6.824 lab1:mapreduce

    这是 MIT 6.824 课程 lab1 的学习总结,记录我在学习过程中的收获和踩的坑. 我的实验环境是 windows 10,所以对lab的code 做了一些环境上的修改,如果你仅仅对code 感兴 ...

  6. MIT 6.824(Spring 2020) Lab1: MapReduce 文档翻译

    首发于公众号:努力学习的阿新 前言 大家好,这里是阿新. MIT 6.824 是麻省理工大学开设的一门关于分布式系统的明星课程,共包含四个配套实验,实验的含金量很高,十分适合作为校招生的项目经历,在文 ...

  7. MIT 6.824 : Spring 2015 lab2 训练笔记

    源代码参见我的github:https://github.com/YaoZengzeng/MIT-6.824 Lab 2:Primary/Backup Key/Value Service Overvi ...

  8. 解读Raft(二 选举和日志复制)

    Leader election Raft采用心跳机制来触发Leader选举.Leader周期性的发送心跳(如果有正常的RPC的请求情况下可以不发心跳)包保持自己Leader的角色(避免集群中其他节点认 ...

  9. MIT 6.824学习笔记4 Lab1

    现在我们准备做第一个作业Lab1啦 wjk大神也在做6.824,可以参考大神的笔记https://github.com/zzzyyyxxxmmm/MIT6824_Distribute_System P ...

随机推荐

  1. nginx反向代理隐藏端口号和项目名

    可利用nginx反向代理隐藏端口号和项目名,直接输入ip即可访问对应的tomcat项目,配置nginx安装目录的nginx/conf/nginx.conf文件,修改如下:(开了两个web项目:项目名为 ...

  2. [题解] 51 nod 1340 地铁环线

    不难看出这是一道差分约束的题目. 但是如果想按照通常的题目那样去建边的话,就会发现这句话--相邻两站的距离至少是1公里--建边后就直接让整个题出现了负环(默认是按求最短路建边),没法做了. 这时我们就 ...

  3. vue大型电商项目尚品汇(前台篇)day02

    现在正式回归,开始好好做项目了,正好这一个项目也开始慢慢的开始起色了,前面的准备工作都做的差不多了. 而且我现在也开始慢慢了解到了一些项目才开始需要的一些什么东西了,vuex.router这些都是必备 ...

  4. docker 保存,加载,导入,导出 命令

    持久化docker的镜像或容器的方法 docker的镜像和容器可以有两种方式来导出 docker save #ID or #Name docker export #ID or #Name docker ...

  5. layui数据表格导入数据

    作为一个后端程序员,前端做的确实很丑,所以就学习了一下layui框架的使用.数据表格主要的问题就是传输数据的问题,这里我用我的前后端代码来做一个实际的分解. 前端部分 可以到layui官网示例中找到数 ...

  6. MyBatis热部署

    代码 import java.io.IOException; import java.lang.reflect.Field; import java.util.HashMap; import java ...

  7. nginx 源码安装配置详解(./configure)

    在"./configure"配置中,"--with"表示启用模块,也就是说这些模块在编译时不会自动构建,"--without"表示禁用模块, ...

  8. Jwt隐藏大坑,通过源码帮你揭秘

    前言 JWT是目前最为流行的接口认证方案之一,有关JWT协议的详细内容,请参考:https://jwt.io/introduction 今天分享一下在使用JWT在项目中遇到的一个问题,主要是一个协议的 ...

  9. [USACO16JAN]Angry Cows G 解题报告

    一图流 参考代码: #include<bits/stdc++.h> #define ll long long #define db double #define filein(a) fre ...

  10. lanedet项目调试记录

    苦水时间:最近深度学习调代码真的是调的郁闷,每次调都是旧的问题没有解决,新的问题又冒出来了.新的好不容易解决了,旧的问题还是没有解决思路解决不了. 正文 最近找到一个实现了很多车道线检测算法的gith ...