MIT6.824-2022 分布式系统课程实验笔记 Lab 2 Raft--xunznux

Lab2:Raft

Lab1:MapReduce

实验一链接

Lab2A:Leader Election

lab2A链接

Lab2B:日志复制

lab2B链接

Lab2C :持久化机制 persistence

lab2C链接

Lab2D:日志压缩 log compaction

lab2D链接

我的代码实现(附带详细代码注释)

直达gitee链接
https://gitee.com/zhou-xiujun/xun-go/tree/master/com/xun/Mit-6.824-xun

介绍

这是一个系列实验中的第一个,在这些实验中将构建一个具有容错性的键值存储系统。在这个实验中将实现一个复制(replicated)状态机协议-Raft。下一个实验将在Raft之上构建一个键值服务。然后,将服务“分片”到多个复制状态机上,以获得更高的性能。
复制服务通过在多个副本服务器上存储完整的状态(即数据)来实现容错。即使在一些服务器发生了故障(崩溃或网络中断),复制可以允许服务继续运行。挑战在于,故障可能导致副本持有不同的数据副本。
Raft将客户端请求组织成一个序列,称为日志,并确保所有副本服务器都看到相同的日志。每个副本按照日志顺序执行客户端请求,将它们应用到服务状态的本地副本上。由于所有存活着的副本看到相同的日志内容,它们都会以相同的顺序执行相同的请求,从而继续保持相同的服务状态。如果一个服务器发生故障但后来恢复了,Raft会负责使其日志保持最新。只要大多数服务器存活并能够相互通信,Raft就会继续运行。如果没有这样的大多数,Raft将无法取得进展,但一旦大多数服务器可以再次通信,它将从上次停止的地方继续。
在本次实验中,将实现一个作为Go对象类型的Raft协议及其相关方法,该模块旨在用于更大服务中的一个组件。一组Raft实例通过RPC互相通信以维护复制日志。你的Raft接口将支持一个无限序列的编号命令,也称为日志条目。这些条目将以索引号进行编号。具有特定索引的日志条目最终会被提交。在那时,你的Raft应将日志条目发送给更大的服务以执行。
应遵循扩展Raft论文中的设计,特别是关注图2的内容。需要实现论文中的大部分内容,包括保存持久状态和在节点失败后重新读取这些状态。但不需要实现集群成员变更(第6节)。
这份指南有用,还有关于并发锁结构的建议。为了获得更广泛的视角,可以参考Paxos、Chubby、Paxos Made Live、Spanner、Zookeeper、Harp、Viewstamped Replication和Bolosky等文献。(注意:学生指南编写于数年前,特别是第2D部分已经发生变化。在盲目遵循之前,请确保你理解某个特定实现策略的合理性。)
本次实验中最具挑战性的部分可能不是实现解决方案,而是调试。为了应对这一挑战,可能需要花时间思考如何让实现更易于调试。可以参考指导页面和这篇关于有效使用打印语句的博客文章。
课程还提供了一个Raft交互图,可以帮助理清Raft代码如何与其上层交互。
在这里插入图片描述

本次实验分为四部分。

开始

如果已经完成了实验1,那么应该已经有实验源代码的副本。如果没有,可以在实验1的说明中找到通过git获取源代码的指示。
框架代码为 src/raft/raft.go。课程还提供了一组测试,应该使用这些测试来推动实现工作,课程也将使用这些测试来评分实验。测试文件位于 src/raft/test_test.go。
当评分提交时,会在不使用 -race 标志的情况下运行测试。然而,个人应该确保代码没有竞态条件,因为竞态条件可能导致测试失败。因此,强烈建议在开发解决方案时也使用 -race 标志来运行测试。
要开始运行,请执行以下命令。不要忘记使用 git pull 获取最新的代码。

$ cd ~/6.824
$ git pull
...
$ cd src/raft
$ go test
Test (2A): initial election ...
--- FAIL: TestInitialElection2A (5.04s)
        config.go:326: expected one leader, got none
Test (2A): election after network failure ...
--- FAIL: TestReElection2A (5.03s)
        config.go:326: expected one leader, got none
...
$

代码实现

在 raft/raft.go 文件中添加代码以实现 Raft。在该文件中,可以找到框架代码,以及发送和接收 RPC 的示例。
实现必须支持以下接口,测试程序和(最终)键/值服务器将使用这个接口。可以在 raft.go 的注释中找到更多详细信息。

// create a new Raft server instance:
rf := Make(peers, me, persister, applyCh)

// start agreement on a new log entry:
rf.Start(command interface{}) (index, term, isleader)

// ask a Raft for its current term, and whether it thinks it is leader
rf.GetState() (term, isLeader)

// each time a new entry is committed to the log, each Raft peer
// should send an ApplyMsg to the service (or tester).
type ApplyMsg

