Introduction
該實(shí)驗(yàn)是mit 6.824課程的第3個(gè)實(shí)驗(yàn)椿肩,基于raft協(xié)議完成一個(gè)key-value系統(tǒng)
實(shí)驗(yàn)分為A和B兩個(gè)部分,在Part A中:我們不考慮日志的大小,在Part B中會(huì)完成快照功能
完整的代碼地址
課程地址
實(shí)驗(yàn)地址
已經(jīng)有的實(shí)驗(yàn)地址:
Lab 1: MapReduce:6.824 Lab 1: MapReduce(2016)
Lab 2: Raft:raft 系列解讀(2) 之 測(cè)試用例
Lab 3: KV Raft Part-A:6.824 Lab 3: Fault-tolerant Key/Value Service Part-A
Part A: Key/value service without log compaction
支持3個(gè)操作
Put(key, value):改變key的值
Append(key, arg):給key的值新增value
Get(key):返回值
任務(wù)
當(dāng)沒有丟包和servers fail的情況下進(jìn)行實(shí)現(xiàn),需要提供客戶端順序一致性的api,調(diào)用Put落蝙,Append和Get3個(gè)api,在所有的server以相同的順序執(zhí)行暂幼,并且具有at-most-once的語義
一個(gè)建議的計(jì)劃是:先完成
server.go
中的Op
結(jié)構(gòu)筏勒,然后完成server.go
中的PutAppend()
和Get()
操作,在操作中旺嬉,應(yīng)該先調(diào)用Start()
管行,當(dāng)日志commit的時(shí)候,回復(fù)客戶端
提示
- 調(diào)用
Start()
后邪媳,kvraft servers 會(huì)等待raft log達(dá)成一致病瞳,通過applyCh
獲取一致的命令,我們需要考慮怎么安排代碼悲酷,才能持續(xù)讀取applyCh
套菜,而其他命令也能執(zhí)行- 我們需要處理case:leader調(diào)用了
Start()
,但是在log commit之前设易,丟失了leadership逗柴,這種情況下,代碼應(yīng)該將請(qǐng)求重新發(fā)送給新的leader顿肺。一種方式是戏溺,server需要檢測(cè)出自己已經(jīng)不是leader了,通過查看相同的start在index上返回一個(gè)不用的請(qǐng)求屠尊,另一種方式是通過調(diào)用GetState()
旷祸,但是如果出現(xiàn)網(wǎng)絡(luò)分區(qū),可能不知道自己已經(jīng)不是leader了讼昆,這種情況下client和server都處在網(wǎng)絡(luò)分區(qū)中托享,因此可以無限的等待下去,直到網(wǎng)絡(luò)恢復(fù)- A kvraft server不應(yīng)該完成
Get()
操作如果得不到majority浸赫,因?yàn)檫@樣子可能會(huì)得不到最新的數(shù)據(jù)
任務(wù):
需要處理重復(fù)請(qǐng)求闰围,保證滿足at-most-once的語義
提示:
- 需要對(duì)每個(gè)client請(qǐng)求編號(hào)
- 要保證快速的釋放內(nèi)存,因此可以在下一個(gè)請(qǐng)求帶上下一個(gè)請(qǐng)求
實(shí)際設(shè)計(jì)中出現(xiàn)的問題
頻繁變化leader
func (ck *Clerk) Get(key string) string {
args := GetArgs{Key:key}
for {
for _,c := range ck.servers {
time.Sleep(time.Millisecond*1000)
reply := GetReply{}
ok := c.Call("RaftKV.Get", &args, &reply)
if ok && !reply.WrongLeader {
return reply.Value
}
}
}
// You will have to modify this function.
return ""
}
此處如果沒有sleep的話既峡,相當(dāng)于客戶端一直不斷的在START羡榴,導(dǎo)致的一個(gè)問題是:server不斷在處理START命令,導(dǎo)致正常的心跳都完成不了了运敢,就出現(xiàn)了頻繁的變化leader了校仑,問題很嚴(yán)重忠售,那應(yīng)該怎么做呢?
后來做了優(yōu)化迄沫,對(duì)于讀操作不走 chan稻扬,這就沒問題了
index := -1
term := -1
isLeader := true
if rf.state != StateLeader {
isLeader = false
return index, term, isLeader
}
這樣就有個(gè)初判斷了
通過labrpc傳遞的數(shù)據(jù)不對(duì)
func StartKVServer(servers []*labrpc.ClientEnd, me int, persister *raft.Persister, maxraftstate int) *RaftKV {
// call gob.Register on structures you want
// Go's RPC library to marshall/unmarshall.
gob.Register(Op{})
如果沒有 gob.Register(Op{}) 這就錯(cuò)誤,為什么要加上這句話呢邢滑?
出現(xiàn)阻塞
分析:此處阻塞了為什么呢?因?yàn)樵趃et上的時(shí)候愿汰,有一個(gè)沒有收到apply困后?好奇怪
// TODO:優(yōu)化超時(shí)的邏輯
select {
case op := <-ch:
commited := op == entry
kv.logger.Debug("index:%d commited:%v",index,commited)
return commited
case <- time.After(AppendTimeOut):
kv.logger.Info("index:%d %s timeout after %v",index, entry.Type,AppendTimeOut)
return false
}
加上上面的超時(shí)邏輯后,就可以解決阻塞的問題衬廷,但是一旦超時(shí)
2016/10/26 14:37:45 I index:323 Append timeout after 1s
2016/10/26 14:37:45 0: client new get 0
2016/10/26 14:37:45 get wrong value, key 0, wanted:
就會(huì)出現(xiàn)問題摇予,會(huì)重復(fù)執(zhí)行 Append操作,因?yàn)槠鋵?shí)已經(jīng)apply了這個(gè)請(qǐng)求了
那怎么解決呢吗跋?我現(xiàn)在去除這個(gè)超時(shí)限制侧戴,在獲取Apply的時(shí)候邏輯變?yōu)橄旅娴模?/p>
// 通知結(jié)果
ch, ok := kv.result[index]
if ok {
select {
case <-ch:
default:
}
}else {
// 沒人讀就有了數(shù)據(jù)?
ch = make(chan Op,1)
kv.result[index] = ch
}
ch <- op
此時(shí)就不會(huì)有超時(shí)的問題了跌宛,為什么呢酗宋?
很反人類的問題:因?yàn)楫?dāng)調(diào)用
func (kv *RaftKV)AppendLog(entry Op) bool {
index, _, isLeader := kv.rf.Start(entry)
此時(shí)可能沒等到執(zhí)行下面的去讀chan的時(shí)候,已經(jīng)apply成功了疆拘,因此我們就需要事先往chan里面存入數(shù)據(jù)
TestUnreliable
? kvraft [master] ? go test -run TestUnreliable
Test: unreliable ...
2016/10/26 14:59:42 get wrong value, key 3, wanted:
x 3 0 yx 3 1 y
, got
x 3 0 yx 3 0 yx 3 1 y
很容易看出問題:一個(gè)請(qǐng)求重復(fù)執(zhí)行了蜕猫,我們需要在客戶端去重
對(duì)于每個(gè)客戶端都給編號(hào),然后每個(gè)請(qǐng)求都順序增長(zhǎng)
TestManyPartitionsManyClients
測(cè)試出現(xiàn)阻塞
select {
case op := <-ch:
commited := op == entry
kv.logger.Debug("index:%d commited:%v", index, commited)
return commited
// 此處超時(shí)其實(shí)也很好理解哎迄,因?yàn)閯傞_始是leader回右,但是在log得到commit之前,丟失了leadership漱挚,此時(shí)
// 如果沒有超時(shí)機(jī)制翔烁,則會(huì)一直阻塞下去
// 或者由于此時(shí)的leader是一個(gè)分區(qū)里面的leader,則只可能一直阻塞下去了
// 因此也需要超時(shí)
case <-time.After(AppendTimeOut):
//kv.logger.Info("index:%d %s timeout after %v", index, entry.Type, AppendTimeOut)
return false
}