MIT6.824 Lab3A Key/value service without snapshot 实验记录与总结

Lab3A Key/value service without snapshots

一、运行原理

在这里插入图片描述

二、请求的运行链路

图中绿色部分代表的是一个请求从clerk到leader节点,经过预写日志同步,再应用到状态机,最后返回的过程。文字描述如下:

  1. clerk 发起请求,调用 put/append/get 的 rpc handler,rpc handler 的返回是同步的,故clerk等待其返回
  2. rpc handler 将 put/append/get 指令与 clerk 信息结合,生成日志条目,调用 raft 的 start(cmd) 接口。start 返回日志 index 信息,rpc handler 根据生成属于该 index 的 agreech, 并阻塞等待,等待时间超过2000ms即返回超时,告知 clerk 请求失败
  3. start(cmd) 将日志写入 leader 的 WAL(预写日志)中
  4. leader 节点在发心跳(appendEntries)的过程中,将新的日志传送给 follower 节点
  5. 日志同步逻辑:半数follower日志同步完成(写入 WAL 中),appendEntries 返回成功,leader节点根据多数派原则更新 commitIndex 和 lastApplied ,在下次发心跳时,将 leaderCommit 告知 follower ,follower也会更新commitIndex 和 lastApplied 。
    异步applyLog:发现 commitIndex < lastApplied, 将日志已提交信息 applymsg 发送至管道 applych
    异步waitAgreee:监听到管道内的 applymsg ,将其中的日志部分提取出后,在保证客户端请求幂等性的前提下,将日志应用到状态机中。并往属于该下标日志的管道 agreeCh 中写入应用结果(实际上传回日志即可)
  6. 阻塞等待的 rpc handler 返回请求成功,clerk收到后即表示一个完整的请求过程结束

图中玫红色部分代表的是 follower 节点中的日志同步、提交、应用的过程。注意 follower 节点是没有 put/append/get rpc 的调用过程的

三、接口的幂等性

同一个客户端重复请求同一个接口,server 要将这多个请求看成一个请求。比如,当某个请求因为日志没有即时同步,客户端重复发出这个请求。那么也会同步多个内容相同日志。

处理方法:

  • 每个客户端保存自己的客户端编号clerkid和请求数seqid。每个client发送请求时,附带上clerkid和seqid
  • server在将请求转换为日志时,日志内容附带上clerkid和seqid
  • 每个server保存所有客户端已提交日志的最大seqid,用lastSeq map[int]int实现
  • 在日志提交信息通过 applych 传送给 server 时,判断日志内容里的seqid是否大于lastSeq[clerkid],若大于则给与应用,若等于则代表这是这条指令已经应用了,是重复指令,不予应用。不会发生小于的情况。这一步称为幂等性判断

四、多客户端请求

针对但客户端:

_, _, isLeader := kv.rf.Start(op) // push op to raft layer to reach the agreement
if !isLeader {
    reply.Err = ErrWrongLeader
    return
}
<- applyCh
// agreed! apply to statemachine
kv.apply(op)

单客户端这样做是没有问题的。但是单个客户端请求是串行发送,多个客户端时请求是并行发送。而 applych只有一个。多个请求发起的rpc handler都在等待applych传回日志提交信息,但是他们无法知道传回的到底是不是之前start(op)里的op同步成功了。

那是否可以通过循环等待applych传回的信息并判断,这个想法大错特错,因为你要判断,就要接受管道里的信息,这个信息一旦接受,如果判断为不是符合要求的op,那么真正需要这个信息的rpc handler就永远无法获取这个信息了。管道的理解不够深才会有这个想法。我太菜了。

处理方法:

每个日志index建立一个管道,用来通知发送该下标日志请求的rpc handler。rpc handler在start(op)后就等待这个管道。

另起一个waitAgree线程用来接受applych中的数据,然后判断幂等性,然后应用。注意应用日志必须与接受applymsg串行进行,如果并行的话(比如说把应用日志放在rpc handler里完成),有可能会日志应用的顺序出错,即后发送applymsg的日志比前发送的日志先进行了日志应用操作(applyLogToStateMachine)

五、代码实现

5.1 client

1)客户端结构体:

type Clerk struct {
	servers []*labrpc.ClientEnd
	lastLeader int  // 上一个请求成功时leaderId
	clerkId    int  // 客户端id
	lastSeq    int  // 该客户端发起的最后一个写请求号,从0开始
	mu         sync.Mutex 
}

2)发起get请求:
如果没有响应成功,需要一直发起请求知道成功。

func (ck *Clerk) Get(key string) string {
	args := GetArgs{
		Key:     key,
		ClerkId: ck.clerkId,
		Seq:     ck.lastSeq,
	}
	reply := GetReply{}
	i := ck.lastLeader
	defer func() {
		ck.mu.Lock()
		ck.lastLeader = i
		ck.mu.Unlock()
	}()
	for {
		ok := ck.servers[i].Call("KVServer.Get", &args, &reply)
		if !ok || reply.Err == ErrWrongLeader {
			i = (i + 1) % len(ck.servers)
			time.Sleep(10 * time.Millisecond)
			continue
		}
		if reply.Err == ErrNoKey {
			return ""
		}
		return reply.Value
	}
}

