实验简介
实现基于raft的KV服务器,您将需要修改kvraft / client.go
,kvraft / server.go
,甚至可能是kvraft / common.go
。
一定要保证lab2是正确的,不然这个实验到处是问题。
实验参考:
- https://www.cnblogs.com/mignet/p/6824_Lab_3_KVRaft_3A.html
- https://blog.csdn.net/Miracle_ma/article/details/80184594
Client
通过RPC调用server的服务,需要考虑两个问题?
- 如果call超时,那么client就会超时重发。如果是因为网络延迟,那么server就会收到两次命令,确保服务端只执行一次命令(多次append会导致逻辑错误)。使用类似TCP的序列号,来去重。
- client应记录raft的Leader,就不用每次循环去尝试
PutAppend和Get的逻辑是完全一样的
Server
server不能立即执行client的命令,而是把命令反应给raft服务,等raft状态机提交了该命令后,再执行。
Part 3A
TestBasic3A
写了个最简单的版本
func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {
// Your code here.
cmd := Op{
Key: args.Key,
Value: args.Value,
Option: args.Op,
}
reply.WrongLeader = false
_, _, isLeader := kv.rf.Start(cmd)
if isLeader == false {
reply.WrongLeader = true
return
}
// TODO : 这样做的话,每次只能处理一个请求
kv.mu.Lock()
defer kv.mu.Unlock()
select {
case cmd := <- kv.applyCh:
op := cmd.Command.(Op)
kv.doCmd(&op)
case <- time.After(500 * time.Millisecond):
reply.Err = ErrTimeOut
return
}
LogDebug("C%d-%d %s key%s success, req=%d", args.ClientId, args.RequestId, args.Op, args.Key)
}
有时会通不过,raft日志如下:
10:49:16.407109 Start():{0 x 0 111 y Append 4 249}
10:49:16.482170 {true {0 x 0 111 y Append 4 249} 397}
10:49:16.482984 Start():{0 x 0 112 y Append 4 250}
10:49:16.483154 {true {0 x 0 111 y Append 4 249} 397}
TestConcurrent3A
为了方便调试,把client改成全局自增的id。
ck.id = int(atomic.AddInt32(&g_clientId, 1))
程序一直 通不过,查看日志:发现从applyCh出来的cmd,不是上一个命令,说明applyCh出来的命令不具有顺序,估计我的lab2有问题…
试着修改PutAppend,在最前面加锁了,就不是并发了,但是通过了,耗时15s。仔细一想,本来就是写操作,就应该加锁。看了这个老哥的测试结果也是15s,我就放心了。
func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {
kv.mu.Lock()
defer kv.mu.Unlock()
...
}
为了提高性能:如果Get读取的不是正在修改的key,那么可以不加锁。因为对于updatingKey string
来说,只有一个写者。代码如下:
func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
// Your code here.
reply.WrongLeader = false
_, isLeader := kv.rf.GetState()
if isLeader == false {
reply.WrongLeader = true
return
}
if args.Key != kv.updatingKey {
reply.Value = kv.db[args.Key]
return
}
kv.mu.Lock()
defer kv.mu.Unlock()
reply.Value = kv.db[args.Key]
LogDebug(" S%d C%dr%d Get key%s success, value:%s", kv.me, args.ClientId, args.RequestId,
args.Key,reply.Value)
return
}
TestUnreliable3A
测试的时候发现,发现数据老是多点,准备加入去重功能。
Test: unreliable net, many clients (3A) ...
2020/04/04 13:28:58 get wrong value, key4, wanted:
x 4 0 yx 4 1 yx 4 2 y
, got
x 4 0 yx 4 1 yx 4 2 yx 4 2 y
怎么都通不过,我严重怀疑我的lab2有问题。因此我给每个raft对象,都添加了日志文件,用来输出applyCh的msg
。
logFile, err := os.Create(fmt.Sprintf("log/log-%d.txt", rf.me))
if err != nil {
panic("cant create log file")
}
rf.logger = log.New(logFile, "", log.Lmicroseconds)
这样就很快找到了问题:
- 我的去重放在了Raft.Start()后面
- Get命令不需要去重,不然会返回空
- 运行一段时间后,Leader会发生变化,但是新的Leader的db好像是空的。
- 还是不行,一直报
not the same cmd
。说明了等了半天,不是上一个命令,参考大佬,引入dispatcher。
select {
case c := <- kv.applyCh:
op := c.Command.(Op)
if op.ClientId != args.ClientId || op.RequestId != args.RequestId {
LogInfo(" not the same cmd,S%d args:%+v, new cmd:%+v", kv.me, args, op)
_, reply.IsLeader = kv.rf.GetState()
if reply.IsLeader == false {
return
}
// retry
reply.Err = ErrUnexpectedCmd
return
}
Dispatcher如下。考虑到频繁的插入删除,底层数据结构采用的是链表。链表的唯一缺点就是查找慢,但是这里的链表长度(并发的写者数量)应该不大。
type Dispatcher struct {
head *ListNode // head of list
ApplyCh chan raft.ApplyMsg
mu sync.Mutex
}
type ListNode struct {
Index int // key
ResultCh chan raft.ApplyMsg
Next *ListNode
}
func NewDispatcher(ch chan raft.ApplyMsg) *Dispatcher {
return &Dispatcher{
head: nil,
ApplyCh: ch,
}
}
// 用于debug
func (d *Dispatcher)RegisterCount() int {
count := 0
for n := d.head; n != nil; n = n.Next {
count++
}
return count
}
func (d *Dispatcher) Register(index int, ch chan raft.ApplyMsg) {
d.mu.Lock()
n := &ListNode{
Index: index,
ResultCh: ch,
Next: d.head,
}
d.head = n
d.mu.Unlock()
}
func (d *Dispatcher) unRegister(index int) {
var pre *ListNode = nil
for n := d.head; n != nil; n = n.Next {
if n.Index == index {
if pre != nil {
pre.Next = n.Next
} else {
d.head = n.Next
}
break
}
pre = n
}
}
func (d *Dispatcher) Dispatch(msg raft.ApplyMsg) {
d.mu.Lock()
n := d.head
for ; n != nil; n = n.Next {
if n.Index == msg.CommandIndex {
n.ResultCh <- msg
break
}
}
if n == nil {
// panic("don't register")
}
d.unRegister(msg.CommandIndex)
d.mu.Unlock()
}
还是通不过,一直找不出问题,准备先看看lab4了。这期间我又把lab2重新review了一遍…
追加日志,找到问题了 。
LOCK
UNLOCK
12:36:32.814970 KV2 is processing cmd:C2r5
12:36:32.815096 C6r4-S4 call [Get], key3
12:36:32.819164 C4r3-S1 call [Get], key1
12:36:32.822949 C4r3-S2 call [Get], key1
12:36:32.826033 C5r3-S2 call [Get], key2
12:36:32.830649 C3r3-S3 call [Append], (key0, x 0 0 y)
12:36:32.834872 C5r3-S3 call [Get], key2
12:36:32.837765 C6r4-S0 call [Get], key3
LOCK
12:36:32.843120 duplicated request:C3r3
加锁后,一个分支没有释放锁。以后手动加锁后,一定要在每个分支释放锁。终于通过了!
kv.mu.Lock()
fmt.Printf("LOCK\n")
cmd := Op{...}
dupl := kv.isDuplicatedRequest(args.ClientId, args.RequestId)
if dupl {
LogInfo(" duplicated request:C%dr%d", args.ClientId, args.RequestId)
+++ kv.mu.Unlock() //。。。。。。。。。。。。。。。。。。。。。。。。。
return
}
index, _, _ := kv.rf.Start(cmd)
ch := make(chan raft.ApplyMsg, 1)
kv.dispatcher.Register(index, ch)
kv.mu.Unlock()
select {
...
}
TestOnePartition3A
因为我的Get并没有提交到raft的状态机,而是直接返回。如果Leader被分在少数区,那么此时Get命令不应该返回。修改Get请求,将命令也放到raft状态机,记得不要去重。
并且命令肯定会超时,那么Client遇到超时情况,一定要retry。
TestManyPartitionsOneClient3A
调试信息太多了,我在客户端请求中做了处理,竟然通过了…
ok := ck.servers[ck.leaderId].Call("KVServer.PutAppend", &args, &reply)
// 如果该节点故障或者不是Leader,换个节点
if ok == false || reply.IsLeader == false {
ck.leaderId = (ck.leaderId + 1) % len(ck.servers)
count++
if count % len(ck.servers) == 0 {
time.Sleep(100 * time.Millisecond)
}
continue
}
至此,除了持久化部分,3A全部通过了。