lab2

lab2


介绍
在本实验中,您将为单台机器构建一个键/值(Key/Value)服务器,该服务器确保即使在网络故障的情况下,每个 Put 操作也最多执行一次(at-most-once),并且操作是线性一致(linearizable)的。您将使用此 KV 服务器来实现一个锁(lock)。在后续的实验中,将复制类似这样的服务器以处理服务器崩溃。

KV 服务器
每个客户端使用一个 Clerk 与键/值服务器交互,Clerk 向服务器发送 RPC(远程过程调用)。客户端可以向服务器发送两种不同的 RPC:Put(key, value, version)Get(key)。服务器维护一个内存中的映射(map),该映射记录每个键对应的(值, 版本号)元组。键(key)和值(value)是字符串。版本号(version)记录该键被写入的次数。

Put(key, value, version) 仅在 Put 的版本号与服务器为该键记录的版本号匹配时,才在映射中安装或替换该键的值。如果版本号匹配,服务器还会增加该键的版本号。如果版本号不匹配,服务器应返回 rpc.ErrVersion

客户端可以通过调用版本号为 0 的 Put 来创建一个新键(服务器存储的最终版本号将是 1)。如果 Put 的版本号大于 0 且该键在服务器上不存在,服务器应返回 rpc.ErrNoKey

Get(key) 获取该键的当前值及其关联的版本号。如果该键在服务器上不存在,服务器应返回 rpc.ErrNoKey

为每个键维护一个版本号,对于使用 Put 实现锁以及在网络不可靠且客户端重传时确保 Put 操作的最多一次(at-most-once)语义非常有用。

当您完成本实验并通过所有测试后,从调用 Clerk.GetClerk.Put 的客户端角度来看,您将拥有一个线性一致的键/值服务。也就是说,如果客户端操作不是并发的,每个客户端的 Clerk.GetClerk.Put 将观察到由先前操作序列所隐含的状态修改。对于并发操作,返回值和最终状态将与这些操作按某种顺序一次执行一个相同。如果操作在时间上重叠,则它们是并发的:例如,如果客户端 X 调用 Clerk.Put(),然后客户端 Y 调用 Clerk.Put(),接着客户端 X 的调用返回。一个操作必须观察到在该操作开始之前所有已完成操作的效果。有关线性一致性的更多背景信息,请参阅 FAQ。

线性一致性对应用程序来说很方便,因为它是您从单台服务器(一次处理一个请求)看到的行为。例如,如果一个客户端从服务器收到更新请求的成功响应,那么随后启动的其他客户端的读取操作保证能看到该更新的效果。对于单台服务器来说,提供线性一致性相对容易。

开始
我们在 src/kvsrv1 中为您提供了骨架代码和测试。kvsrv1/client.go 实现了一个 Clerk,客户端用它来管理与服务器的 RPC 交互;Clerk 提供了 PutGet 方法。kvsrv1/server.go 包含服务器代码,包括实现 RPC 请求服务器端的 PutGet 处理程序。您需要修改 client.goserver.go。RPC 请求、回复和错误值定义在 kvsrv1/rpc 包中的文件 kvsrv1/rpc/rpc.go 中,您应该查看该文件,但无需修改 rpc.go

要开始运行,请执行以下命令。别忘了 git pull 以获取最新软件。

step1

具有可靠网络的键/值服务器(简单)
您的第一个任务是在没有消息丢失的情况下实现一个解决方案。您需要在 client.go 中的 ClerkPut/Get 方法中添加发送 RPC 的代码,并在 server.go 中实现 PutGet 的 RPC 处理程序。

当您通过测试套件中的 Reliable 测试时,即完成此任务:

$ go test -v -run Reliable
=== RUN   TestReliablePut
One client and reliable Put (reliable network)...
  ... Passed --   0.0  1     5    0
--- PASS: TestReliablePut (0.00s)
=== RUN   TestPutConcurrentReliable
Test: many clients racing to put values to the same key (reliable network)...
info: linearizability check timed out, assuming history is ok
  ... Passed --   3.1  1 90171 90171
--- PASS: TestPutConcurrentReliable (3.07s)
=== RUN   TestMemPutManyClientsReliable
Test: memory use many put clients (reliable network)...
  ... Passed --   9.2  1 100000    0
