「实验记录」MIT 6.824 Raft Lab2C Persist
#Lab2C - Persist
- I. Source
- II. My Code
- III. Motivation
- IV. Solution
- S1 - 实现persist()
- S2 - 实现readPersist()
- S3 - 持久化三字段
- S4 - 在newRaft()中初始化nextIdxs和matchIdxs
- S5 - 适当缩短心跳时间
- V. Result
I. Source
- MIT-6.824 2020 课程官网
- Lab2: Raft 实验主页
- simviso 精品付费翻译 MIT 6.824 课程
- Paper - Raft extended version
II. My Code
- source code 的 Gitee 地址
- Lab2C: Persist 的 Gitee 地址
课程官网提供的 Lab 代码下载地址,我没有访问成功,于是我从 Github 其他用户那里 clone 到干净的源码,有需要可以访问我的 Gitee 获取
III. Motivation
提出 Raft 的主要目的,是为了解决容错问题,即使集群中有一些机器发生了故障,也不影响整体的运作(对外提供的服务)
我用一个 demo 来说明,假设我们的需求一直都是自己的 PC 能够顺利访问云端的资源(HTTP 或数据库)服务器。在服务器稳定在线的情况下,我们去访问它,一点问题都没有
但是,如果那唯一的一台服务器掉线了,那么我们将无法再访问,即对外的服务到此停止。这是我们无法忍受的,我们希望提供服务的一方能够保持稳定,时时刻刻为我提供访问服务。这就是我们的需求
好,现在问题摆在眼前,提供服务的一方怎样保证稳定性?让唯一的那台服务器永远维持稳定的状态,不允许宕机?这非常地不现实,就好比让一个人练成金刚不坏之身
所以,我们只能琢磨是否可以通过添加服务器的数量来确保对外服务的稳定。更近一步,即是现在服务器不再只有一台,扩充到 3 台,这 3 台中有一台是 primary 服务器,也主要由它对外提供服务;其他 2 台是 secondary 服务器(后备力量),拥有和 primary 服务器相同的数据内容
在 primary 服务器出现故障的时候,secondary 服务器顶上去,替代它的位置。这样就可以保持稳定的对外服务了
这就是我们应对资源服务器崩溃的最常用最有效的法子,但是想实现这个想法,首先要解决数据同步的问题,即如何确保 secondary 服务器拥有和 primary 服务器同样的内容?
这个同步问题,在学术上被称为共识算法,最经典的共识算法是 Paxos,但是它太难理解了。于是,斯坦福那帮人想出了更为简便的共识算法,即 Raft
通过 Raft 算法就可以同步集群中服务器的内容。要实现该算法,分三步走,5 - The Raft consensus algorithm 章节中的 Leader Election、Log Replication 和 Safety
本文主要针对第三步,Lab2C: persist 展开讲解,如有 Lab2A: Leader Election 或 Lab2B: Log Replication 的需要,请移步
IV. Solution
Lab2C: persist 主要就是为了解决一个问题,即网络中的复杂情况会导致集群中的机器掉线 OR 机器本身的宕机。我们希望发生此类的情况,Raft 也能很好地应对
论文中的图 2 也提到了要完成持久化操作,我们需要保存哪些字段放在磁盘中,最重要的莫过于 log[]
、curTerm
和 votedFor
三个字段。其他的例如 nextIdxs[]
和 matchIdxs[]
是可以通过这三个字段即时计算出来的,从工程角度上来讲可以不用保存,这样减少了读写磁盘的时间,从一定程度上提高了 Lab2C: persist 的效率
好,话不多说,我们可以直接开始具体的编码工作了,只要在 Lab2B: Log Replication 确保没有问题的情况下,Lab2C: persist 将会容易很多
S1 - 实现persist()
第一步,就是要实现持久化函数,即 raft.go:persist()
,这个函数干的事情,即是将 log[]
、curTerm
和 votedFor
三个字段写入磁盘,具体如何写入,这不是我们操心的事,我们只需要将其序列化交给 Encoder
即可,
func (rf *Raft) persist() {// Your code here (2C).// Example:// w := new(bytes.Buffer)// e := labgob.NewEncoder(w)// e.Encode(rf.xxx)// e.Encode(rf.yyy)// data := w.Bytes()// rf.persister.SaveRaftState(data)w := new(bytes.Buffer)e := gob.NewEncoder(w)e.Encode(rf.curTerm)e.Encode(rf.votedFor)e.Encode(rf.log)data := w.Bytes()rf.persister.SaveRaftState(data)
}
就像这样,按照上面助教已给出的例子,照猫画虎即可
S2 - 实现readPersist()
我们也要实现读的具体操作,同样如何去读取磁盘也不是我们关心的事,我们只需要调用 Decoder
分解序列化即可,
func (rf *Raft) readPersist(data []byte) {if data == nil || len(data) < 1 { // bootstrap without any state?return}// Your code here (2C).// Example:// r := bytes.NewBuffer(data)// d := labgob.NewDecoder(r)// var xxx// var yyy// if d.Decode(&xxx) != nil ||// d.Decode(&yyy) != nil {// error...// } else {// rf.xxx = xxx// rf.yyy = yyy// }r := bytes.NewBuffer(data)d := gob.NewDecoder(r)var curTerm intvar votedFor intvar log []LogEntryif d.Decode(&curTerm) != nil || d.Decode(&votedFor) != nil || d.Decode(&log) != nil {DPrintf("read persist fail\n")} else {rf.curTerm = curTermrf.votedFor = votedForrf.log = log}
}
又是一个照猫画虎,跟着助教的写法来就可以。每一次的 raft.go:Make()
都会调用 readPersist()
读取已持久化的数据用以初始化,所以它不需要我们操心 OR 考虑应该在何处调用,自带的框架已经帮我们安排好了
S3 - 持久化三字段
我们要在 raft.go
、appendEntries.go
和 requestVote.go
中寻找到流程中更新 log[]
、curTerm
和 votedFor
三个字段的地方,然后在它们完成操作之后持久化此类的数据
在 raft.go
中第一处出现在 raft.go:Start()
中,即 client 追加日志条目之后,要持久化,
func (rf *Raft) Start(command interface{}) (int, int, bool) {// Your code here (2B).rf.mu.Lock()defer rf.mu.Unlock()/*------------Lab2C Persist---------------*/defer rf.persist()index := -1term := rf.curTermisLeader := rf.role == Leaderif isLeader {rf.log = append(rf.log, LogEntry{Idx: rf.lastLogIdx() + 1, Term: term, Cmd: command})index = rf.lastLogIdx()}return index, term, isLeader
}
第二处出现在 raft.go:run()
的 candidate 阶段,因为 follower 成为 candidate 之后会更新 curTerm
和 votedFor
,
func (rf *Raft) run() {for !rf.killed() {switch rf.role {case Follower:...case Candidate:rf.mu.Lock()rf.curTerm++rf.votedFor = rf.merf.voteCount = 1rf.persist()rf.mu.Unlock()...}time.Sleep(10 * time.Millisecond)}
}
之后,就是 requestVote.go
中,需要在 RequestVote()
中添加持久化操作,
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {// Your code here (2A, 2B).rf.mu.Lock()defer rf.mu.Unlock()/*------------Lab2C Persist---------------*/defer rf.persist()...
}
就像上述一样,调用 defer rf.persist()
即可,这样就可以在离开函数之前将数据写回磁盘,以及在 sendRequestVote()
中,
func (rf *Raft) sendRequestVote(server int, args *RequestVoteArgs, reply *RequestVoteReply) bool {ok := rf.peers[server].Call("Raft.RequestVote", args, reply)rf.mu.Lock()defer rf.mu.Unlock()if !ok {return ok}term := rf.curTerm/* 自身过期的情况下,直接不再唱票 */if rf.role != Candidate || args.Term != term {return ok}/* 碰到一个任期比自己高的人 */if reply.Term > term {rf.curTerm = reply.Termrf.role = Follower /* candidate 主动回滚至 follower */rf.votedFor = NoBodyrf.persist()return ok}if reply.VoteGranted {rf.voteCount++if rf.role == Candidate && rf.voteCount > len(rf.peers)/2 {rf.role = Leader /* 至关重要 */rf.leaderCh <- struct{}{}}}return ok
}
在碰到一个任期比自己高的人之后,candidate 会更新自己的 curTerm
和 votedFor
,这里也需要注意持久化
最后来到 appendEntries.go
中,同样在 AppendEntries()
中添上 defer rf.persist()
即可,
func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {rf.mu.Lock()defer rf.mu.Unlock()/*------------Lab2C Persist---------------*/defer rf.persist()reply.Success = falsereply.Term = rf.curTerm...
}
在 sendAppendEntries()
中碰见更新任期的情况下,也要持久化,
func (rf *Raft) sendAppendEntries(server int, args *AppendEntriesArgs, reply *AppendEntriesReply) bool {ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)rf.mu.Lock()defer rf.mu.Unlock()if !ok {return ok}term := rf.curTerm/* 自身过期的情况下,不需要在维护 nextIdx 了 */if rf.role != Leader || args.Term != term {return ok}/* 仅仅是被动退位,不涉及到需要投票给谁 */if reply.Term > term {rf.curTerm = reply.Termrf.role = Follower /* 主动回滚至 follower */rf.votedFor = NoBodyrf.persist()return ok}/*------------Lab2B Log Replication----------------*/if reply.Success {...} else {...}return ok
}
至此,已经功成大半,但测试的准确率还不是 100%,是因为有一些问题还需要考虑到
S4 - 在newRaft()中初始化nextIdxs和matchIdxs
记得要在 raft.go:Make()
的子函数 newRaft()
中写上定义 nextIdxs[]
和 matchIdxs[]
的代码,不然会在 boatcastAE()
时出现 index out of range 切片越界的问题,我想不懂为什么会这样,因为即使机器掉线了,它重新上线之后,也需要经过选举才能成为 leader,而成为 leader 的第一件事就是初始化 nextIdxs[]
和 matchIdxs[]
按理说,这两个切片不可能在发送心跳包时为空,具体什么 bug,我称之为玄学,看我代码吧,
func newRaft(peers []*labrpc.ClientEnd, me int, persister *Persister, applyCh chan ApplyMsg) *Raft {rf := &Raft{peers: peers,persister: persister,me: me,role: Follower,voteCount: 0,curTerm: 0,votedFor: NoBody,grantVoteCh: make(chan struct{}, ChanCap),leaderCh: make(chan struct{}, ChanCap),heartBeatCh: make(chan struct{}, ChanCap),commitCh: make(chan struct{}, ChanCap),nextIdxs: make([]int, len(peers)),matchIdxs: make([]int, len(peers)),applyCh: applyCh,commitIdx: 0,appliedIdx: 0,}/* 下标从 1 开始,这非常重要,0 号位置存放默认题目 */rf.log = append(rf.log, LogEntry{Idx: 0, Term: 0})for i := range rf.peers {rf.nextIdxs[i] = rf.lastLogIdx() + 1rf.matchIdxs[i] = 0}return rf
}
这样就不会再出现 index out of range 的情况了
S5 - 适当缩短心跳时间
上面的几种手段已经能够解决大部分错误了,测试基本能够到达 200 次只出现一个失败的情况,即 one(%v) fail to agreement
这错误提示的意思就是集群不能在有效的时间内选出 leader,进而完成日志同步的工作
很明显,就是选主不够快,最简单的办法,即是缩短心跳时间,虽然在 Lab2: Raft 实验主页 中建议我们心跳 1 s 中不超过十次,但是按照 100 ms 的频率,从工程角度来看是不行的,我改成了 90 ms 就没有此类的错误了,
ElectionTimeOut = 250 * time.Millisecond /* 要远大于论文中的 150-300 ms 才有意义,当然也要保证在 5 秒之内完成测试 */
HeartBeatTimeOut = 90 * time.Millisecond /* 心跳 1 秒不超过 10 次 *//* 生成随机超时时间,在 250ms~500 ms 范围之内 */
func randElectionTimeOut() time.Duration {r := rand.New(rand.NewSource(time.Now().UnixNano()))t := time.Duration(r.Int63()) % ElectionTimeOutreturn ElectionTimeOut + t
}/* 生成固定的心跳时间,固定值为 90 ms */
func fixedHeartBeatTimeOut() time.Duration {return HeartBeatTimeOut
}
所以,我还是称之为玄学。模型正确,不一定代表实现正确!
至此,已然讲明白了 Lab2C: persist 整个一套流程
V. Result
golang 比较麻烦,它有 GOPATH 模式,也有 GOMODULE 模式,6.824-golabs-2020 采用的是 GOPATH,所以在运行之前,需要将 golang 默认的 GOMODULE 关掉,
$ export GO111MODULE="off"
随后,就可以进入 src/raft
中开始运行测试程序,
$ go test -run 2C
仅此一次的测试远远不够,可以通过 shell 循环,让测试跑个两百次就差不多了
$ for i in {1..200}; go test -run 2C
这样,如果还没错误,那应该是真的通过了。分布式的很多 bug 需要通过反复模拟才能复现出来的,它不像单线程程序那样,永远是幂等的情况。也可以用我写的脚本 test_2c.py,
import osntests = 200
nfails = 0
noks = 0if __name__ == "__main__":for i in range(ntests):print("*************ROUND " + str(i+1) + "/" + str(ntests) + "*************")filename = "out" + str(i+1)os.system("go test -run 2C | tee " + filename)with open(filename) as f:if 'FAIL' in f.read():nfails += 1print("✖️fails, " + str(nfails) + "/" + str(ntests))continueelse:noks += 1print("✔️ok, " + str(noks) + "/" + str(ntests))os.system("rm " + filename)
我已经跑过两百次,无一 FAIL。之后的 Lab3: Fault-tolerant Key/Value Service 和 Lab4: Sharded Key/Value Service 都是基于 Lab2: Raft 的,要确保你实现的 Raft 算法没有 bug,不然 Labs 越做到后面越难受
相关文章:
「实验记录」MIT 6.824 Raft Lab2C Persist
#Lab2C - Persist I. SourceII. My CodeIII. MotivationIV. SolutionS1 - 实现persist()S2 - 实现readPersist()S3 - 持久化三字段S4 - 在newRaft()中初始化nextIdxs和matchIdxsS5 - 适当缩短心跳时间 V. Result I. Source MIT-6.824 2020 课程官网Lab2: Raft 实验主页simviso…...

