实验内容
server.go
: 添加Op
结构, 其描述了一个Get\Put\Append
操作和值
client.go
: 使用.Start()
, 完善Put(), Append(), Get()
等rpc handler.
Hint
- 调用Start()后,应该等待raft达成aggrement.
- kvserver和raft的applyer容易形成死锁
- 要格外注意, leader在提交log之前失去了leadership, 这可能被network-parition,crash, larger term导致
- 非majority中的server不处理
Get()
请求,另外读取也作为entry放入log中
一次正常交互过程
- client 向其Clert对象调用方法
- Clerk找到一个kvserver, 其raft实例是leader
- kvserver调用raft.Start()来提交操作
Op struct
,同时返回index
- raft层将该
Op
提交 - kvserver执行被提交的日志
- kvserver执行完毕,发送rpc reply.
- Clerk处理reply.
故障处理
上述步骤中, 2~7都可能出现故障。
- Clerk找到非leader节点,不断等待重试.当rpc返回时,进行处理。
- raft未能提交日志,可能因为leader节点crash,或者网络分区.对外表现为rpc请求超时。
- kvserver执行时出错,那么
Clerk
端就会超时,重试即可。 - kvs执行完,但reply丢失或延迟,导致Clerk端重试。KVS检测到该命令已执行,直接返回结果。
- 注意要检测冗余的reply,Clerk段确保只有上一条命令完成后,才开始下一条命令,每条命令以
(Cid, Seq)
标记,cid用随机数即可,但seq必须单调递增。而KVS端则维护一个表,维护各个Cid最大的Seq。
实现
CLerk
首先 CLerk端复制与KVServer联系,并且等待rpc reply返回.
一个CLerk一次只执行一个请求(带有Cid, Seq
).
何时终止重试? 直到KVServer在超时前返回reply.
Op
Op
结构除了操作类型,对应的Key, Val外还有Cid, Seq
两个域.
KVServer
交互:
KVServer向raft leader发送了日志,raft将其提交后会把该日志发往applyCh
, 那么KVServer需要监控applyCh
中的内容,并且根据其中的内容来更新kv.sm
. 执行命令后,通知entryIndex对应的rpc handler。rpc handler检测该命令是否是自己需要的。
示意图:
KVServer --> rpc request ---------> raft instance
↑ ↓
kv.opch[logEntryIndex] ↓
↑ ↓
kv. applyer <----- applyCh <----- rf.applyer
Op ApplyMsg
每个rpc handler调用kv.rf.Start(args.Op)
得到一个entryIndex
, 随后要监听想要的日志是否出现, 即op <- kv.opch[entryIndex]
.
还要判断: args.Op == op
, 因为raft不保证该index下的日志一定是你想要的, 你的日志可能被leader强行覆盖掉.
遇到的bug
Client端rpc handler存在不正确的结束条件
Get()
, PutAppend()
方法应该得到想要结果后才退出, 其余情况都要重试.
kv.seqRecord更新
首先, seqRecord
给kvserver来进行幂等性判断的.
那么seqRecord
应该等到日志被提交之后再更新,而不是在rpc handler开头更新
// kvraft.go: KVServer.apply()
// ..
maxSeq, ok := kv.seqRecord[op.Cid]
if !ok || op.Seq > maxSeq {
switch op.Type {
case "Put":
kv.sm[op.Key] = op.Val
case "Append":
kv.sm[op.Key] += op.Val
}
kv.seqRecord[op.Cid] = op.Seq // update
kv.debugPrint(fmt.Sprintf("apply index=%v, type=%v, key=%v, val=%v", index, op.Type, op.Key, op.Val))
}
死锁
解决了seqRecord
的问题后,遇到了死锁,日志停止打印.
发现是getOpChan()
这个函数内部要获取锁,但是在其外面已经获得了锁,造成死锁.将该函数改为死锁.
修改后还是有死锁,可能因为sync chan引起的:
func (kv *KVServer) apply(op Op, index int) {
// ...
// kv.opch[index] <- op
// 这里有问题吗? 对于从节点, 谁来从chan里面取数据?
kv.getOpChan(index) <- op
}
不难发现: 对于主节点, 每次进行新操作时都会新生产一个(在rpc handler).
对于从节点, 没有rpc handler,没人来取,也就根本放不进去,会导致阻塞,怎么办?改为buffer chan就好了:
// getOpChan(index int):
// ch := make(chan Op) // sync chan
ch := make(chan Op, 1) // Ok
潜在问题: 未初始化的chan
从节点的seqRecord[index]
可能不存在, 所以要这样写:
kv.getOpChan(index) <- op // 当该kv不存在时,创建一个
kv.opch[index] <- op // 有问题
不可靠网络下实现幂等性
同一个kvserver可以分辨延迟, 重复的请求,从而避免请求重复执行.
clerk给一个kvserver A发请求但没有得到肯定回复,那么clerk会联系另一个kvserver B,那么此时如何保证操作不会被执行两次呢?
Clerk端通过唯一的(cid, seq)来标记每一个操作.
非Majority的节点不能返回读结果
如果检查到applyCh传出来的Op不是所要的,报错. 但在笔者的代码中,设计为kvserver会重试.
参考他人代码, 发现:
- 重试是Clerk负责的,不是KVS.
- rpc handler不做幂等性判断,判断被放在
MainBody()
中
笔者在rpc handler中先判断seq,如果是小于记录值,则直接返回结果.如果这是个非majority中的节点,返回的结果可能是过时的.
那么如何判断这个节点是否在Majority中?
一种做法是:读操作也要经过日志,日志中记录读操作,由leader发给各个follower。当前节点监听到这条日志,在kv.mainBody()
中更新seq和cid, 再通知rpc handler,handler再读取内容发给clerk.
TestPersistConcurrent3A()失败
目前有两种错误:
1.值不同; 如要x 0 0 y
, 只获得了""
.
2.append尾部的值缺少, 如需要x 0 0 yx 0 1 y
, 只有x 0 0 y
.
此测试会依次令节点crash, 然后重启节点. 看test_test.go
相关注释才发现StartKVServer()
有一个raft.Persister
参数,才发现KVServer在一开始要先读取持久化内容.
翻阅raft代码,发现持久化了rf.lastApplied
导致了日志rf.log[0, rf.lastApplied)
这一段内容没有发给rf.applyCh
(以为已经apply 了), 进而导致从崩溃中恢复的节点初始状态不一样!
修复: raft中取消对rf.lastApplied
的持久化即可通过测试.
最后一个测试
出现三种结果:
通过,正常结束
通过,日志输出停止但是测试不终止.
日志一直输出,超过1min
发现持续输出的情况中,后半段没有PutAppend操作,只有Get,同时伴有ErrNoKey
, 复查代码,发现当kv.sm
没有该key时返回ErrNoKey
, client会继续重试,导致不能终止.
改为当没有该key, 返回空串,同时设reply.Err = OK
, 可以结束client的循环.
不能正常结束测试
仍然有部分测试失败,
另外出现如下情况: 测试240+s结束,但是time结束时显示耗时4min多.
测试TestPersistOneClient
10次出现如下结果:
tsujo@masterTsujo[19:07:50]:~/mycode/mit_6824/src/kvraft$ grep "Passed" ./res_out/*
./res_out/out_10.txt: ... Passed -- 19.7 5 1754 149
./res_out/out_1.txt: ... Passed --2020/02/04 17:46:58 (kv=2)--apply index=14, type=Append, key=0, val=x 0 7 y
./res_out/out_2.txt: ... Passed -- 19.7 5 1735 149
./res_out/out_3.txt: ... Passed -- 19.8 5 1746 149
./res_out/out_4.txt: ... Passed --2020/02/04 18:07:26 (kv=2)--apply index=4, type=Get, key=0, val=
./res_out/out_5.txt: ... Passed --2020/02/04 18:17:30 (kv=0)--apply index=11, type=Append, key=0, val=x 0 5 y
./res_out/out_6.txt: ... Passed -- 19.9 5 1739 149
./res_out/out_7.txt: ... Passed --2020/02/04 18:37:38 (kv=1)--apply index=10, type=Get, key=0, val=
./res_out/out_8.txt: ... Passed -- 19.6 5 1728 149
./res_out/out_9.txt: ... Passed -- 19.9 5 1766 149
tsujo@masterTsujo[19:18:49]:~/mycode/mit_6824/src/kvraft$ grep -n "failed" ./res_out/*
tsujo@masterTsujo[19:19:33]:~/mycode/mit_6824/src/kvraft$ grep -i "failed" ./res_out/*
tsujo@masterTsujo[19:19:36]:~/mycode/mit_6824/src/kvraft$ grep -i "fail" ./res_out/*
./res_out/out_10.txt:FAIL kvraft 603.421s
./res_out/out_1.txt:FAIL kvraft 603.846s
./res_out/out_3.txt:FAIL kvraft 603.416s
./res_out/out_4.txt:FAIL kvraft 603.409s
./res_out/out_5.txt:FAIL kvraft 603.421s
./res_out/out_6.txt:FAIL kvraft 603.406s
./res_out/out_7.txt:FAIL kvraft 603.461s
./res_out/out_8.txt:FAIL kvraft 603.446s
打印日志发现测试通过之后(显示Passed
),部分goroutine没有被kill,继续在运行。
目前还没解决这个问题。