--- PASS: TestMemPutManyClientsReliable (16.59s)
PASS
ok  	6.5840/kvsrv1	19.681s

每个 Passed 后面的数字分别是:实际时间(秒)、常量 1、发送的 RPC 数量(包括客户端 RPC)以及执行的键/值操作数量(ClerkGetPut 调用)。

使用 go test -race 检查您的代码是否存在竞争条件。

个人想法:

以上是关于这个实验的介绍, 第一步在客户端和服务器中完善get和put方法即可,

服务端用的是mit6.5840自己写的RPC, 不需要register … ,测试里面以及做了这部分工作

代码如下:

client.go

package kvsrv

import (
	"time"

	"6.5840/kvsrv1/rpc"
	kvtest "6.5840/kvtest1"
	tester "6.5840/tester1"
)

type Clerk struct {
	clnt   *tester.Clnt
	server string
}

func MakeClerk(clnt *tester.Clnt, server string) kvtest.IKVClerk {
	ck := &Clerk{clnt: clnt, server: server}
	// You may add code here.
	return ck
}

// Get 获取键的当前值和版本。
// 如果密钥不存在,则返回 ErrNoKey。
// 面对所有其他错误,它会永远尝试。
//
// 您可以使用如下代码发送 RPC:
// ok := ck.clnt.Call(ck.server, “KVServer.Get”, &args, &reply)
//
// 参数和回复的类型(包括它们是否为指针)
// 必须与 RPC 处理程序函数参数的声明类型匹配。
// 此外,回复必须作为指针传递。
func (ck *Clerk) Get(key string) (string, rpc.Tversion, rpc.Err) {
	// You will have to modify this function.
	args := rpc.GetArgs{
		Key: key,
	}
	reply := rpc.GetReply{}

    ck.clnt.Call(ck.server, "KVServer.Get", &args, &reply)
		
	if reply.Err == rpc.OK {
		return reply.Value, reply.Version, rpc.OK
	}
	return "", 0, rpc.ErrNoKey
}

// 仅当请求中的版本与服务器上的密钥版本匹配时,才将 update key 与值匹配。
// 如果版本号不匹配,服务器应返回 ErrVersion。
//
//	如果 Put 在其第一个 RPC 上收到 ErrVersion,则 Put 应返回 ErrVersion,因为 Put 肯定不是在服务器上执行的。
//	如果客户端在重新发送 RPC 时收到 ErrVersion,则 Put 必须将 ErrMaybe 返回给应用程序,
//
// 因为服务器可能已经成功处理了它之前的 RPC,但响应丢失了,并且 Clerk 不知道是否执行了 Put。
//
// 您可以使用如下代码发送 RPC:
// ok := ck.clnt.Call(ck.server, “KVServer.Put”, &args, &reply)
//
// 参数和回复的类型(包括它们是否为指针)
// 必须与 RPC 处理程序函数的声明类型匹配
// 参数。此外,回复必须作为指针传递。
func (ck *Clerk) Put(key, value string, version rpc.Tversion) rpc.Err {
	// You will have to modify this function.
	// 有重发机制
	args := rpc.PutArgs{
		Key:     key,
		Value:   value,
		Version: version,
	}
	reply := rpc.PutReply{}
	repeatCnt := 0
	for { // 因为网络丢包,而重传
		ok := ck.clnt.Call(ck.server, "KVServer.Put", &args, &reply)
		if ok {
			break
		}
		repeatCnt++
		time.Sleep(10 * time.Millisecond)
	}
	if reply.Err == rpc.OK {
		return rpc.OK
	}
	if reply.Err == rpc.ErrVersion && repeatCnt == 0 {
		return rpc.ErrVersion
	}
	if reply.Err == rpc.ErrVersion && repeatCnt > 0 {
		return rpc.ErrMaybe
	}
	return rpc.ErrNoKey
}

server.go

package kvsrv

import (
	"log"
	"sync"

	"6.5840/kvsrv1/rpc"
	"6.5840/labrpc"
	tester "6.5840/tester1"
)

const Debug = false