软件详细设计总复习(三)【太原理工大学】
题型及分值: 选择 30 分,填空 20 分, 判断 10 分,简答 20 分,综合设计 20 分。 文章目录 三、行为型模式1. 命令模式2. 迭代器模式3. 观察者模式4. 状态模式5. 策略模式 三、行为型模式 1. 命令模式 举个例子&#x…...

Vue3(一):创建vue3工程、setup、vue3响应式原理、computed和watch
Vue3:第一章 一、创建Vue3.0工程1.使用vue-cli创建2.使用vite创建 二、Vue3中的响应式1.拉开序幕的setup2.ref函数3.reactive函数4.vue3中响应式的原理(1)vue2中响应式原理(2)Vue3中的Proxy 5.reactive和ref的对比6.se…...

Spring中的@Value注解详解
Spring中的Value注解详解 概述 本文配置文件为yml文件 在使用spring框架的项目中,Value是经常使用的注解之一。其功能是将与配置文件中的键对应的值分配给其带注解的属性。在日常使用中,我们常用的功能相对简单。本文使您系统地了解Value的用法。 Value…...

YSL赢麻了?SMI社媒心智品牌榜Top20公布:YSL破局夺魁,国货品牌现后起之秀
全文速览 1.数说故事联合用户说从美妆、彩妆、护肤三板块全新发布《SMI社媒心智品牌榜》。 2.圣罗兰、兰蔻、欧莱雅等法国高端美妆大牌垄断美妆《SMI社媒心智品牌榜》前三甲。 3.彩妆Top20榜单中,底妆产品稳居前列,色彩美妆占据一席之地。 4.护肤TOP…...

