MIT 6.824 Lab2A Raft之领导者选举

实验准备

  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字段。

type Raft struct {
    mu          sync.Mutex
    peers       []*labrpc.ClientEnd // 集群中所有的节点
    // persister   *Persister
    me          int         // 当前节点在peers中的索引
    dead        int32       // 标记当前节点是否存活

    lastRecv    time.Time
    role        Role

    currentTerm int
    votedFor    int
    // log         []LogEntry

    // commitIndex int
    // lastApplied int

    // nextIndex   []int
    // matchIndex  []int
}

超时选举

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

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

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

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

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

func (rf *Raft) elect() {
    for !rf.killed() {
        if rf.role == Leader || time.Since(rf.lastRecv) < rf.electionTimeout() {
            return
        }
        /* On conversion to candidate, start election. */
        rf.role = Candidate
        /* Vote for self. */
        rf.voteFor = rf.me
        voteCount := 1
        /* Reset election timer. */
        rf.lastRecv = time.Now()
        /* Send RequestVote RPCs to all other servers. */
        for i, peer := range rf.peers {
            if i == rf.me {
                continue
            }
            reply := RequestVoteReply{}
            peer.Call("Raft.RequestVote", &RequestVoteArgs{
                CandidateId: rf.me,
            }, &reply)

            if reply.VoteGranted {
                voteCount++
            }
        }
        /* If votes received from majority of servers: become leader. */
        if voteCount > len(rf.peers)/2 {
            rf.role = Leader
            rf.votedFor = -1
            rf.heartbeat()
        }
        time.Sleep(10 * time.Millisecond)
    }
}

Explain 1:如何理解 rf.role == Leader
Follower和Candidate都可以参加选举,Candidate可以参加的原因在于,选出一个Leader可能不止一轮选举,假设非常不幸,所有节点都在同一时刻发起选举,他们都把自己的选票投给了自己,那么本轮选举将无法选出Leader。
这时候将开启第二轮选举,因此不能限制只有Follower可以参与选举。

发送心跳

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

func (rf *Raft) heartbeatInterval() {
    return 100 * time.Millisecond
}

func (rf *Raft) heartbeat() {
    for !rf.killed() {
        if rf.role != Leader {
            return
        }
        for i, peer := range rf.peers {
            if i == rf.me {
                continue
            }
            reply := AppendEntriesReply{}
            peer.Call("Raft.AppendEntries", &AppendEntriesArgs{}, &reply)
        }
    }
    time.Sleep(rf.heartbeatInterval())
}

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的概念。

type RequestVoteArgs struct {
    Term         int
    CandidateId  int
    // LastLogIndex int
    // LastLogTerm  int
}

type RequestVoteReply struct {
    Term        int
    VoteGranted bool
}

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

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

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

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

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

AppendEntries

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

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

type AppendEntriesArgs struct {
    Term         int
    // LeaderId     int
    // PrevLogIndex int
    // PrevLogTerm  int
    // Entries      []LogEntry
    // LeaderCommit int
}

type AppendEntriesReply struct {
    Term    int
    Success bool
}

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

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

唯一的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更高。

实验总结

MIT 6.824 Lab2A Raft之领导者选举

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

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

MIT 6.824 Lab2A Raft之领导者选举

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