func DPrintf(format string, a ...interface{}) (n int, err error) {
	if Debug {
		log.Printf(format, a...)
	}
	return
}

type valueVersion struct {
	value   string
	version rpc.Tversion
}
type KVServer struct {
	mu sync.Mutex

	// Your definitions here.
	kvMap map[string]*valueVersion
}

func MakeKVServer() *KVServer {
	kv := &KVServer{
		kvMap: make(map[string]*valueVersion),
	}
	// Your code here.

	// kv.server()
	return kv
}

// func (kvserver *KVServer) server() {
// 	// 创建网络
// 	net := labrpc.MakeNetwork()
// 	// 创建服务器
// 	server := labrpc.MakeServer()
// 	// 创建一个服务
// 	service := labrpc.MakeService(kvserver)
// 	// 向服务器中添加服务
// 	server.AddService(service)
// 	// 向网络中添加服务器
// 	net.AddServer("kv-server", server)
// }

// Get 返回 args 的值和版本。键,如果参数。密钥存在。
// 否则,Get 将返回 ErrNoKey。
func (kv *KVServer) Get(args *rpc.GetArgs, reply *rpc.GetReply) {
	// Your code here.
	kv.mu.Lock()
	vv, ok := kv.kvMap[args.Key]
	defer kv.mu.Unlock()
	if !ok {
		reply.Err = rpc.ErrNoKey
		return
	}
	reply.Value = vv.value
	reply.Version = vv.version
	reply.Err = rpc.OK
}

// 如果参数的键存在,请更新键的值:
//
//	版本与服务器上的密钥版本匹配, 更新: value, Version + 1
//	如果版本不匹配,则返回 ErrVersion。
//
// 如果键不存在,则 Put 安装值 if args.Version 为 0,否则返回 ErrNoKey。
func (kv *KVServer) Put(args *rpc.PutArgs, reply *rpc.PutReply) {
	// Your code here.
	kv.mu.Lock()
	defer kv.mu.Unlock()
	if _, ok := kv.kvMap[args.Key]; !ok {
		if args.Version != 0 {
			reply.Err = rpc.ErrNoKey
			return
		}
		// 创建新条目
		kv.kvMap[args.Key] = &valueVersion{
			value:   args.Value,
			version: 1, // 新键版本从1开始
		}
		reply.Err = rpc.OK
		return
	}
	if args.Version != kv.kvMap[args.Key].version {
		reply.Err = rpc.ErrVersion
		return
	}
	kv.kvMap[args.Key].value = args.Value
	kv.kvMap[args.Key].version++
	reply.Err = rpc.OK
}

// You can ignore Kill() for this lab
func (kv *KVServer) Kill() {
}

// You can ignore all arguments; they are for replicated KVservers
func StartKVServer(ends []*labrpc.ClientEnd, gid tester.Tgid, srv int, persister *tester.Persister) []tester.IService {
	kv := MakeKVServer()
	return []tester.IService{kv}
}

step2

使用键/值 Clerk 实现锁(中等)
在许多分布式应用程序中,运行在不同机器上的客户端使用键/值服务器来协调它们的活动。例如,ZooKeeper 和 Etcd 允许客户端使用分布式锁进行协调,类似于 Go 程序中的线程如何使用锁(即 sync.Mutex)进行协调。Zookeeper 和 Etcd 使用条件写入(conditional put)来实现这种锁。

在本练习中,您的任务是实现一个基于客户端 Clerk.PutClerk.Get 调用的锁。该锁支持两个方法:Acquire(获取)和 Release(释放)。锁的规范是:一次只能有一个客户端成功获取锁;其他客户端必须等待第一个客户端使用 Release 释放锁。

我们在 src/kvsrv1/lock/ 中为您提供了骨架代码和测试。您需要修改 src/kvsrv1/lock/lock.go。您的 AcquireRelease 代码可以通过调用 lk.ck.Put()lk.ck.Get() 与您的键/值服务器通信。

注意:如果客户端在持有锁时崩溃,该锁将永远不会被释放。在比本实验更复杂的设计中,客户端会为锁附加一个租约(lease)。当租约到期时,锁服务器将代表客户端释放锁。在本实验中,客户端不会崩溃,您可以忽略此问题。