服务调用 Make(peers, me, …) 来创建一个 Raft 节点。peers 参数是一个包含所有 Raft 节点(包括本节点)网络标识符的数组,用于 RPC 通信。me 参数是本节点在 peers 数组中的索引。Start(command) 要求 Raft 开始处理,将命令追加到复制日志。Start() 应该立即返回,而不等待日志追加完成(异步)。服务期望你的实现为每个新提交的日志条目发送一个 ApplyMsg 到 Make() 的 applyCh 通道参数。
raft.go 包含发送 RPC(sendRequestVote())和处理接收的 RPC(RequestVote())的示例代码。 Raft 节点应该使用 labrpc Go 包(源代码在 src/labrpc)交换 RPC请求。测试程序可以指示 labrpc 延迟、重新排序和丢弃 RPC,以模拟各种网络故障。虽然可以暂时修改 labrpc,但确保所实现的 Raft 能与原始的 labrpc 协同工作,因为课程会使用原始的 labrpc 来测试和评分实验。所实现的 Raft 实例必须仅通过 RPC 交互;例如,不允许它们使用共享的 Go 变量或文件进行通信。
后续的实验将基于这个实验,因此使用足够的时间编写稳健的代码非常重要。

先了解一下Raft协议的基本概念和工作原理。Raft是一种用于管理复制日志的一致性算法,通过将日志条目复制到多个服务器上以实现容错。Raft通过选举机制来选出一个领导者(Leader),由领导者负责处理所有客户端请求并将日志条目复制到所有从节点(Follower)上。

Raft协议的主要组成部分

  1. 领导者选举(Leader Election)

    • 当集群启动时,所有节点都是Follower。
    • 如果Follower在选举超时时间内没有收到领导者的心跳消息,它会转换为候选人(Candidate)。
    • 候选人向其他所有节点发送请求投票(RequestVote)消息以请求选票。
    • 其他节点收到请求投票消息时,会根据自身状态和请求者的日志状态来决定是否投票给该候选人。
  2. 日志复制(Log Replication)

    • 领导者接收客户端的请求并将其作为新的日志条目添加到自身的日志中。
    • 领导者将新的日志条目复制到所有的Follower中。
    • 当日志条目在多数节点上被复制并应用时,领导者会通知所有Follower该日志条目已提交。
  3. 安全性(Safety)

    • Raft保证所有已提交的日志条目在所有节点上的顺序和内容都是一致的。
    • 如果一个新的领导者被选出,它的日志必须包含所有已提交的日志条目。

代码解释

代码中的RequestVoteReply结构体用于处理RequestVote RPC请求的响应。具体解释如下:

// RequestVoteReply 是 RequestVoteHandler RPC 响应的结构体。
// 字段名称必须以大写字母开头,以便在序列化时正确处理。
type RequestVoteReply struct {
    // Term 是当前任期,用于候选人更新自身的任期。
    Term int // currentTerm, for candidate to update itself

    // VoteGranted 表示是否投票给了候选人,true 表示候选人获得了投票。
    VoteGranted bool // true means candidate received vote
}
  • Term:这是当前节点的任期。每次进行新的选举时,任期会增加。如果一个候选人在请求投票时发现自己任期落后于请求的Term,它会更新自己的Term并转变为Follower状态。

  • VoteGranted:这是一个布尔值,表示当前节点是否同意投票给请求的候选人。如果当前节点的日志比请求节点的新或当前节点已经投过票,它会拒绝投票请求。

这段代码是Raft协议中的一部分,负责在选举过程中处理投票请求的响应。通过这种方式,Raft协议能够保证在一个集群中最多只有一个领导者,并且所有的Follower都在跟随这个领导者的日志状态,从而实现一致性和容错。

Raft协议的相关知识

  • 选举超时(Election Timeout):这是Follower等待心跳消息的最大时间。如果在此时间内没有收到领导者的心跳消息,Follower会变成候选人。
  • 心跳超时(Heartbeat Timeout):领导者定期发送心跳消息给Follower,以防止它们发起选举。
  • 投票请求(RequestVote RPC):候选人用来请求其他节点投票给自己的RPC。
  • 日志条目(Log Entry):客户端请求在日志中的表示。每个日志条目都有一个唯一的索引和任期号。
  • 提交(Commit):当日志条目在多数节点上被复制后,领导者会认为该条目已经提交,并将其应用到状态机。

Raft 协议中的 term 是一个关键概念,用于帮助维护系统的一致性并处理不同节点之间的协调和领导者选举。下面是对 Raft 协议中 term 的详细解释:

什么是 Term

  • Term 是一个单调递增的整数,用于标识 Raft 系统中的不同时间段。
  • 每个 term 开始于一次领导者选举,可能会产生一个新的领导者,也可能不会(在没有领导者选出的情况下,term 会继续增加)。

