mit6.824-lab3A

实验简介

实现基于raft的KV服务器,您将需要修改kvraft / client.gokvraft / server.go,甚至可能是kvraft / common.go

一定要保证lab2是正确的,不然这个实验到处是问题。

实验参考:

  • https://www.cnblogs.com/mignet/p/6824_Lab_3_KVRaft_3A.html
  • https://blog.csdn.net/Miracle_ma/article/details/80184594

Client

通过RPC调用server的服务,需要考虑两个问题?

  1. 如果call超时,那么client就会超时重发。如果是因为网络延迟,那么server就会收到两次命令,确保服务端只执行一次命令(多次append会导致逻辑错误)。使用类似TCP的序列号,来去重。
  2. client应记录raft的Leader,就不用每次循环去尝试

PutAppend和Get的逻辑是完全一样的

Server

server不能立即执行client的命令,而是把命令反应给raft服务,等raft状态机提交了该命令后,再执行。

Part 3A

TestBasic3A

写了个最简单的版本

func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {
	// Your code here.
	cmd := Op{
		Key:   args.Key,
		Value: args.Value,
		Option:    args.Op,
	}
	reply.WrongLeader = false
	_, _, isLeader := kv.rf.Start(cmd)
	if isLeader == false {
		reply.WrongLeader = true
		return
	}
	// TODO : 这样做的话,每次只能处理一个请求
	kv.mu.Lock()
	defer kv.mu.Unlock()
	select {
	case cmd := <- kv.applyCh:
		op := cmd.Command.(Op)
		kv.doCmd(&op)
	case <- time.After(500 * time.Millisecond):
		reply.Err = ErrTimeOut
		return
	}
	LogDebug("C%d-%d %s key%s success, req=%d", args.ClientId, args.RequestId, args.Op, args.Key)
}

有时会通不过,raft日志如下:

10:49:16.407109 	Start():{0 x 0 111 y Append 4 249}
10:49:16.482170 {true {0 x 0 111 y Append 4 249} 397}
10:49:16.482984 	Start():{0 x 0 112 y Append 4 250}
10:49:16.483154 {true {0 x 0 111 y Append 4 249} 397}

TestConcurrent3A

为了方便调试,把client改成全局自增的id。

ck.id = int(atomic.AddInt32(&g_clientId, 1))

程序一直 通不过,查看日志:发现从applyCh出来的cmd,不是上一个命令,说明applyCh出来的命令不具有顺序,估计我的lab2有问题…

试着修改PutAppend,在最前面加锁了,就不是并发了,但是通过了,耗时15s。仔细一想,本来就是写操作,就应该加锁。看了这个老哥的测试结果也是15s,我就放心了。

func (kv *KVServer) PutAppend(args *PutAppendArgs, reply *PutAppendReply) {
	kv.mu.Lock()
	defer kv.mu.Unlock()
    ...
}

为了提高性能:如果Get读取的不是正在修改的key,那么可以不加锁。因为对于updatingKey string来说,只有一个写者。代码如下:

func (kv *KVServer) Get(args *GetArgs, reply *GetReply) {
	// Your code here.
	reply.WrongLeader = false
	_, isLeader := kv.rf.GetState()
	if isLeader == false {
		reply.WrongLeader = true
		return
	}
	if args.Key != kv.updatingKey {
		reply.Value = kv.db[args.Key]
		return
	}
	kv.mu.Lock()
	defer kv.mu.Unlock()
	reply.Value = kv.db[args.Key]
	LogDebug("	S%d	C%dr%d Get key%s success, value:%s", kv.me, args.ClientId, args.RequestId,
		args.Key,reply.Value)
	return
}

TestUnreliable3A

测试的时候发现,发现数据老是多点,准备加入去重功能。

Test: unreliable net, many clients (3A) ...
2020/04/04 13:28:58 get wrong value, key4, wanted:
x 4 0 yx 4 1 yx 4 2 y
, got
x 4 0 yx 4 1 yx 4 2 yx 4 2 y

怎么都通不过,我严重怀疑我的lab2有问题。因此我给每个raft对象,都添加了日志文件,用来输出applyCh的msg

	logFile, err := os.Create(fmt.Sprintf("log/log-%d.txt", rf.me))
	if err != nil {
		panic("cant create log file")
	}
	rf.logger  = log.New(logFile, "", log.Lmicroseconds)

这样就很快找到了问题:

  1. 我的去重放在了Raft.Start()后面
  2. Get命令不需要去重,不然会返回空
  3. 运行一段时间后,Leader会发生变化,但是新的Leader的db好像是空的。
  4. 还是不行,一直报not the same cmd。说明了等了半天,不是上一个命令,参考大佬,引入dispatcher。
