前置知识
线性一致(强一致):对于整个请求历史记录,只存在一个序列,不允许不同的客户端看见不同的序列;或者说对于多个请求可以构造一个带环的图,那就证明不是线性一致的请求记录。
线性一致不是有关系统设计的定义,这是有关系统行为的定义。
Zookeeper 的确允许客户端将读请求发送给任意副本,并由副本根据自己的状态来响应读请求。副本的 Log 可能并没有拥有最新的条目,所以尽管系统中可能有一些更新的数据,这个副本可能还是会返回旧的数据。
Zookeeper 的一致性保证:写请求是线性一致的、 FIFO 客户端序列(所有客户端发送的请求以一个特定的序列执行,通过 zxid 维护)。
实验内容
使用 lab2 中的 Raft 库构建 Fault-tolerant K/V Service,即维护一个简单的键/值对数据库,其中键和值都是字符串。具体来说,该服务是一个复制状态机,由多个使用 Raft 进行复制的键/值服务器组成,只要大多数服务器处于活动状态并且可以通信,该服务就应该继续处理客户端请求。
实验环境
OS:WSL-Ubuntu-18.04
golang:go1.17.6 linux/amd64
Part A: Key/value service without snapshots
每一个 kvserver 都带有一个 raft,client 将 Put()、Append()、Get() 等 rpc 发送给带有 leader raft 的 kvserver。
kvserver leader:
- 接收 client 发送的 rpc request;
- kvserver 将这些 rpc 封装成 Op,并通过 Start() 发送给 raft,同时在 waitCh 上定时等待执行结果;
- 过半 raft 日志复制成功,raft leader 向 applyCh 发送 ApplyMsg;
- 所有 kvserver 收到 ApplyMsg 都会执行相应的命令,其中 kvserver leader 将执行结果通知 waitCh;
- 超时或者 waitCh 中有结果,发送 rpc response;
其他注意的点:
- 重复或过期的 Put、Append 命令,通过 ClientId 和 ReqId 判断;
- 向 waitCh 信道内发送消息前检查是否存在,避免因为超时信道被释放;
- applyCh 的性能问题,测试要求每 100ms 完成 3 个 req,要么将 raft 中的 apply() 定时器设置成 30ms 内,要么修改代码将 applyCh 的更新改成条件变量;
- 保存 leaderId,记住哪台服务器是上一次 RPC 的领导者,并首先将下一次 RPC 发送到该服务器,避免浪费时间;
rpc及其他格式
lastReq:client 与 reqId 的对应关系,解决重复问题。
waitCh:此次 rpc 携带的 command 存放在 log 中的 index 与监听此次 op 的信道的对应关系。
// Put or Append
type PutAppendArgs struct {
Key string
Value string
Op string // "Put" or "Append"
// You'll have to add definitions here.
// Field names must start with capital letters,
// otherwise RPC will break.
ClientId int64
ReqId int64
}
type PutAppendReply struct {
Err Err
}
type GetArgs struct {
Key string
// You'll have to add definitions here.
ClientId int64
ReqId int64
}
type GetReply struct {
Err Err
Value string
}
type Clerk struct {
servers []*labrpc.ClientEnd
// You will have to modify this struct.
clientId int64
reqId int64
leaderId int
}
type Op struct {
// Your definitions here.
// Field names must start with capital letters,
// otherwise RPC will break.
ClientId int64
ReqId int64
OpType string
Key string
Value string
}
type KVServer struct {
mu sync.Mutex
me int
rf *raft.Raft
applyCh chan raft.ApplyMsg
dead int32 // set by Kill()
maxraftstate int // snapshot if log grows this big
// Your definitions here.
kvs map[string]string
waitCh map[int]chan *Op
lastReq map[int64]int64
timeout time.Duration
}
client
Put、Append放在一起处理。
记录 ReqId、ClientId,并传给 kvserver,以便 kvserver 判断重复的 rpc(Put、Append)。
server
Get:
func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
// Your code here.
op := Op{
ClientId: args.ClientId,
ReqId: args.ReqId,
OpType: "Get",
Key: args.Key,
}
index, term, isLeader := kv.rf.Start(op)
if !isLeader {
reply.Err = ErrWrongLeader
return
}
kv.mu.Lock()
waitCh, ok := kv.waitCh[index]
if !ok {
kv.waitCh[index] = make(chan *Op, 1)
waitCh = kv.waitCh[index]
}
kv.mu.Unlock()
// wait exec finished
select {
case op := <-waitCh:
reply.Value = op.Value
nowTerm, isLeader := kv.rf.GetState()
if isLeader && nowTerm == term {
reply.Err = OK
} else {
reply.Err = ErrWrongLeader
}
case <-time.After(kv.timeout):
reply.Err = ErrWrongLeader
}
kv.mu.Lock()
delete(kv.waitCh, index)
kv.mu.Unlock()
}
PutAppend:
与 Get 不同的是,在执行此次 rpc 开始前以及执行时,两次都需要判断是否为重复 rpc。
func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {
// Your code here.
kv.mu.Lock()
if kv.isInvalidReq(args.ClientId, args.ReqId) {
reply.Err = OK
kv.mu.Unlock()
return
}
kv.mu.Unlock()
op := Op{
ClientId: args.ClientId,
ReqId: args.ReqId,
OpType: args.Op,
Key: args.Key,
Value: args.Value,
}
index, term, isLeader := kv.rf.Start(op)
if !isLeader {
reply.Err = ErrWrongLeader
return
}
kv.mu.Lock()
waitCh, ok := kv.waitCh[index]
if !ok {
kv.waitCh[index] = make(chan *Op, 1)
waitCh = kv.waitCh[index]
}
kv.mu.Unlock()
// wait exec finished
select {
case <-waitCh:
nowTerm, isLeader := kv.rf.GetState()
if isLeader && nowTerm == term {
reply.Err = OK
} else {
reply.Err = ErrWrongLeader
}
case <-time.After(kv.timeout):
reply.Err = ErrWrongLeader
}
kv.mu.Lock()
delete(kv.waitCh, index)
kv.mu.Unlock()
}
向 waitCh 发送完成通知
func (kv *KVServer) apply() {
for kv.killed() == false {
applyMsg := <-kv.applyCh
kv.mu.Lock()
if applyMsg.CommandValid {
if op, ok := applyMsg.Command.(Op); ok {
log.Println("exec command")
kv.exec(&op)
term, isLeader := kv.rf.GetState()
if isLeader && applyMsg.CommandTerm == term {
if waitCh, ok := kv.waitCh[applyMsg.CommandIndex]; ok {
waitCh <- &op
}
}
}
} else if applyMsg.SnapshotValid {
}
kv.mu.Unlock()
}
}