Original: https://www.cnblogs.com/suqinglee/p/15549995.html
Author: 李素晴
Title: MIT 6.824 Lab2A Raft之领导者选举

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/611814/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • MySQL使用步骤

    出现mysqld: Can’t create directory ‘D:\Environment\mysql-5.7.37 \data’ (Er…

    数据库 2023年5月24日
    0144
  • Redis概述及基本数据结构

    Redis 是一个基于内存的键值型 NoSQL 数据库 特征: 键值型:value 支持多种不同数据类型,功能丰富 单线程:每个命令具备原子性 延迟低、速度快: 基于内存、IO多路…

    数据库 2023年6月16日
    074
  • 打破千篇一律,DIY属于自己独一无二的商城

    随着线上购物成为了人们的主要消费之一,搭建商城系统也成为一大热门的发展方向,在现在的电商市场中,经营的主体规模非常庞大,各种各样的电商系统琳琅满目,但是只要仔细观察就会发现,有很大…

    数据库 2023年6月14日
    0105
  • 515. 在每个树行中找最大值

    515. 在每个树行中找最大值 给定一棵二叉树的根节点 root ,请找出该二叉树中每一层的最大值。 示例1: 输入: root = [1,3,2,5,3,null,9]输出: […

    数据库 2023年6月16日
    0102
  • kettle插入更新

    kettle实现若主键存在则更新,若主键不存在则插入 Original: https://www.cnblogs.com/cheng9999/p/14085922.htmlAuth…

    数据库 2023年6月16日
    070
  • 二叉树遍历的常用方法

    概述 二叉树的遍历可以说是解决二叉树问题的基础。我们常用的遍历方式无外乎就四种 &#x524D;&#x5E8F;&#x904D;&#x5386;、 …

    数据库 2023年6月11日
    082
  • 2022蓝帽杯初赛wp(取证)

    战果 取证全解 misc出了1个 解其他题就像在坐牢 有那么一点思路,但不是完全有 手机取证_1 解压并打开阅读器,搜索627604C2-C586-48C1-AA16-FF33C3…

    数据库 2023年6月11日
    0100
  • Java 函数式编程

    有且仅有一个未实现的非静态方法的接口叫做”函数式接口” interface IFactory<t> { T create(); } </t…

    数据库 2023年6月6日
    0120
  • SpringBoot整合Redis和SpringBoot(SpringCache)整合Redis

    参考博客: https://blog.csdn.net/lingerlan510/article/details/121906813 https://blog.csdn.net/u…

    数据库 2023年6月14日
    0107
  • MySQL变量、流程控制和游标

    变量、流程控制和游标 变量 在MySQL数据库的存储过程和函数中,可以使用变量来存储查询或计算的中间结果数据,或者输出最终的结果的数据 系统变量 变量由系统定义,属于服务器级别 […

    数据库 2023年5月24日
    067
  • 第07章 MySQL单行函数

    第07章 MySQL单行函数 1. 函数的理解 1.1 什么是函数 函数在计算机语言的使用中贯穿始终,函数的作用是什么呢?它可以把我们经常使用的代码封装起来,需要的时候直接调用即可…

    数据库 2023年5月24日
    070
  • 解读《Benchmarking Hybrid OLTP&OLAP Database Systems》| StoneDB学术分享会

    编者按: Benchmarking 作为一个衡量标尺,可从不同的维度来客观公正公平的评价相关产品,例如:对应数据测评而言,有 TPC-C、TPC-H,TP-DS 等等。现有的这些测…

    数据库 2023年6月11日
    0112
  • JUC学习笔记(二)

    1.1、Synchronized synchronized 是 Java 中的关键字,是一种同步锁。它修饰的对象有以下几种: 修饰一个代码块,被修饰的代码块称为同步语句块,其作用的…

    数据库 2023年6月6日
    080
  • [LeetCode]剑指 Offer 17. 打印从1到最大的n位数

    输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数 999。 示例 1: 输入: n = 1输出: [1,2,3…

    数据库 2023年6月9日
    090
  • 【StoneDB Class】入门第一课:数据库知识科普

    在没有出现数据库之前,数据存储在文本中,这种数据存储方式不管是管理还是查询,效率都是极其低下的,数据之间没有关联性。到了1970年,IBM研究员 E.F.Codd 发表了论文&#8…

    数据库 2023年5月24日
    086
  • python 里 certifi 库的作用

    安装了certifi之后,和requests库一样也有一个cacert.pem,可以用编辑器打开cacert.pem,里面包含了很多可信任知名公司的证书/公钥库的路径,我这里是py…

    数据库 2023年6月9日
    097
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球