select {
	case c := <- kv.applyCh:
		op := c.Command.(Op)
		if op.ClientId != args.ClientId || op.RequestId != args.RequestId {
			LogInfo("	not the same cmd,S%d	args:%+v, new cmd:%+v", kv.me, args, op)
			_, reply.IsLeader = kv.rf.GetState()
			if reply.IsLeader == false {
				return
			}
			// retry
			reply.Err = ErrUnexpectedCmd
			return
		}

Dispatcher如下。考虑到频繁的插入删除,底层数据结构采用的是链表。链表的唯一缺点就是查找慢,但是这里的链表长度(并发的写者数量)应该不大。

type Dispatcher struct {
	head    *ListNode // head of list
	ApplyCh chan raft.ApplyMsg
	mu sync.Mutex
}

type ListNode struct {
	Index    int // key
	ResultCh chan raft.ApplyMsg
	Next     *ListNode
}

func NewDispatcher(ch chan raft.ApplyMsg) *Dispatcher {
	return &Dispatcher{
		head:    nil,
		ApplyCh: ch,
	}
}

// 用于debug
func (d *Dispatcher)RegisterCount() int {
	count := 0
	for n := d.head; n != nil; n = n.Next {
		count++
	}
	return count
}

func (d *Dispatcher) Register(index int, ch chan raft.ApplyMsg) {
	d.mu.Lock()
	n := &ListNode{
		Index:    index,
		ResultCh: ch,
		Next:     d.head,
	}
	d.head = n
	d.mu.Unlock()
}


func (d *Dispatcher) unRegister(index int) {
	var pre *ListNode = nil
	for n := d.head; n != nil; n = n.Next {
		if n.Index == index {
			if pre != nil {
				pre.Next = n.Next
			} else {
				d.head = n.Next
			}
			break
		}
		pre = n
	}
}

func (d *Dispatcher) Dispatch(msg raft.ApplyMsg) {
	d.mu.Lock()
	n := d.head
	for ; n != nil; n = n.Next {
		if n.Index == msg.CommandIndex {
			n.ResultCh <- msg
			break
		}
	}
	if n == nil {
		// panic("don't register")
	}
	d.unRegister(msg.CommandIndex)
	d.mu.Unlock()
}


还是通不过,一直找不出问题,准备先看看lab4了。这期间我又把lab2重新review了一遍…

追加日志,找到问题了 。

LOCK
UNLOCK
12:36:32.814970 	KV2 is processing cmd:C2r5
12:36:32.815096 C6r4-S4 call [Get], key3
12:36:32.819164 C4r3-S1 call [Get], key1
12:36:32.822949 C4r3-S2 call [Get], key1
12:36:32.826033 C5r3-S2 call [Get], key2
12:36:32.830649 C3r3-S3 call [Append], (key0, x 0 0 y)
12:36:32.834872 C5r3-S3 call [Get], key2
12:36:32.837765 C6r4-S0 call [Get], key3
LOCK
12:36:32.843120 	duplicated request:C3r3

加锁后,一个分支没有释放锁。以后手动加锁后,一定要在每个分支释放锁。终于通过了!

kv.mu.Lock()
fmt.Printf("LOCK\n")
cmd := Op{...}
dupl := kv.isDuplicatedRequest(args.ClientId, args.RequestId)
if dupl {
    LogInfo("	duplicated request:C%dr%d", args.ClientId, args.RequestId)
+++ kv.mu.Unlock() //。。。。。。。。。。。。。。。。。。。。。。。。。
    return
}
index, _, _ := kv.rf.Start(cmd)
ch := make(chan raft.ApplyMsg, 1)
kv.dispatcher.Register(index, ch)
kv.mu.Unlock()
select {
    ...
}

TestOnePartition3A

因为我的Get并没有提交到raft的状态机,而是直接返回。如果Leader被分在少数区,那么此时Get命令不应该返回。修改Get请求,将命令也放到raft状态机,记得不要去重。

并且命令肯定会超时,那么Client遇到超时情况,一定要retry。

TestManyPartitionsOneClient3A

调试信息太多了,我在客户端请求中做了处理,竟然通过了…

ok := ck.servers[ck.leaderId].Call("KVServer.PutAppend", &args, &reply)
//  如果该节点故障或者不是Leader,换个节点
if ok == false || reply.IsLeader == false {
    ck.leaderId = (ck.leaderId + 1) % len(ck.servers)
    count++
    if count % len(ck.servers) == 0 {
        time.Sleep(100 * time.Millisecond)
    }
    continue
}

至此,除了持久化部分,3A全部通过了。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
MIT 6.824 课程的 Lab1 是关于 Map 的实现,这里单介绍一下实现过程。 MapReduce 是一种布式计算模型,它可以用来处理大规模数据集。MapReduce 的核心想是将数据划分为多个块,每个块都可以在不同的节点上并行处理,然后将结果合并在一起。 在 Lab1 中,我们需要实现 MapReduce 的基本功能,包括 Map 函数、Reduce 函数、分区函数、排序函数以及对作业的整体控制等。 首先,我们需要实现 Map 函数。Map 函数会读取输入文件,并将其解析成一系列键值对。对于每个键值对,Map 函数会将其传递给用户定义的 Map 函数,生成一些新的键值对。这些新的键值对会被分派到不同的 Reduce 任务中,进行进一步的处理。 接着,我们需要实现 Reduce 函数。Reduce 函数接收到所有具有相同键的键值对,并将它们合并成一个结果。Reduce 函数将结果写入输出文件。 然后,我们需要实现分区函数和排序函数。分区函数将 Map 函数生成的键值对映射到不同的 Reduce 任务中。排序函数将键值对按键进行排序,确保同一键的所有值都被传递给同一个 Reduce 任务。 最后,我们需要实现整个作业的控制逻辑。这包括读取输入文件、调用 Map 函数、分区、排序、调用 Reduce 函数以及写入输出文件。 Lab1 的实现可以使用 Go 语言、Python 或者其他编程语言。我们可以使用本地文件系统或者分布式文件系统(比如 HDFS)来存储输入和输出文件。 总体来说,Lab1 是一个比较简单的 MapReduce 实现,但它奠定了 MapReduce 的基础,为后续的 Lab 提供了良好的基础。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值