【6.824分布式系统】Lab 2: Raft|Part 2A: Leader 选举

代码 github 链接
强烈建议先自己做。

先放上测试全部通过的结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MqIQhcAH-1652175242868)(https://secure2.wostatic.cn/static/bxiUe3Zc29ZNAqbCnPPE32/image.png)]

流程逻辑

论文中的图 2 非常重要!经常去看相关的参数和流程。

这部分主要实现 Leader 选举心跳

主要关⼼发送和接收 RequestVote RPC与选举相关的服务器规则以及与领导者选举相关的状态

  1. 2A 部分启动测试程序只需在 raft.go 所在目录命令行输入:
go test -run 2A -race 

这里加了 -race 用于检测并发冲突,建议加上。

  1. 测试程序首先会调用多次 rf = Make(...) 创建多个 Raft 节点
  2. 每个节点在 Make() 中初始化一些参数后就会go rf.ticker() 启动 ticker 协程
  3. ticker 用来循环检测是否选举超时,是不是要变为 candidate 开始选举。这里就需要写选举逻辑,请求投票 RPC,循环需要一个随机睡眠时间
  4. 选举成为 leader 后就要开启 leader 的工作,2A 部分只有心跳,也就是空的 追加日志 RPC
  5. 需要处理发送接收 RPC 的逻辑,看论文图 2 和 5.2 节,下面也挑出重点总结了。
  6. 在 Raft 节点转变身份、处理 RPC 时建议打印日志,方便后续 Debug(util.go 中有封装好的打印日志方法,可以直接用)。

其他要求

仔细查看实验手册,很多细节需要注意。

  • 测试要求leader每秒发送⼼跳RPC不超过⼗次,也就是最多 100ms 一次。
  • 要求 Raft 在旧领导者失败后的 5 秒内选举新领导者,即使需要多轮选举,需要自己选择合适的检查选举超时时间。
  • 检查选举超时需要随机睡眠时间,原因论文里有,防止节点只给自己投票一直选举失败。
  • 在死循环时加上 rf.killed() 判断,测试杀死 Raft 后直接退出。
  • RPC 所有结构首字母都要大写,不然无法外部访问。
  • 这里面使用 RPC 都是调用的 labrpc 包,位置 ../labrpc/labrpc.go,便于模拟有损网络,其中服务器可能无法访问,请求和回复可能会丢失。但是使用和 Lab 1里的一样,直接用就行
  • 注意并发安全,共享变量需要加锁。

设置参数

  • 首先在 Raft struct 中补充节点在选举中的状态参数,这里还需定义一个 Log 条目的结构体(每⼀个条⽬包含⼀个⽤户状态机执⾏的指令,和收到时的任期号)。
  • 填写RequestVoteArgsRequestVoteReply结构。注意字段名首字母都要大写才能外部访问。
  • 要实现⼼跳,需要定义 AppendEntriesArgsAppendEntriesReply 结构(虽然此时还不需要所有参数)。

论文中需要关心的部分

摘自我的论文笔记,只摘录与 2a 部分有关的。


AppendEntries RPC(追加待同步⽇志 RPC)

由 Leader 负责调⽤来复制⽇志(5.3);也会⽤作心跳(5.2)

传入参数:

term领导⼈的任期号
leaderId领导⼈的 Id,以便于跟随者重定向请求

返回值:

term当前的任期号,⽤于领导⼈去更新⾃⼰

接收者实现:

  1. 如果 term < currentTerm 就返回 false (5.1 节)
  2. 如果⽇志在 prevLogIndex 位置处的⽇志条⽬的任期号和 prevLogTerm 不匹配,则返回 false (5.3 节)
  3. 如果现有的⽇志条⽬和新的产⽣冲突(索引值相同但是任期号不同),删除现有的和之后所有的条目 (5.3 节)
  4. 追加⽇志中尚未存在的任何新条⽬
  5. 如果 leaderCommit > commitIndex ,令 commitIndex = min(leaderCommit, 新日志条目索引)

RequestVote RPC(请求投票 RPC)

由候选⼈调⽤⽤来征集选票(5.2 节)

传入参数

term候选⼈的任期号
candidateId请求选票的候选⼈的 Id

返回值

term当前任期号,以便于候选⼈去更新⾃⼰的任期号
voteGranted候选⼈赢得了此张选票时为 true

接收者实现:

  1. 如果term < currentTerm返回 false (5.2 节)
  2. 如果 votedFornull 或者为 candidateId,并且候选人的日志至少和接受者一样新,那么就给它投票(5.2 节,5.4 节)

Rules for Servers(服务器的规则)

所有服务器

  • 如果接收到的 RPC 请求或响应中,任期号T > currentTerm,那么就令 currentTerm 等于 T,并切换状态为 Follower(5.1 节)

Followers(跟随者)(5.2 节):

  • 响应来自候选人和领导者的 RPC 请求
  • 如果选举超时,都没有收到现任 Leader 的AppendEntries RPC,也没有给候选人投票:自己转变成候选人。

Candidates(候选人)(5.2 节):

  • 在转变成候选人后就立即开始选举过程
    • 自增当前的任期号(currentTerm
    • 给自己投票
    • 重置选举超时计时器
    • 发送 RequestVote RPC 给其他所有服务器
  • 如果接收到大多数服务器的选票,那么就变成 Leader
  • 如果接收到来自新的 Leader 的 AppendEntries RPC,转变成 follower
  • 如果选举过程超时,再次发起一轮选举

Leader(领导人):

  • 一旦成为领导人:发送空 AppendEntries RPCs(心跳)给每个服务器;在空闲期间重复发送,防止选举超时(5.2 节)

Raft 集群的服务器都处于三个状态之一:

  • Leader:只有一个,响应所有客户端请求
  • Follower:其余都是,不发送只响应 Leader 或 Candidate 的请求。若客户向其请求,会重定向到 Leader。
  • Candidate:选举新 Leader 时使用(5.2)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-myOdzSpu-1652175242871)(https://secure2.wostatic.cn/static/v4JBNkXgPr5imGt1soLUuJ/image.png)]

图 4:服务器状态。Follower 只响应来自其他服务器的请求。如果 Follower 接收不到消息,那么他就会变成 Candidate 并发起一次选举。获得集群中大多数选票的 Candidate 将成为 Leader。在一个任期内,Leader 保持身份直到自己宕机。

任期编号在 Raft 算法中充当逻辑时钟,每个节点都储存当前任期号,节点之间通信会交换任期号,当一个节点:

  • 当前任期号比其他节点小,更新自己的任期号
  • Leader 或 Candidate 发现自己任期号过期,立即转为 Follower
  • 收到过期的任期号请求,拒绝请求。

节点之间通信使用远程过程调用(RPCs),包含两种(第7节还增加了第三种传送快照的):

  • **请求投票(RequestVote) RPCs:**Candidate 在选举期间发起(5.2)
  • 追加条目(AppendEntries)RPCs:Leader 发起,用于复制日志和心跳(5.3)

节点未及时收到 RPC 响应会重试,能并行发起 RPCs。


5.2 Leader 选举(Leader election)

  1. 刚开始所有节点都是 Follower
    • Follwer 一段时间没接收到消息即选举超时,发起新选举。
    • Leader 周期性发送**心跳包(不含日志的 AE RPC)**给所有 Follower 来维持自己地位。
    • Follwer 只要能收到 Leader 或 Candidate 的 RPC 就保持当前状态。
  2. 开始选举。Follower 自增 term(任期号)并转为 Candidate,并行向其他节点发送 RV RPC 等待给自己投票。
    • 等待时收到 Leader 的心跳,且心跳中的任期不小于自己的当前任期,则自己变为 Follower。若小于自己的任期,则拒绝并保持 Candidate。
    • 如果同时出现多个 Candidate,选票可能被瓜分,没有人得到多数选票。则等待超时后重新选举。
    • Raft 使用随机选举超时时间(例如 150-300 毫秒)防止多次无人上任。每个节点开始选举时重制超时时间。可以让多数情况只有一个节点超时,进入下一轮赢得选举。
  3. 获得多数选票的 Candidate 变为 Leader
    • 每个节点在一个任期内,按先来先服务(5.4节还有额外限制)最多为一个 Candidate 投票
    • 成为 Leader 后向其他节点发送心跳建立权威。

我的代码逻辑

代码逻辑框架仅供参考,完整代码可以到 我的 github 查看

非常建议自己写,收获会最大

// 创建节点
func Make(peers []*labrpc.ClientEnd, me int, persister *Persister, applyCh chan ApplyMsg) *Raft {
    // ...
    go rf.ticker()
}

// 判断选举超时
func (rf *Raft) ticker() {
    for rf.killed() == false{
        // 随机睡眠
        // 如果成为了 leader,循环结束
        // if 选举超时
        go rf.candidateDo()
        // 未超时
        //...
    }
}

// candidate 要实现的请求投票
func (rf *Raft) candidateDo() {
    // 并发请求投票 RPC,计算票数
    ok := rf.sendRequestVote(server, rvArgs, rvReply)
    // 票数过半,启动 leader
    go rf.leaderDo()
}

// 发送 RequestVote RPC
func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {
  ok := rf.peers[server].Call("Raft.RequestVote", args, reply)
  return ok
}

// RequestVote RPC 处理程序.
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
    // 处理逻辑看论文
}

// leader 要实现的功能
func (rf *Raft) leaderDo() {
    for rf.killed() == false {
          // 失去 leader 身份,结束循环
          // 发追加日志 RPC,实现心跳
          ok := rf.sendAppendEntries(server, aeArgs, aeReply)
          // 心跳间隔睡眠
    }
}

// 发送 AppendEntries RPC
func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {
  ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)
  return ok
}

// AppendEntries RPC 处理程序.
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
    // 处理逻辑看论文
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值