链式哈希,一致性哈希,倒排表
在普通的查询中,通过关键码的比较进行查找,而哈希是根据关键码直接定位到数据项 哈希冲突:同一个关键码经过哈希函数后指向同一个记录集 链式哈希 using namespace std; #define M 13 typedef int KeyType; //typedef struct //{ // KeyTyp…...
Python操作XML教程:读取、写入、修改和保存XML文档
目录 导入所需模块解析XML文档获取元素遍历XML文档写入新的元素修改元素的内容和属性删除元素保存修改后的XML文档示例演示python操作xml的常用方法 XML是一种常见的数据交换格式,在许多应用中都被广泛使用。通过掌握Python操作XML的基础知识,您将能够轻…...

Oracle数据库中了locked1勒索病毒,用友nchome配置文件损坏该如何解除
随着互联网技术的不断发展,网络安全问题也越来越受到人们的关注。其中,勒索病毒是一种比较常见的网络安全威胁。最近很多集团企业在使用Oracle数据库的过程中,遭遇到了locked1勒索病毒的攻击,导致企业的用友nchome配置文件损坏&am…...
leecode 数据库: 602. 好友申请 II :谁有最多的好友
数据导入: Create table If Not Exists RequestAccepted (requester_id int not null, accepter_id int null, accept_date date null); Truncate table RequestAccepted; insert into RequestAccepted (requester_id, accepter_id, accept_date) values (1, 2, 20…...
基于 Prometheus 的 SLO告警实战
Prometheus是一个流行的开源监控系统,它可以帮助我们收集、存储和查询应用程序或系统的时间序列数据。在使用Prometheus进行监控时,通常需要根据服务水平指标(Service Level Objectives,简称SLO)来设置告警规则。 SLO…...

调用百度API实现图像风格转换
目录 1、作者介绍2、基本概念2.1 人工智能云服务与百度智能云2.2 图像风格转换 3、调用百度API实现图像风格转换3.1 配置百度智能云平台3.2 环境配置3.3 完整代码实现3.4 效果展示3.5 问题与分析 1、作者介绍 张元帮,男,西安工程大学电子信息学院&#…...

5个最好的WooCommerce商城自动化动作来增加销售量
您是否正在寻找简单智能的方法来自动执行任务并增加 WooCommerce 商店的销售额? 通过在线商店中的自动化任务,您可以在发展业务和增加销售额的同时节省时间和金钱。 在本文中,我们将向您展示如何使用 WooCommerce商城自动化来增加销售额。 …...

打开数据结构大门——实现小小顺序表
文章目录 前言顺序表的概念及分类搭建项目(Seqlist):apple:搭建一个顺序表结构&&定义所需头文件&&函数:banana:初始化:pear:打印:watermelon:数据个数:smile:检查容量:fireworks:判空:tea:在尾部插入数据:tomato:在尾部删除数据:lemon:在…...

一.RxJava
1.RxJava使用场景 RxJava核心思想 Rx思维:响应式编程,从起点到终点,中途不能断掉,并且可以在中途添加拦截. 生活中的例子: 起点(分发事件,我饿了)->下楼->去餐厅->点餐->终点(吃饭,消费事件) 程序中的例子: 起点(分发事件,点击登录)->登录API->请求服务器-…...

如何使用 VSCode 软件运行C代码
VSCode 的下载和扩展的配置可以参考文章:VSCode 的安装与插件配置。 VSCode 是很好用的编辑器,通过给其配置 MinGW-w64 插件就可以在它上面编译运行C代码了。 在没有配置 MinGW-w64 插件时,在 VSCode 中运行下面的代码后打印如下图所示。 这…...

C# 调用Matlab打包的 DLL文件(傻瓜式操作)
1、准备Matlab代码 2. 打包 在matlab命令行窗口输入deploytool,打开MATLAB Complier,选择Library Compiler 在TYPE中选择.NET Assembly;在EXPORTED FUNCTIONS中选择要打包的文件;可以选择为自己打包的文件自定义NameSpace名称,本例中将NameSpace定义为…...

微信小程序学习实录3(环境部署、百度地图微信小程序、单击更换图标、弹窗信息、导航、支持腾讯百度高德地图调起)
百度地图微信小程序 一、环境部署1.need to be declared in the requiredPrivateInfos2.api.map.baidu.com 不在以下 request 合法域名3.width and heigth of marker id 9 are required 二、核心代码(一)逻辑层index.js(二)渲染层…...

【面试题】中高级前端工程师都需要熟悉的技能--前端缓存
前端缓存 一、前言二、web缓存分类1. HTTP缓存:2. 浏览器缓存:3. Service Worker:4. Web Storage缓存:5. 内存缓存: 三、http缓存详解1、http缓存类型a. 基于有效时间的缓存控制:b. 基于资源标识的缓存&…...

小红书数据分析:首播卖6亿,小红书直播开启新纪元!
5月22日,章小蕙在小红书开启了第一场带货直播。继董洁之后,小红书又迎来一位超级带货KOL。 据千瓜数据显示,相关话题#章小蕙小红书直播#上线不到30天,话题浏览量就高达2814.89万,笔记互动量达22.24万。 图 | 千瓜数据…...

Weex中,关于组件的水平排列竖直排列居中对齐居左对齐居右对齐低部对齐顶部对齐布局对齐说明
容器内子组件排列方向 子组件竖直方向排列(默认) 子组件水平方向排列 <style> .container {flex-direction: row;direction: ltr; } </style>子组件在父组件容器中的对齐方式 我们主要使用两个属性实现子组件在父组件的对齐方式ÿ…...

超短脉冲激光自聚焦效应
前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应,这是一种非线性光学现象,主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场,对材料产生非线性响应,可能…...

css实现圆环展示百分比,根据值动态展示所占比例
代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...
树莓派超全系列教程文档--(62)使用rpicam-app通过网络流式传输视频
使用rpicam-app通过网络流式传输视频 使用 rpicam-app 通过网络流式传输视频UDPTCPRTSPlibavGStreamerRTPlibcamerasrc GStreamer 元素 文章来源: http://raspberry.dns8844.cn/documentation 原文网址 使用 rpicam-app 通过网络流式传输视频 本节介绍来自 rpica…...

Zustand 状态管理库:极简而强大的解决方案
Zustand 是一个轻量级、快速和可扩展的状态管理库,特别适合 React 应用。它以简洁的 API 和高效的性能解决了 Redux 等状态管理方案中的繁琐问题。 核心优势对比 基本使用指南 1. 创建 Store // store.js import create from zustandconst useStore create((set)…...

CMake基础:构建流程详解
目录 1.CMake构建过程的基本流程 2.CMake构建的具体步骤 2.1.创建构建目录 2.2.使用 CMake 生成构建文件 2.3.编译和构建 2.4.清理构建文件 2.5.重新配置和构建 3.跨平台构建示例 4.工具链与交叉编译 5.CMake构建后的项目结构解析 5.1.CMake构建后的目录结构 5.2.构…...
【AI学习】三、AI算法中的向量
在人工智能(AI)算法中,向量(Vector)是一种将现实世界中的数据(如图像、文本、音频等)转化为计算机可处理的数值型特征表示的工具。它是连接人类认知(如语义、视觉特征)与…...
Spring Boot面试题精选汇总
🤟致敬读者 🟩感谢阅读🟦笑口常开🟪生日快乐⬛早点睡觉 📘博主相关 🟧博主信息🟨博客首页🟫专栏推荐🟥活动信息 文章目录 Spring Boot面试题精选汇总⚙️ **一、核心概…...
Robots.txt 文件
什么是robots.txt? robots.txt 是一个位于网站根目录下的文本文件(如:https://example.com/robots.txt),它用于指导网络爬虫(如搜索引擎的蜘蛛程序)如何抓取该网站的内容。这个文件遵循 Robots…...
docker 部署发现spring.profiles.active 问题
报错: org.springframework.boot.context.config.InvalidConfigDataPropertyException: Property spring.profiles.active imported from location class path resource [application-test.yml] is invalid in a profile specific resource [origin: class path re…...

C++使用 new 来创建动态数组
问题: 不能使用变量定义数组大小 原因: 这是因为数组在内存中是连续存储的,编译器需要在编译阶段就确定数组的大小,以便正确地分配内存空间。如果允许使用变量来定义数组的大小,那么编译器就无法在编译时确定数组的大…...