MIT 6.5840(6.824) 2023 Spring Lab3 FT-KVServer(3A, 3B) Summary 踩坑记录

前言

时间又过了一个月,这波是终于完成了mit 6.5840(824)的所有lab及challenge了。先上lab3吧。个人认为lab3(写完后)是除了lab1以外最简单的一个,当然还是会踩很多坑。在这篇博客里,我将描述一下lab3的整体框架及各种小坑。那么就开始吧。

整体思路及框架

前置内容

lab3A上来就是要实现一个简单的kvserver。主要包括三种操作:
1. Put(插入)
2. Append(追加)
3. Get(查询)
三种操作的具体区别这里不再赘述了,指导中写得很明白。
lab3A的要求如下:

  1. 读写均线性化
  2. 幂等性设计以应付网络错误等

对于lab3B,如果lab2D完成得很好,其实lab3B只需要添加不到20行代码就行了。个人认为特别easy,10分钟搞定,但是却标了个hard标记。可能是认为有些同学需要回头去调lab2D所以标了个hard吧。

相关设计

  • lab3A读写线性化设计
    • 踩坑记录
      1. 在最开始,我没有仔细看指导,因此最开始的设计是保证“读己之写”,也就是类似ZooKeeper的设计。每次服务端的写操作成功后,会返回一个 idx 下标,表示本次操作的日志位置。那么当下一次写操作时,对于同一客户端,需要等待 lastApplied>=idx,从而保证“读己之写”。 不出所料,寄!
      2. 然后我尝试使用类似 Quorum 的做法,每次读操作在实现“读己之写”的基础上,同时对 (n+1)/2 个服务器进行读操作,然后选出其中最新的那个。不出所料,寄!
      3. 最后,我尝试在每次读操作之前,都会在raft中插入一个NoneOp的日志,用于将之前所有的日志提交,然后再进行读操作。不出所料,寄!
    • 正确设计:正如指导中所说的,不仅仅可以将写操作加入raft日志中,将读操作也加入raft日志中就可实现读写线性化(这是因为线性化其实就是需要将操作排个顺序,由于我们在lab2中实现的raft的apply是线性化,是串行的,因此,将所有操作均加入raft日志中,即可实现线性化)
  • lab3A幂等性设计
    对于每个 Clerk ,分配一个 ClerkIdOpIndex 。用 ClientId{ClerkId,OpIndex} 进行 Client 操作的标识。我最开始是在RPC函数中进行幂等性的设计(也就是在 applier 以外),从而防止多余的日志加入到raft中。在lab4中,我还是选择了在 applier 内进行幂等性的设计,《日志任你加,秋后算总账》。同时,最初的设计中,OpIndex 并不是递增的,而是{0,1} 交替的(因为我认为 Client 是单线程的,只有当前一个 op 完成时,才会进入下一个 op ),但是测试下来这种做法是错误的,最后还是乖乖改为递增模式了。
    • 踩坑记录(伪)
      1. 在最开始,我的幂等性设计与完成提示模块(条件变量 A,将log加入到raft中,会进行对于条件变量 A 的等待,当 applier 将对应log提交时,会改变条件变量 A 的状态,并通知在此等待的goroutine)是共享的。对于非leader,应用该log时,会主动创建对应的完成提示模块。这也就是所谓的applier以外的幂等性设计。这种设计实现起来较为复杂,虽然确实可以防止多余的log进入raft,但是代码很丑。虽然整个lab3我都采用了这种方案,并且通过了万次测试,但是在之后的lab4中完全放弃了此种做法,采用了applier内,也就是状态机内进行幂等性设计。
    • 正确设计:
      1. 其实在 applier 内进行幂等性的设计很简单。只需要记录每个Client 最近应用到状态机的 OpIndex 即可,然后如果之后的Command 所对应的 OpIndex<=ClientLastOpIndex 那么就认为改操作是重复的。 同时,对于写操作来说,要实现在 applier 内的幂等性设计,需要额外开一个 ClientGetResStore ,用于存储对应 ClientIdGet 获取结果(因为可能存在应用过慢导致超时,或者网络错误导致客户端没有收到响应,因此客户端会重传)。
  • lab3b相关细节
    • 记得将所有状态机相关的内容保存到 snapshot 中(不仅仅时kv,还有 ClientGetResStore, ClientLastOpIndex 等一切需要在raft内同步的数据)。
    • raft 提交上来的 snapshotsnapshotLastIndex<kv.lastApplied, 那么就直接忽略即可。

部分结构代码

type Op struct {  
// Your definitions here.  
// Field names must start with capital letters,  
// otherwise RPC will break.  
	Type OpType  
	Key string  
	Args string  
	ClientId ClientId  
}
type ClientId struct {  
	ClerkId int  
	NextCallIndex int  
}
type OpChan struct {  
	expectTerm int  
	op Op  
	cond *utils.MCond  // 自己实现的一个可广播的条件变量
	opRes OpRes  
	index int  
	val string  
}

部分逻辑

由于lab3采用的是 applier 外的幂等设计,因此这里就不放了,避免误导大家,lab4的blog中再放吧。

总结

个人认为,在写得过程中,lab3难度小于lab2,稍次于lab4。写完后感觉,lab3难度远小于lab2和lab4。因此可以认为,除了lab1以外,lab3可以算是后3个lab中最简单的了。虽然简单,但是还是踩了很多坑。。甚至lab3虽然过了万次测试,也还在坑里呆着,懒得改了,毕竟也没错。
万次测试结果如图。

posted @   Alyjay  阅读(791)  评论(1编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
点击右上角即可分享
微信分享提示