MIT6.5840-2023-Lab3A: Fault-tolerant K/V Service-Key/value service without snapshots

前置知识

线性一致(强一致):对于整个请求历史记录,只存在一个序列,不允许不同的客户端看见不同的序列;或者说对于多个请求可以构造一个带环的图,那就证明不是线性一致的请求记录。
线性一致不是有关系统设计的定义,这是有关系统行为的定义。
Zookeeper 的确允许客户端将读请求发送给任意副本,并由副本根据自己的状态来响应读请求。副本的 Log 可能并没有拥有最新的条目,所以尽管系统中可能有一些更新的数据,这个副本可能还是会返回旧的数据
Zookeeper 的一致性保证:写请求是线性一致的、 FIFO 客户端序列(所有客户端发送的请求以一个特定的序列执行,通过 zxid 维护)。
image.png

实验内容

使用 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:

  1. 接收 client 发送的 rpc request;
  2. kvserver 将这些 rpc 封装成 Op,并通过 Start() 发送给 raft,同时在 waitCh 上定时等待执行结果;
  3. 过半 raft 日志复制成功,raft leader 向 applyCh 发送 ApplyMsg;
  4. 所有 kvserver 收到 ApplyMsg 都会执行相应的命令,其中 kvserver leader 将执行结果通知 waitCh;
  5. 超时或者 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()
	}
}

实验结果

在这里插入图片描述

  • 21
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值