实现 AcquireRelease。当您的代码通过 lock 子目录中测试套件的 Reliable 测试时,即完成此练习:

$ cd lock
$ go test -v -run Reliable
=== RUN   TestOneClientReliable
Test: 1 lock clients (reliable network)...
  ... Passed --   2.0  1   974    0
--- PASS: TestOneClientReliable (2.01s)
=== RUN   TestManyClientsReliable
Test: 10 lock clients (reliable network)...
  ... Passed --   2.1  1 83194    0
--- PASS: TestManyClientsReliable (2.11s)
PASS
ok  	6.5840/kvsrv1/lock	4.120s

(如果尚未实现锁,第一个测试会成功。)

此练习需要很少的代码,但比前一个练习需要更多独立思考。

  • 您需要为每个锁客户端生成一个唯一标识符;调用 kvtest.RandValue(8) 生成一个随机字符串。
  • 锁服务应使用一个特定的键来存储“锁状态”(您需要精确决定锁状态是什么)。要使用的键通过 src/kvsrv1/lock/lock.goMakeLock 的参数 l 传递。

下面是代码中的部分注释:

// 测试程序调用 MakeLock() 并传入 k/v clerk;

// 您的代码可以通过调用 lk.ck.Put() 或 lk.ck.Get() 来执行 Put 或 Get。

// 使用 l 作为存储“锁定状态”的键(您必须准确确定锁定状态是什么)。

个人想法:

这个step2刚开始让我有点困惑, 不明白它的意思, 可能是因为之前没接触过分布式锁相关的内容吧,

简单地说就是借助step1实现的kv server的put和get操作区实现一个简单的分布式锁:

  • 简单地说就是把锁的信息以kv的形式存进服务端

  • 我把这个 l 作为key, 把 kvtest.RandValue(8)作为value 存储在step实现的kv server中

  • 用get操作获取锁的状态(k对应的v(反映了目前锁的持有者)), 用put操作来改变锁的状态

代码如下:

lock.go

package lock

import (
	"time"

	"6.5840/kvsrv1/rpc"
	kvtest "6.5840/kvtest1"
)

type Lock struct {
	// IKVClerk 是 k/v clerks 的 go 接口:该接口隐藏了 ck 的特定 Clerk 类型,但承诺 ck 支持Put和Get。
	// 测试人员在调用 MakeLock() 时将 clerk 传入。
	ck kvtest.IKVClerk
	// You may add code here
	key string
	id  string
}

// 测试程序调用 MakeLock() 并传入 k/v clerk;
// 您的代码可以通过调用 lk.ck.Put() 或 lk.ck.Get() 来执行 Put 或 Get。
//
// 使用 l 作为存储“锁定状态”的键(您必须准确确定锁定状态是什么)。
func MakeLock(ck kvtest.IKVClerk, l string) *Lock {
	lk := &Lock{
		ck:  ck,
		key: l,
		id:  " ",
	}
	// You may add code here

	return lk
}

func (lk *Lock) Acquire() {
	// Your code here
	// lk.mu.Lock()
	// defer lk.mu.Unlock()
	value, version, err := lk.ck.Get(lk.key)
	currentClientId := kvtest.RandValue(8)
	if lk.id == value { // 避免重复加锁
		return
	}
	backoff := 10 * time.Millisecond
	maxBackoff := 1 * time.Second
	for {
		if err == rpc.ErrNoKey || value == " " {
			err := lk.ck.Put(lk.key, currentClientId, version)
			if err == rpc.OK {
				lk.id = currentClientId
				break
			}
		}
		new_value, new_version, _ := lk.ck.Get(lk.key)
		value = new_value
		version = new_version
		// 添加等待机制
		time.Sleep(backoff)
		backoff *= 2
		if backoff > maxBackoff {
			backoff = maxBackoff
		}
	}

}

func (lk *Lock) Release() {
	// Your code here

	
	value, version, _ := lk.ck.Get(lk.key)
	if lk.id == value {
		err := lk.ck.Put(lk.key, " ", version)		
	}

}

// 我们无法在分布式锁中使用本地channel来等待,因为锁的状态是分布式的,多个客户端可能在不同的机器上,它们之间无法直接通过本地channel通信。

