实验内容

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中

一次正常交互过程

  1. client 向其Clert对象调用方法 
  2. Clerk找到一个kvserver, 其raft实例是leader
  3. kvserver调用raft.Start()来提交操作Op struct,同时返回index
  4. raft层将该Op提交
  5. kvserver执行被提交的日志
  6. kvserver执行完毕,发送rpc reply.
  7. 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会重试. 

参考他人代码, 发现:

  1. 重试是Clerk负责的,不是KVS.
  2. 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多.

测试TestPersistOneClient10次出现如下结果:

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,继续在运行。

目前还没解决这个问题。

参考资料

  1. https://blog.csdn.net/qq_4239...

Tsukami
9 声望9 粉丝

语雀: [链接]