-
lab1->MapReduce:实现一个MapReduce系统,其是一个具有Map和Reduce功能的分布式计算系统
-
lab2->Raft:实现Raft算法,其是一个分布式一致性协议,分为以下3个部分
-
2A:Leader选举
-
2B:日志复制
-
2C:持久化数据
-
lab3->分布式容错的Key/Value存储服务:搭建一个容错的Key-Value分布式服务,其是建立在lab2-Raft的一个上层建筑,需要在lab2的基础上实现日志快照等功能,对外可以提供 K-V 存储服务
-
lab4->Shared Key/Value服务:一个分片的存储服务
而本篇文章讨论的是如何学习lab2的部分,也就是实现一个Raft算法,本文会指出学习方式,以及你需要做到的一些要点、常见的坑、资料等等。你可以将本文作为一个lab2的Guide来进行阅读。
如果读者对其他lab有兴趣,也可以参照本文差不多的方式进行其他lab的学习。
首先放一张lab2A、2B、3C 的 3pass图(做完还是有满满的成就感的)
前段时间花了一周左右的时间动手写代码完成了MIT6.824课程中的lab2,达到 bug-free 属实不易,在做的过程中踩过许多坑,发现做lab的时候交流、沟通代码中的一些问题很重要,交流会开拓了我们的思路、解决方法,如果没人交流,就比较容易出现一个疑难杂症会卡好几个小时甚至几天的情况,比较容易产生气馁、想放弃的情绪,我在做lab2C部分的最后一个具有挑战性的unreliable test的时候有一个bug硬找了快两天,中途有几次想过放弃,但意志力和对技术的热情驱使我不能将就,所以坚持下来,最终会找到解决方法的思路的。
学习MIT6.824课程,我们不像MIT学生那样,学生之间可以进行讨论,有问题可以询问助教、教授,我们在做的时候只是一个人,你最多可以找到MIT6.824的交流群,但群里真正能帮助你解决一些问题的人不多,最终靠自己的比重还是比较大的,所以一些学习资料就显得比较重要,这也是本文创作的初衷,想让更多人学习到MIT6.824这门课程,学习Raft算法不止是阅读paper和一些理论知识,没有什么比直接实现一个Raft还能够深刻学习分布式一致性协议的了。其次自己实现一个Raft,想想就很有意思。
学习lab2,我希望至少需要有CAP和分布式一致性相关知识基础,起码要了解他们,知道Raft是干嘛用的,为什么需要使用Raft。这里推荐自己的一篇文章,从CAP理论延伸来讲讲分布式一致性
林林林:从CAP理论到分布式一致性协议52 赞同 · 5 评论文章正在上传…重新上传取消
1. Lab前的预备工作
1.1 如何检验Raft算法的正确性
感觉这个是大多数人首先都比较关心的问题,这个Raft算法做出来之后我怎么知道它能work呢?lab中首先会给你一个代码大致骨架,骨架中附带了很多单元测试可以测试你的代码的正确性,所以按照一定规则去实现你的算法之后run一遍单元测试就行了。
1.2 编程语言
MIT6.824 中 lab 使用的语言均为Go语言,不会Go语言的同学不要就这么打退堂鼓了,我在做lab之前也不会Go语言,但这个语言简单高效,如果有Java或者C++的基础的话上手会非常快,实际做lab的话只用到了少数并发的Go库函数,所以库函数的学习成本也不会特别高,Go的语法与Java、C++类似,熟悉几天就能上手,关于IDE我个人使用的是GoLand 30天免费体验,也可以使用比较强大的 Vim -> vim使用文档,用熟练之后效率不亚于GoLand。
在Go中使用的一些特定的Go的库函数、一些比如定时器的做法在下面介绍lab的时候会具体涉猎
1.3 阅读论文
做lab之前,首当其冲的当然就是阅读Paper
-
Raft论文:https://raft.github.io/raft.pdf (英文版)
-
有英文底子的都建议看英文版,因为难免英文原著有些意思翻译成中文会丢失了一些味道,直接看中文的话有的地方可能会有点疑惑
建议先读一遍paper,大概了解了解Raft算法的具体构思,看不懂的先跳过,第一遍不求甚解,有个大致思想即可。
1.4 Lecture
此时你大致已经对Raft有一定的想法了,相当于预习了一遍课程,这时候就可以开始上课了,如果只做lab2的话,你需要关注以下几个lecture:
-
Lecture 5: Go, Threads, and Raft
-
Lecture 6: Fault Tolerance: Raft (1)
-
Lecture 7: Fault Tolerance: Raft (2)
其中第一个lecture讲的是在使用Go语言实现Raft时会出现的几个问题,有参考价值,第二个和第三个lecture讲的是Raft算法的一些细节,这几个lecture建议都要看,对实现lab有一定的帮助。
以下是我找到的有三个课程资源:
-
YouTube的全英文无字幕高端玩家版:https://www.youtube.com/channel/UC_7WrbZTCODu1o_kfUMq88g/videos
-
这个比较适合英文特别好的,门槛比较高不是很推荐哈哈
-
simviso团队中文翻译版:https://www.simtoco.com/#/albums?id=1000019
-
翻译的不错,但缺点是没翻译完,只翻译了Lecture5、6和Lecture7的前面一点点。不过也翻译了大部分了,前面大半部分可以参考这里
-
由于是机翻,很多翻译不到位,需要有一定的英文阅读水平,看英文字幕就可以了,结合上面的中文翻译版的这里就只需要从Lecture7开始看
1.5 回顾论文
可以动手做lab之前我认为有一个指标就是你至少需要懂论文中的Figure2中的每一个字的意思,知道为什么这样子设计,Raft算法由简单易懂著称,其只有两个RPC方法,一个是AppendEntries日志复制,一个是RequestVote请求投票,以及一系列的Raft属性都在Figure2中,同时有一系列Follower、Candidate、Leader、AllServer需要遵循的规则,理解这些规则并且做lab的时候一定要按照论文中的这些规则说的去做。
当你对某个Figure2中的规则产生疑惑,请多回顾多读几遍论文,这是做lab时bug-free的关键。做之前务必保证理解了Figure2。
1.6 参考资料
最后总结几个参考资料,做lab时应该能帮到你:
-
助教的blog《Students’ Guide to Raft 》: https://thesquareplanet.com/blog/students-guide-to-raft/ (课程的助教总结了几个做lab时需要注意的几个点,和一些bug经验,做之前可以参考参考)
-
教授写的Lock锁使用建议:http://nil.csail.mit.edu/6.824/2020/labs/raft-locking.txt
-
教授写的Raft结构建议:http://nil.csail.mit.edu/6.824/2020/labs/raft-structure.txt
-
lecture的讲义:http://nil.csail.mit.edu/6.824/2020/notes/l-raft2.txt
2. 开始lab2实现Raft
6.824 Home Page: Spring 2020nil.csail.mit.edu/6.824/2020/index.html
务必遵循paper中的Figure2的每条规则来实现你的lab
现在就开始着手做lab了,进入课程主页,左边的导航中进入lab2 ,开始动手之前务必保证读一遍教授说的话,以及仔细阅读每个Task下面的Hint提示(我做的时候进的是2018的网页,提示相当少,做完才发现有2020年的网页,提示变多了好几条)
2.1 Lab2A
首先是2A,实现Leader选举,刚开始2A里的两个测试个人认为是最简单的,因为leader选举在下面的2B、2C都会迎来更大的挑战,如果你能pass2A,并不能代表Leader选举的逻辑就一定ok,也就是说在2B、2C中如果出现BUG还是有可能因为你的Leader选举逻辑有问题导致的。
下面就提几个要点帮助你快速上手实现Raft
要点只会涉及一些Raft算法无关的东西,比如语言这块,初衷是希望算法之外的东西不要浪费大家太多时间,更多关注算法的实现
2.1.1 加锁建议
一个原则,不要考虑锁性能(锁的粒度)问题,我们更关注的是算法的正确性,有可能data-race的时候请毫不犹豫加上一把大锁
可见性与原子性
由于算法中很多地方都需要并发编程,比如Candidate发起投票请求RPC,要同时给所有节点发送RPC,此时就开多个goroutine进行RPC,一旦涉及并发编程,就会有data-race、数据可见性的问题,参照Happen-before原则,在所有有data-race的地方都加一把锁,为了可见性也为了原子性。
func (rf *Raft) GetState() (int, bool) {
// 为了可见性
rf.mu.Lock()
defer rf.mu.Unlock()
return rf.currentTerm, rf.state == Leader
}
这里获取节点中的当前Term和节点的state属性的时候加锁是为了可见性,currentTerm、state这两个属性明显会有data-race,所以这里一定注意可见性,不然Agoroutine修改了currentTerm,Bgoroutine调用上面的GetState方法有可能看不到最新的currentTerm值
同时有些方法需要加一把大锁,有些方法需要你读取currentTerm,然后又要根据某个值去修改currentTerm,请毫不犹豫加上一把大锁。
死锁
如何避免死锁?大部分死锁是由于锁获取顺序问题,比如有两把锁X和Y,同时有两个线程A和B,A先获取X锁后再去请求Y锁,B先获取Y锁后再去请求X锁造成死锁。这里锁获取顺序一个是先X后Y,一个是先Y后X,有这种锁获取顺序的时候务必注意死锁问题。
也就是说,我们避免锁上加锁的问题就可以避免死锁,所以一个原则,在RPC调用的时候不要持有锁,为什么呢?举一个例子:
func (rf *Raft) TimeoutAndVote() {
rf.mu.Lock()
// 节点的选举计时器超时,开始发起选举投票RPC
for i := 0; i < peersCount; i++ {
go func(server int) {
// 发送RPC投票请求
rf.sendRequestVote(server, &request, reply)
}
}
rf.mu.Unlock()
}
func (rf *Raft) RequestVote(args *RequestVoteArgs, reply *RequestVoteReply) {
rf.mu.Lock()
// 节点收到请求投票RPC后的处理函数
rf.mu.Unlock()
}
每一个节点都是不同的Raft实例,也就是说每个节点都是不同的锁,集群3个节点就总共有3把锁
假设集群两个节点A和B,A、B同时选举超时,发起选举投票,所以A、B同时进入TimeoutAndVote函数发起选举投票,A获取了A锁,B获取了B锁,此时A发送RPC给B,进入RequestVote函数,需要获取到B锁,同时B发送RPC给A,进入RequestVote函数,需要获取到A锁,锁获取顺序一个是先A后B,一个是先B后A,所以发生了死锁。
如果我在调用RPC之前释放了锁,然后RPC结束之后重新获取锁,这样的话就避免了锁上加锁的情况,没有了多锁场景自然就没有死锁问题。所以一个原则,调用RPC过程不要持有锁。个人在做lab的时候遵循这个原则死锁就不会出现。
死锁调试
为了死锁能方便调试,你可以选择性把加锁函数封装起来,打上日志
func (rf *Raft) lock(where string) {
// DPrintf是src/raft/util.go的一个日志工具函数,通过修改其Debug值方便选择是否开启日志
DPrintf(“%s lock”, where)
rf.mu.Lock()
}
func (rf *Raft) unlock(where string) {
DPrintf(“%s unlock”, where)
rf.mu.Lock()
}
当然我是没用到这种技巧,如果你遵循上面原则,并且在Lock的地方都记得Unlock了,基本不会有死锁(我在做的时候死锁都是出现在忘记unlock上了。。。)如果打上日志,在程序死锁的时候会比较方便排查问题
2.1.2 定时器实现
节点有一段时间收不到Leader的心跳或AE(AppendEntries,下文称AE)的时候,就会变为Candidate并发起投票选举,这是2A中需要实现的,实现这个功能就需要一个定时器,那么你可以这样做:
// 设置一个时间值
const CandidateDurationMin = time.Duration(time.Millisecond * 200)
// 初始化定时器
rf.electionTimer = time.NewTimer(CandidateDuration)
// 另外开一个线程进行不断循环
for !rf.isKilled {
// 阻塞
<-rf.electionTimer.C
// timeout之后往下走
if rf.isKilled {
break
}
// here 2A…
// 重新倒计时
rf.electionTimer.Reset(time.Duration(CandidateDuration))
}
timer的实现是依靠Go中的channel管道来做的,可以理解为一个阻塞队列,等你设置的timeout之后就会往阻塞队列里面放值, <-rf.electionTimer.C 这行代码在timeout之前会被阻塞,这样就实现了定时器的功能。在收到心跳或者AE的时候就像最后一行调用Reset函数那样重置定时器,这样就能保证Follower收到RPC就永远不会发起一个投票选举。若想马上开始走定时器逻辑:
rf.electionTimer.Reset(time.Duration(0))
2.1.3 等待RPC建议
一个节点变为Candidate后,会发起投票选举,向其余所有节点发送RPC,此时若获取到大多数选票(3个节点就只需要获取到1票,和自己的一票一共两票)就可以返回并声明自己是Leader,换句话说,3个节点发送2次RPC的情况下,收到其中一个RPC投票OK的响应,主线程就可以继续往下做Leader的逻辑了,不需要等待另一个RPC投票响应。那么这种逻辑怎么做呢?
WaitGroup
我使用了比较取巧的waitGroup的方式(个人浅显理解感觉很像Java的CountDownLatch,就直接拿来当CountDownLatch来用了)
var wg sync.WaitGroup
wg.Add(1)
for i := 0; i < peersCount; i++ {
go func(server int) {
// RPC
rf.sendRequestVote(server, &request, &reply);
// if 大多数ok 或 全部节点RPC都结束
if reply.xxx {
defer func() {
if err := recover(); err != nil {
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后
前端CSS面试题文档,JavaScript面试题文档,Vue面试题文档,大厂面试题文档,需要的读者可以戳这里免费领取!
4652)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-g9OKXvSq-1712334934653)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:前端)
最后
前端CSS面试题文档,JavaScript面试题文档,Vue面试题文档,大厂面试题文档,需要的读者可以戳这里免费领取!
[外链图片转存中…(img-2N5b9gsU-1712334934653)]
[外链图片转存中…(img-4WnVwsX1-1712334934653)]