// 但是,我们可以使用条件变量(sync.Cond)或者channel来在本地实现等待,避免忙等待(即避免不断轮询)。然而,这只能优化同一个进程内的多个goroutine。

// 对于不同进程的客户端,我们无法使用本地同步机制。因此,我们只能通过退避策略(如指数退避)来减少轮询次数。

// 但是,在当前的锁实现中,我们只有一个客户端(一个Lock实例)对应一个键。同一个Lock实例可能被多个goroutine使用吗?根据测试代码,
// 每个客户端(clerk)是独立的,每个Lock实例也是独立的,并且测试中每个客户端运行在自己的goroutine中。

// 因此,我们无法在多个客户端(不同的Lock实例)之间共享本地channel。所以,我们只能使用退避策略。
// 核心问题:分布式环境与本地资源的隔离性
// 1. ​​进程隔离性​​
// 在分布式系统中:

// •
// 每个客户端运行在独立的进程或机器上

// •
// 进程之间不共享内存空间

// •
// 本地 channel 只能在单个进程内工作

// ​​示例​​:

// 假设有两个客户端 A 和 B:

// •
// 客户端 A 运行在机器 1 上

// •
// 客户端 B 运行在机器 2 上

// •
// 客户端 A 创建一个本地 channel 用于等待锁释放

// •
// 客户端 B 无法访问这个 channel,因为它位于不同的进程和机器

// 2. ​​网络分区​​
// 分布式系统面临网络问题:

// •
// 客户端之间可能无法直接通信

// •
// 网络延迟和分区是常态

// •
// 本地 channel 无法跨越网络边界

// ​​示例场景​​:

// 1.
// 客户端 A 持有锁

// 2.
// 客户端 B 尝试获取锁,在本地创建等待 channel

// 3.
// 客户端 A 释放锁

// 4.
// ​​问题​​:客户端 A 无法通知客户端 B 的 channel,因为:

// •
// 不知道客户端 B 的存在

// •
// 无法访问客户端 B 的内存空间

// •
// 网络可能不允许直接通信

// 3. ​​状态一致性​​
// 分布式锁的核心要求:

// •
// 所有客户端必须基于共享状态做决策

// •
// 本地 channel 无法反映全局状态

// ​​示例问题​​:

// 1.
// 客户端 A 持有锁

// 2.
// 客户端 B 和 C 都在等待

// 3.
// 客户端 A 释放锁

// 4.
// 客户端 B 的本地 channel 收到通知

// 5.
// 客户端 B 获取锁

// 6.
// ​​问题​​:客户端 C 不知道锁已被 B 获取,仍在等待

step3

具有消息丢失的键/值服务器(中等)
本练习的主要挑战在于网络可能会重新排序、延迟或丢弃 RPC 请求和/或回复。为了从丢弃的请求/回复中恢复,Clerk 必须不断重试每个 RPC,直到收到服务器的回复。

如果网络丢弃了一个 RPC 请求消息,那么客户端重新发送请求将解决问题:服务器将接收并仅执行重新发送的请求。

然而,网络也可能丢弃 RPC 回复消息。客户端不知道哪条消息被丢弃;客户端只观察到它没有收到回复。如果是回复被丢弃,并且客户端重新发送 RPC 请求,那么服务器将收到该请求的两个副本。这对于 Get 是可以的,因为 Get 不会修改服务器状态。使用相同的版本号重新发送 Put RPC 是安全的,因为服务器根据版本号有条件地执行 Put;如果服务器已经接收并执行了一个 Put RPC,它将对重新传输的该 RPC 副本响应 rpc.ErrVersion,而不是第二次执行 Put

