MIT6.824 lab 3A 记录与bug总结
debug真的很搞心态,不过对理解很有帮助(我觉得,当然看别经验贴可能效率会更高)。anyway如果一次都没跌倒过,我也许永远都注意不到这些bug呢
所以这里 严重剧透警告, 严重剧透警告 , 严重剧透警告
欢迎批评指正,欢迎讨论补充,欢迎分享自己遇到的corner case
3A
fuck the lab
- 出问题时不要害怕再次重头阅读你写的代码(log看半天看不出来)
- 理解测试很有必要
p1, p2 := make_partition()
制造网络分区,然后把当前leader放到minority(p2)中
- 乱序无处不在
bug记录
- 小心一切脱离了raft mock序列的问题
- 小心一切二次加工raft mock序列的问题
- TODO: summary
过滤重复请求
- 在kv的applyer中过滤
- 优化:在接收client请求时可以可以过滤,用于加速但是比较tricky
首先说明一下我的实现的大概流程:
1 | func CommandHandler() { |
CommandHandler
负责接收客户端的请求,然后通过Start()
交给raftapplyer
接收到raft层的commit后应用到状态机
考虑client重复请求的问题,以下是我的经历:
- 仅在
CommandHandler
中,Start()
调用前,根据client携带的seqNum判断重复请求,然后过滤- 为什么我要先在
CommandHandler
中判?因为我觉得,如果在applyer
中判,那么raft日志中就会有很多重复的日志,因为没经过过滤直接Start()
- 为什么我要先在
- 仅在
applyer
执行(execute
)前判断- 为什么这时我把
CommandHandler
中的判断删了?因为我觉得两头判引入复杂性
- 为什么这时我把
- 在
CommandHandler
和applyer
中都判断重复- 减少raft日志压缩的频率,并过滤重复请求
以下是结论:
- 首先第一个方案是不对的,考虑等待commit超时的情况
- 首先为了防止网络失败的情况,等待commit失败了就应该将记录的”lastApplied”信息回滚
- 这个回滚就出现大问题了,因为command可能已经在log里了,只是还没执行到 ,这样也回滚后相同的命令将可以再次进入raft log导致重复请求
- 第二个方案呢就会在raft log中记录大量重复情况,在apply时才过滤
- 问题就raft log大量重复,触发日志压缩频率高,
- 在
CommandHandler
和applyer
中都判断重复CommandHandler
中的过滤减少raft log中的重复applyer
中个过滤方式重复应用到状态机
论文中类似”滑动窗口”的解决方案
- 为什么必须使用滑动窗口? 主要是考虑一个client可以同时发送多个请求的情况
- 首先mit6.824中是不用担心的,因为已经说明一个client一次只会发送一个请求
- 发送多个请求时需要考虑乱序和rpc fail,如果不把历史回复缓存(滑动窗口),那么在rpc fail时和乱序时,seqNum递增,低seqNum就会被高seqNum覆盖,从而导致低seqNum的请求永远得不到执行
正确使用监听用的channel
使用map封装chan,让写者可以监听chan的关闭
kvserver层需要一种机制来监听raft层的apply事件,然后应用到kv状态机。ok基于这个目的很容易想到在go中可以使用channel来方便的实现。不过这可能存在一个问题:写者如何判断chan是否关闭。
一般来说规定chan写者是负责关闭的,但是因为我们的kvserver是要超时重试的,所以在channel的另一端会超时然后关闭channel,这就导致已关闭的channel写的问题。
所以可以使用一个map
来封装chan,关闭chan的时候从map删除,写时判断map中是否存在。
1 | // bug |
非线性一致性bug: 用log的Index做索引存在的问题
怎么索引watchCh: 使用uuid
我的bug就出在我用了Start()
返回的index做索引:index, _ , _ := kv.rv.Start()
。这会出现什么问题?因index在节点间是可能出现冲突的,最终会和leader一致。所以设想两个leader, L1
, L2
,我们的节点cur
发给L1
,但是发生了网络分区L1
其实已经不是leader了。而如果此时L2
的index和L1
的一致,最后L1
一定会被覆盖,并且错误的以为该index是客户端等待index从而导致不一致的情况发生。
1 | // (1) |
TODO 全局uuid办法
我其实也没有解决这个问题,使用一个大随机数做uuid然后索引。
注意Decode一个type interface
的问题
golang不熟悉:type interface内存模型,具体含义
因为我的实现中,kv数据库是一个interface,以方便扩展:
1 | type KvDb interface { |
这时decode就会出现golang菜鸟问题,如我是这么decode的:
1 | var database KvDb |
类型匹配了,也没有报错。不过这么是解码不出来任何东西的。
改成下面这样就可以了:
1 | var database BasicDB |
TODO 学习golang的type interface{}
,学习其内存模型
死锁
kv层锁后不要再尝试对raft层加锁
本质原因是并发导致的AB,BA型锁
1 | func CommandHandler() { |
死锁是这样子的:
1 | (1) (4) (3) (1) |
所以我的答案是: 别把rf锁嵌套在kv锁内。
测试超时问题
GET的处理
get不存在时直接返回空字符串和OK
我在get不存在的key的时候的处理有大问题,导致client一直重试,然后测试超时。
因为实验文档说在get不存在的key时返回空字符串""
,我是返回了,但是还返回了一个NoKey错误。然后client端for reply.Err != Ok{重试}
,这就导致了超时
watchCh阻塞问题
watchCh待缓冲,因为读者可能跑路了
watchCh
要待1个缓冲。因为读者在监听时是不可能加锁的,所以就就可能不再监听后写者立刻写导致阻塞。即(1)
后立刻调度到(2)
1 | { |
watchCh
要待1个缓冲。因为读者在监听时是不可能加锁的,所以就就可能不再监听后写者立刻写导致阻塞。即(1)
后立刻调度到(2)
1 | { |
Test: completion after heal (3A)
RPC fail 和 WrongLeader都要换一个leader重试
RPC fail和WrongLeader都换leader,因为RPC fail说明该节点大概率也挂了,而且该”少数派的leader”并不会知道自己已经不是leader(因为仍能收到心跳),如果不换leader重新尝试,并且网络恢复不及时那么该请求会一种超时。
为什么必须保存上一次的响应
基本思想类似TCP滑动窗口,buffer的本质
防乱序,有backup
responseTracker
不单单是为了”加速”,还防止了RPC fail。因为要过滤重复请求,client端要携带一些标识的信息,server根据这些信息判断重复。RPC fail时就可能出现server觉得这个client的已经接收到结果了发了个重复的请求,而实际的client是并没有收到结果所以重发请求。所以就会server不断以为是重复请求拒绝,client不断收不到结果。
TestSpeed
关闭数据竞争检测功能
3B
Gross
CommandHandler
处理client请求的入口函数,等待apply后返回结果watchCh
, applyer用于通知CommandHandler
apply情况的channeltracker
,responseTracker
,session
,sessonTracker
- 都是论文中说的用于记录”上一次response”的结构
seqNum
, 客户端发送请求时携带的用于标识次序的id,单调递增
MIT6.824 lab 3B 记录与bug总结
MIT6.824 lab 3B bug总结记录 到目前lab3为止,可以导致状态变更的,有潜在是不一致风险的事件有:状态变更,日志追加,日志恢复 这些事件在我看来是类似操作系统中”中断”的存在,...
Are You Sure You Want to Use MMAP in Your Database Management System?论文笔记
TODO review Are You Sure You Want to Use MMAP in Your Database Management System?论文笔记Abstract 我要重...