Introduction
在这次实验中,我们将使用Lab2的Raft库来实现容错的key-value存储服务。
存储系统将由客户端和key/value服务器,每个key/value服务器使用Raft节点。客户端发送Put(), Append()和Get()的RPCs 到key/value服务器(kvraft),然后将这些RPC调用存入Raft的log中并按序执行。客户端可以向任何kvraft服务器发送RPC请求,但是如果该服务器不是Raft领导者或者请求失败超时,则需要重新发送给另外一个。假如操作被committed到Raft的log中并应用到状态机中,那么它的结果需要反馈到客户端。假如commit操作失败(比如领导者被替换),客户端必须重新发送请求。
本次实验分为2部分。在Part A,我们要实现key/value服务,无需考虑日志长度。在Part B,我们需要考虑日志长度,实现Snapshot。
Part A: Key/value service without log compaction
该服务支持3种RPC调用:Put(key, value),Append(key, arg)和Get(key)。Put()函数替换key对应的value,Append(key, arg)函数将arg增加到key对应的value,Get()函数获取key对应的value。Append值到不存在的key,就理解为Put函数。
具体实现
与前面博客一样,我们先从测试代码出发,一步步实现具体函数。
首先是TestBasic测试函数,该测试函数检查在无Fault情况下单客户端操作的正确性。其中涉及到了1个重要的测试函数GenericTest。
func GenericTest(t *testing.T, tag string, nclients int, unreliable bool, crash bool, partitions bool, maxraftstate int) {
const nservers = 5
cfg := make_config(t, tag, nservers, unreliable, maxraftstate)
defer cfg.cleanup()
ck := cfg.makeClient(cfg.All())
done_partitioner := int32(0)
done_clients := int32(0)
ch_partitioner := make(chan bool)
clnts := make([]chan int, nclients)
for i := 0; i < nclients; i++ {
clnts[i] = make(chan int)
}
for i := 0; i < 3; i++ {
// log.Printf("Iteration %v\n", i)
atomic.StoreInt32(&done_clients, 0)
atomic.StoreInt32(&done_partitioner, 0)
go spawn_clients_and_wait(t, cfg, nclients, func(cli int, myck *Clerk, t *testing.T) {
j := 0
defer func() {
clnts[cli] <- j
}()
last := ""
key := strconv.Itoa(cli)
myck.Put(key, last)
for atomic.LoadInt32(&done_clients) == 0 {
if (rand.Int() % 1000) < 500 {
nv := "x " + strconv.Itoa(cli) + " " + strconv.Itoa(j) + " y"
// log.Printf("%d: client new append %v\n", cli, nv)
myck.Append(key, nv)
last = NextValue(last, nv)
j++
} else {
// log.Printf("%d: client new get %v\n", cli, key)
v := myck.Get(key)
if v != last {
log.Fatalf("get wrong value, key %v, wanted:\n%v\n, got\n%v\n", key, last, v)
}
}
}
})
if partitions {
// Allow the clients to perform some operations without interrup