一个棘手的情况是,如果服务器对 Clerk 重试的 RPC 回复了 rpc.ErrVersion。在这种情况下,Clerk 无法知道其 Put 是否已被服务器执行:第一个 RPC 可能已被服务器执行,但网络可能丢弃了服务器的成功响应,因此服务器仅对重传的 RPC 发送了 rpc.ErrVersion。或者,可能是在该 Clerk 的第一个 RPC 到达服务器之前,另一个 Clerk 更新了该键,因此服务器既没有执行该 Clerk 的两个 RPC,并对两者都回复了 rpc.ErrVersion。因此,如果 Clerk 为重传的 Put RPC 收到 rpc.ErrVersionClerk.Put 必须向应用程序返回 rpc.ErrMaybe 而不是 rpc.ErrVersion,因为该请求可能已被执行。然后由应用程序来处理这种情况。如果服务器对初始(非重传的)Put RPC 响应 rpc.ErrVersion,那么 Clerk 应向应用程序返回 rpc.ErrVersion,因为该 RPC 肯定没有被服务器执行。

(对应用程序开发者来说,如果 Put 是精确一次(exactly-once)的(即没有 rpc.ErrMaybe 错误)会更方便,但这很难保证,除非在服务器上为每个 Clerk 维护状态。在本实验的最后一个练习中,您将使用您的 Clerk 实现一个锁,以探索如何使用最多一次(at-most-once)的 Clerk.Put 进行编程。)

现在,您应该修改您的 kvsrv1/client.go,以便在面对丢弃的 RPC 请求和回复时继续工作。客户端 ck.clnt.Call() 返回 true 表示客户端收到了来自服务器的 RPC 回复;返回 false 表示它没有收到回复(更准确地说,Call() 等待回复消息超时,如果在超时间隔内没有回复到达,则返回 false)。您的 Clerk 应不断重新发送 RPC,直到收到回复。请记住上面关于 rpc.ErrMaybe 的讨论。您的解决方案不应要求对服务器进行任何更改。

Clerk 中添加代码,使其在未收到回复时重试。如果您的代码通过了 kvsrv1/ 中的所有测试,即完成此任务,如下所示:

$ go test -v
=== RUN   TestReliablePut
One client and reliable Put (reliable network)...
  ... Passed --   0.0  1     5    0
--- PASS: TestReliablePut (0.00s)
=== RUN   TestPutConcurrentReliable
Test: many clients racing to put values to the same key (reliable network)...
info: linearizability check timed out, assuming history is ok
  ... Passed --   3.1  1 106647 106647
--- PASS: TestPutConcurrentReliable (3.09s)
=== RUN   TestMemPutManyClientsReliable
Test: memory use many put clients (reliable network)...
  ... Passed --   8.0  1 100000    0
--- PASS: TestMemPutManyClientsReliable (14.61s)
=== RUN   TestUnreliableNet
One client (unreliable network)...
  ... Passed --   7.6  1   251  208
--- PASS: TestUnreliableNet (7.60s)
PASS
ok  	6.5840/kvsrv1	25.319s

