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

浙公网安备 33010602011771号