Term 的作用

  1. 领导者选举:term 用于区分不同的领导者选举周期。每次选举都会增加当前的 term,确保在新的选举周期中,所有节点都能明确区分当前的选举状态。

  2. 日志一致性:term 还用于日志条目的版本控制。每个日志条目都包含其创建时的 term 信息。这样,当节点之间进行日志同步时,可以根据 term 来决定哪一个日志条目是最新的,从而保持日志的一致性。

  3. 防止旧领导者作乱:当一个节点发现自己的 term 落后于其他节点时,会自动更新自己的 term 并切换到 Follower 状态。这可以防止旧领导者继续做出决定,确保系统不会被过时的信息干扰。

Term 的操作

  • 初始值:每个节点启动时,term 初始化为 0。
  • 更新 term:节点在以下情况下更新自己的 term:
    • 收到来自其他节点的 RPC 请求或响应中包含的更高 term。
    • 进入新的选举周期,term 自增。
  • 持久化 term:每次更新 term 时,节点会将新的 term 持久化到存储中,以防节点重启后 term 信息丢失。

Term 在 Raft 中的具体使用

  • 领导者选举:在发起选举时,候选人会增加自己的 term 并向其他节点发送请求投票(RequestVote)消息。如果一个节点收到的投票请求的 term 大于自己的 term,它会更新自己的 term 并投票给这个候选人。
  • 日志复制:领导者在发送追加日志条目(AppendEntries)消息时,会附带当前的 term。跟随者节点在收到消息后,如果发现消息中的 term 大于自己的 term,会更新自己的 term 并接受日志条目。

代码示例

以下是一个简单的代码示例,展示了 term 在 Raft 中的更新和使用:

// RequestVoteArgs example RequestVoteHandler RPC arguments structure.
type RequestVoteArgs struct {
	Term         int // 候选人的任期
	CandidateId  int // 请求投票的候选人ID
	LastLogIndex int // 候选人最后日志条目的索引
	LastLogTerm  int // 候选人最后日志条目的任期
}

// RequestVoteReply example RequestVoteHandler RPC reply structure.
type RequestVoteReply struct {
	Term        int  // 当前节点的任期
	VoteGranted bool // 是否投票给候选人
}

// RequestVoteHandler example RequestVoteHandler RPC handler.
func (rf *Raft) RequestVoteHandler(args *RequestVoteArgs, reply *RequestVoteReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()

	// 设置返回的任期
	reply.Term = rf.currentTerm

	// 如果候选人的任期大于当前任期,更新当前任期并转换为Follower
	if args.Term > rf.currentTerm {
		rf.ConvertToFollower(args.Term)
		rf.VoteMsgChan <- struct{}{}
	}

	// 如果候选人的任期小于当前任期,拒绝投票
	if args.Term < rf.currentTerm {
		return
	}

	// 判断候选人的日志是否至少和当前节点的日志一样新
	lastLogTerm := rf.getLastLogTerm()
	lastLogIndex := rf.getLastLogIndex()

    //在 Raft 协议中,判断一个候选人的日志是否至少和当前节点的日志一样新需要比较两个条件:
	//
	//候选人的最后日志条目的任期号(LastLogTerm)。
	//候选人的最后日志条目的索引号(LastLogIndex)。
	//具体的判断逻辑是:
	//
	//如果候选人的最后日志条目的任期号大于当前节点的最后日志条目的任期号,则候选人的日志更新。
	//如果候选人的最后日志条目的任期号等于当前节点的最后日志条目的任期号,则比较它们的日志条目索引号,候选人的日志条目索引号大,则候选人的日志更新。
	if (args.LastLogTerm > lastLogTerm) || (args.LastLogTerm == lastLogTerm && args.LastLogIndex >= lastLogIndex) {
		// 如果当前节点是Follower并且尚未投票或投票给了该候选人,则投票给该候选人
		if rf.role == Follower && (rf.votedFor == noVoted || rf.votedFor == args.CandidateId) {
			rf.votedFor = args.CandidateId
			reply.VoteGranted = true
			rf.VoteMsgChan <- struct{}{}
		}
	}
}

// 获取当前节点最后一个日志条目的任期号
func (rf *Raft) getLastLogTerm() int {
	if len(rf.log) == 0 {
		return 0
	}
	return rf.log[len(rf.log)-1].Term
}

// 获取当前节点最后一个日志条目的索引号
func (rf *Raft) getLastLogIndex() int {
	return len(rf.log)
}

通过上面的代码示例,可以看到 term 是如何在领导者选举和日志复制过程中发挥作用的。

我的代码实现(附带详细代码注释)

直达gitee链接
https://gitee.com/zhou-xiujun/xun-go/tree/master/com/xun/Mit-6.824-xun

相关链接

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值