(在客户端重试之前,它应该等待一小会儿;您可以使用 Go 的 time 包并调用 time.Sleep(100 \* time.Millisecond)

个人想法:

就是网络不稳定,可能会丢失 :

  • 客户端 发送的消息(get/put请求)
  • 服务端的回复

做法: 重发, 加指数退避sleep

代码如下:

client.go

package kvsrv

import (
	"time"

	"6.5840/kvsrv1/rpc"
	kvtest "6.5840/kvtest1"
	tester "6.5840/tester1"
)

type Clerk struct {
	clnt   *tester.Clnt
	server string
}

func MakeClerk(clnt *tester.Clnt, server string) kvtest.IKVClerk {
	ck := &Clerk{clnt: clnt, server: server}
	// You may add code here.
	return ck
}

// Get 获取键的当前值和版本。
// 如果密钥不存在,则返回 ErrNoKey。
// 面对所有其他错误,它会永远尝试。
//
// 您可以使用如下代码发送 RPC:
// ok := ck.clnt.Call(ck.server, “KVServer.Get”, &args, &reply)
//
// 参数和回复的类型(包括它们是否为指针)
// 必须与 RPC 处理程序函数参数的声明类型匹配。
// 此外,回复必须作为指针传递。
func (ck *Clerk) Get(key string) (string, rpc.Tversion, rpc.Err) {
	// You will have to modify this function.
	args := rpc.GetArgs{
		Key: key,
	}
	reply := rpc.GetReply{}

	for { // 因为网络丢包,而重传
		ok := ck.clnt.Call(ck.server, "KVServer.Get", &args, &reply)
		if ok {
			break
		}
		time.Sleep(10 * time.Millisecond)
	}
	if reply.Err == rpc.OK {
		return reply.Value, reply.Version, rpc.OK
	}
	return "", 0, rpc.ErrNoKey
}

// 仅当请求中的版本与服务器上的密钥版本匹配时,才将 update key 与值匹配。
// 如果版本号不匹配,服务器应返回 ErrVersion。
//
//	如果 Put 在其第一个 RPC 上收到 ErrVersion,则 Put 应返回 ErrVersion,因为 Put 肯定不是在服务器上执行的。
//	如果客户端在重新发送 RPC 时收到 ErrVersion,则 Put 必须将 ErrMaybe 返回给应用程序,
//
// 因为服务器可能已经成功处理了它之前的 RPC,但响应丢失了,并且 Clerk 不知道是否执行了 Put。
//
// 您可以使用如下代码发送 RPC:
// ok := ck.clnt.Call(ck.server, “KVServer.Put”, &args, &reply)
//
// 参数和回复的类型(包括它们是否为指针)
// 必须与 RPC 处理程序函数的声明类型匹配
// 参数。此外,回复必须作为指针传递。
func (ck *Clerk) Put(key, value string, version rpc.Tversion) rpc.Err {
	// You will have to modify this function.
	// 有重发机制
	args := rpc.PutArgs{
		Key:     key,
		Value:   value,
		Version: version,
	}
	reply := rpc.PutReply{}
	repeatCnt := 0
	for { // 因为网络丢包,而重传
		ok := ck.clnt.Call(ck.server, "KVServer.Put", &args, &reply)
		if ok {
			break
		}
		repeatCnt++
		time.Sleep(10 * time.Millisecond)
	}
	if reply.Err == rpc.OK {
		return rpc.OK
	}
	if reply.Err == rpc.ErrVersion && repeatCnt == 0 {
		return rpc.ErrVersion
	}
	if reply.Err == rpc.ErrVersion && repeatCnt > 0 {
		return rpc.ErrMaybe
	}
	return rpc.ErrNoKey
}

测试结果:

go test -v
=== RUN   TestReliablePut
One client and reliable Put (reliable network)...
  ... Passed --  time  0.0s #peers 1 #RPCs     5 #Ops    0
--- PASS: TestReliablePut (0.00s)
=== RUN   TestPutConcurrentReliable
Test: many clients racing to put values to the same key (reliable network)...
  ... Passed --  time  4.4s #peers 1 #RPCs 49419 #Ops 49419
--- PASS: TestPutConcurrentReliable (4.37s)
=== RUN   TestMemPutManyClientsReliable
Test: memory use many put clients (reliable network)...
  ... Passed --  time 10.4s #peers 1 #RPCs 100000 #Ops    0
--- PASS: TestMemPutManyClientsReliable (19.86s)
=== RUN   TestUnreliableNet
One client (unreliable network)...
  ... Passed --  time  4.3s #peers 1 #RPCs   271 #Ops  213
--- PASS: TestUnreliableNet (4.31s)
PASS
ok      6.5840/kvsrv1   28.544s

step4

使用键/值 Clerk 和不可靠网络实现锁(简单)
修改您的锁实现,使其在网络不可靠时与您修改后的键/值客户端一起正确工作。当您的代码通过 kvsrv1/lock/ 中的所有测试(包括不可靠网络的测试)时,即完成此练习:

$ cd lock
$ go test -v
=== RUN   TestOneClientReliable
Test: 1 lock clients (reliable network)...
  ... Passed --   2.0  1   968    0
--- PASS: TestOneClientReliable (2.01s)
=== RUN   TestManyClientsReliable
Test: 10 lock clients (reliable network)...
  ... Passed --   2.1  1 10789    0
--- PASS: TestManyClientsReliable (2.12s)
=== RUN   TestOneClientUnreliable
Test: 1 lock clients (unreliable network)...
  ... Passed --   2.3  1    70    0
--- PASS: TestOneClientUnreliable (2.27s)
=== RUN   TestManyClientsUnreliable
Test: 10 lock clients (unreliable network)...
  ... Passed --   3.6  1   908    0
--- PASS: TestManyClientsUnreliable (3.62s)
PASS
ok  	6.5840/kvsrv1/lock	10.033s

个人想法:

简单地说, step3在client已经针对不可靠网络做了重发, 但是这个重发并不等于get/put操作成功, 只能保证客户端最终收到服务端的消息, 但这个消息可能并不是我们所期待的, 所以在lock.go中还要对get/put, 尤其是put操作的返回值加以判断, 并加上重发机制, 来保证达到预期的效果

代码如下:

package lock

import (
	"time"

	"6.5840/kvsrv1/rpc"
	kvtest "6.5840/kvtest1"
)

type Lock struct {
	// IKVClerk 是 k/v clerks 的 go 接口:该接口隐藏了 ck 的特定 Clerk 类型,但承诺 ck 支持Put和Get。
	// 测试人员在调用 MakeLock() 时将 clerk 传入。
	ck kvtest.IKVClerk
	// You may add code here
	key string
	id  string
}

// 测试程序调用 MakeLock() 并传入 k/v clerk;
// 您的代码可以通过调用 lk.ck.Put() 或 lk.ck.Get() 来执行 Put 或 Get。
//
// 使用 l 作为存储“锁定状态”的键(您必须准确确定锁定状态是什么)。
func MakeLock(ck kvtest.IKVClerk, l string) *Lock {
	lk := &Lock{
		ck:  ck,
		key: l,
		id:  " ",
	}
	// You may add code here

	return lk
}

func (lk *Lock) Acquire() {
	// Your code here

	currentClientId := kvtest.RandValue(8)

	backoff := 10 * time.Millisecond
	maxBackoff := 1 * time.Second
	for {
		value, version, err := lk.ck.Get(lk.key)

		if err == rpc.ErrNoKey || value == " " {
			err := lk.ck.Put(lk.key, currentClientId, version)
			if err == rpc.OK {
				lk.id = currentClientId
				break
			}
		}
		if value == currentClientId { // 有可能已经put成功, 但是服务端回复丢失, 客户端重传但version不对,导致err为false
			lk.id = currentClientId
			break
		}
		// 关键修复:添加等待机制
		time.Sleep(backoff)
		backoff *= 2
		if backoff > maxBackoff {
			backoff = maxBackoff
		}
	}

}

func (lk *Lock) Release() {
	// Your code here
	// lk.mu.Lock()
	// defer lk.mu.Unlock()
	backoff := 10 * time.Millisecond
	maxBackoff := 1 * time.Second
	for {

		value, version, _ := lk.ck.Get(lk.key)
		if lk.id == value {
			err := lk.ck.Put(lk.key, " ", version)
			if err == rpc.OK {
				lk.id = " "
				break
			}
			time.Sleep(backoff)
			backoff *= 2
			if backoff > maxBackoff {
				backoff = maxBackoff
			}
			continue
		}
		break // 有可能已经释放, 即服务端kv已经改变,但是服务端回复丢失, 客户端重传但version不对,导致err为false
	}

}



测试结果:

go test -v
=== RUN   TestOneClientReliable
Test: 1 lock clients (reliable network)...
  ... Passed --  time  2.0s #peers 1 #RPCs  1254 #Ops    0
--- PASS: TestOneClientReliable (2.00s)
=== RUN   TestManyClientsReliable
Test: 10 lock clients (reliable network)...
  ... Passed --  time 10.3s #peers 1 #RPCs  1452 #Ops    0
--- PASS: TestManyClientsReliable (10.29s)
=== RUN   TestOneClientUnreliable
Test: 1 lock clients (unreliable network)...
  ... Passed --  time  2.1s #peers 1 #RPCs   133 #Ops    0
--- PASS: TestOneClientUnreliable (2.12s)
=== RUN   TestManyClientsUnreliable
Test: 10 lock clients (unreliable network)...
  ... Passed --  time  8.8s #peers 1 #RPCs   382 #Ops    0
--- PASS: TestManyClientsUnreliable (8.83s)
PASS
ok      6.5840/kvsrv1/lock      23.249s
posted @ 2025-08-17 23:00  msnthh  阅读(1)  评论(0)    收藏  举报