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.Get
和 Clerk.Put
的客户端角度来看,您将拥有一个线性一致的键/值服务。也就是说,如果客户端操作不是并发的,每个客户端的 Clerk.Get
和 Clerk.Put
将观察到由先前操作序列所隐含的状态修改。对于并发操作,返回值和最终状态将与这些操作按某种顺序一次执行一个相同。如果操作在时间上重叠,则它们是并发的:例如,如果客户端 X 调用 Clerk.Put()
,然后客户端 Y 调用 Clerk.Put()
,接着客户端 X 的调用返回。一个操作必须观察到在该操作开始之前所有已完成操作的效果。有关线性一致性的更多背景信息,请参阅 FAQ。
线性一致性对应用程序来说很方便,因为它是您从单台服务器(一次处理一个请求)看到的行为。例如,如果一个客户端从服务器收到更新请求的成功响应,那么随后启动的其他客户端的读取操作保证能看到该更新的效果。对于单台服务器来说,提供线性一致性相对容易。
开始
我们在 src/kvsrv1
中为您提供了骨架代码和测试。kvsrv1/client.go
实现了一个 Clerk
,客户端用它来管理与服务器的 RPC 交互;Clerk
提供了 Put
和 Get
方法。kvsrv1/server.go
包含服务器代码,包括实现 RPC 请求服务器端的 Put
和 Get
处理程序。您需要修改 client.go
和 server.go
。RPC 请求、回复和错误值定义在 kvsrv1/rpc
包中的文件 kvsrv1/rpc/rpc.go
中,您应该查看该文件,但无需修改 rpc.go
。
要开始运行,请执行以下命令。别忘了 git pull
以获取最新软件。
step1
具有可靠网络的键/值服务器(简单)
您的第一个任务是在没有消息丢失的情况下实现一个解决方案。您需要在 client.go
中的 Clerk
的 Put
/Get
方法中添加发送 RPC 的代码,并在 server.go
中实现 Put
和 Get
的 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)以及执行的键/值操作数量(Clerk
的 Get
和 Put
调用)。
使用 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.Put
和 Clerk.Get
调用的锁。该锁支持两个方法:Acquire
(获取)和 Release
(释放)。锁的规范是:一次只能有一个客户端成功获取锁;其他客户端必须等待第一个客户端使用 Release
释放锁。
我们在 src/kvsrv1/lock/
中为您提供了骨架代码和测试。您需要修改 src/kvsrv1/lock/lock.go
。您的 Acquire
和 Release
代码可以通过调用 lk.ck.Put()
和 lk.ck.Get()
与您的键/值服务器通信。
注意:如果客户端在持有锁时崩溃,该锁将永远不会被释放。在比本实验更复杂的设计中,客户端会为锁附加一个租约(lease)。当租约到期时,锁服务器将代表客户端释放锁。在本实验中,客户端不会崩溃,您可以忽略此问题。
实现 Acquire
和 Release
。当您的代码通过 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.go
中MakeLock
的参数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.ErrVersion
,Clerk.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