3)发起put/append请求:

func (ck *Clerk) PutAppend(key string, value string, op string) {
	// You will have to modify this function.
	ck.mu.Lock()
	ck.lastSeq += 1
	ck.mu.Unlock()

	args := PutAppendArgs{
		Key:     key,
		Value:   value,
		OpPA:    op,
		ClerkId: ck.clerkId,
		Seq:     ck.lastSeq,
	}
	reply := PutAppendReply{}

	i := ck.lastLeader
	defer func() {
		ck.mu.Lock()
		ck.lastLeader = i
		ck.mu.Unlock()
	}()

	for {
		// fmt.Printf("clerk %d send cmd to server层 %d\n", ck.clerkId, i)
		// fmt.Println(args)
		ok := ck.servers[i].Call("KVServer.PutAppend", &args, &reply)
		if !ok || reply.Err == ErrWrongLeader {
			i = (i + 1) % len(ck.servers)
			time.Sleep(10 * time.Millisecond)
			continue
		}
		return
	}
}
5.2 server

1)日志条目Op

type Op struct {
	OpPA    string
	Key     string
	Val     string
	ClerkId int
	Seq     int
}

2)KVServer

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.
	kvdb     map[string]string
	lastSeq  map[int]int     // 记录每个clerk已经提交的日志的序列号(op中的序列号,与日志下标不一样)
	agreeChs map[int]chan Op // 每个日志下标对应的通知rpc结束的管道
}

3)Get rpc

func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
	// Your code here.
	cmd := Op{
		OpPA:    "Get",
		Key:     args.Key,
		ClerkId: args.ClerkId,
		Seq:     args.Seq,
	}

	index, _, isLeader := kv.rf.Start(cmd)
	if !isLeader {
		reply.Err = ErrWrongLeader
		return
	}
	ch := kv.getAgreeCh(index)
	var cmd_ Op
	select {
	case cmd_ = <-ch:
		close(ch)
	case <-time.After(1000 * time.Millisecond): // 超时重试
		reply.Err = ErrWrongLeader
		return
	}
	if !isSameOp(cmd_, cmd) {
		reply.Err = ErrWrongLeader
		return
	}
	reply.Value = kv.kvdb[args.Key] // 读请求不涉及数据库的更新,直接读
	reply.Err = OK
	return
}

4)PutAppend rpc

func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {
	cmd := Op{
		OpPA:    args.OpPA,
		Key:     args.Key,
		Val:     args.Value,
		ClerkId: args.ClerkId,
		Seq:     args.Seq,
	}
	index, _, isLeader := kv.rf.Start(cmd)
	if !isLeader {
		reply.Err = ErrWrongLeader
		return
	}
	ch := kv.getAgreeCh(index)
	var cmd_ Op
	select {
	case cmd_ = <-ch:
		close(ch)
	case <-time.After(1000 * time.Millisecond): // 超时重试
		reply.Err = ErrWrongLeader
		return
	}
	// 网络分区修复前,某个term更小的leader正好start(cmd),然后cmd还未被同步完成即还未提交,
	// 网络分区修复后,新的cmd覆盖了这个index上原有的cmd,然后通过applych传回了该cmd,
	// 此时该rpc handler发起时的需同步的cmd与applych传回的不是同一个cmd,之前的需同步的cmd实际未同步,所以需要告知clerk重试
	if !isSameOp(cmd_, cmd) { // 所以需要判断cmd_与cmd是否相同
		reply.Err = ErrWrongLeader
		return
	}
	reply.Err = OK
	return
}

5)WaitAgree,getAgreeCh,ApplyToStateMachine

func (kv *KVServer) WaitAgree() {
	for !kv.killed() {
		select {
		case msg := <-kv.applyCh:
			cmd := msg.Command.(Op)
			kv.mu.Lock()
			lastSeq, ok := kv.lastSeq[cmd.ClerkId]
			if !ok || lastSeq < cmd.Seq {
				kv.ApplyToStateMachine(cmd) // 同步进行
				kv.lastSeq[cmd.ClerkId] = cmd.Seq
			}
			kv.mu.Unlock()
			kv.getAgreeCh(msg.CommandIndex) <- cmd
		}
	}
}
// 获取每个index的channel,没有则新建一个
func (kv *KVServer) getAgreeCh(idx int) chan Op {
	kv.mu.Lock()
	defer kv.mu.Unlock()

	ch, ok := kv.agreeChs[idx]
	if !ok {
		ch = make(chan Op, 1)
		kv.agreeChs[idx] = ch
	}
	return ch
}
func (kv *KVServer) ApplyToStateMachine(cmd Op) {
	op := cmd.OpPA // 命令操作类型
	switch op {
	case "Put":
		kv.kvdb[cmd.Key] = cmd.Val
	case "Append":
		kv.kvdb[cmd.Key] += cmd.Val
	}
}

六、总结

在做lab2时,有很多时间在看测试代码,看的头疼,其实lab2的测试代码里的请求发送代码逻辑与lab3A实现的差不多。做完3A确实对整个基于raft的应用如何运行有了全面的理解。虽然这里的kv只是一个map。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值