11 KiB
6.5840 Lab 2: Key/Value Server
简介
在本实验中你将构建一个单机 key/value 服务器,在网络故障下保证每次 Put 操作至多执行一次,并保证操作满足 linearizable(线性一致性)。你将用该 KV 服务器实现一把锁。后续实验会复制此类服务器以应对服务器崩溃。
KV 服务器
每个客户端通过 Clerk(一组库例程)与 key/value 服务器交互,Clerk 向服务器发送 RPC。客户端可向服务器发送两种 RPC:Put(key, value, version) 和 Get(key)。服务器在内存中维护一个 map,为每个 key 记录 (value, version) 二元组。key 和 value 均为字符串。version 记录该 key 被写入的次数。
-
Put(key, value, version) 仅当该 Put 的 version 与服务器上该 key 的 version 一致时,才在 map 中安装或替换该 key 的值。若 version 一致,服务器还会将该 key 的 version 加一。若 version 不一致,服务器应返回
rpc.ErrVersion。客户端可通过 version 为 0 的 Put 创建新 key(服务器存储的 version 将变为 1)。若 Put 的 version 大于 0 且 key 不存在,服务器应返回rpc.ErrNoKey。 -
Get(key) 获取该 key 的当前值及其 version。若 key 在服务器上不存在,服务器应返回
rpc.ErrNoKey。
为每个 key 维护 version 有助于用 Put 实现锁,并在网络不可靠、客户端重传时保证 Put 的至多一次语义。
完成本实验并通过全部测试后,从调用 Clerk.Get 和 Clerk.Put 的客户端角度看,你将得到一个 linearizable 的 key/value 服务。即:若客户端操作不并发,每个 Clerk.Get 和 Clerk.Put 将观察到由先前操作序列所蕴含的状态修改。对于并发操作,返回值和最终状态将等同于这些操作以某种顺序一次执行一个的结果。若两操作在时间上重叠则视为并发,例如客户端 X 调用 Clerk.Put()、客户端 Y 调用 Clerk.Put(),然后 X 的调用返回。一个操作必须观察到在该操作开始前已完成的全部操作的效果。更多背景见 linearizability 常见问题。
Linearizability 对应用很方便,因为其行为与单台一次处理一个请求的服务器一致。例如,若某客户端从服务器得到一次更新请求的成功响应,之后其他客户端发起的读保证能看到该更新的效果。对单机服务器而言,提供 linearizability 相对容易。
起步
我们在 src/kvsrv1 中提供了骨架代码和测试。kvsrv1/client.go 实现了客户端用于与服务器管理 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 获取最新代码。
$ cd ~/6.5840
$ git pull
...
$ cd src
$ make kvsrv1
go build -race -o main/kvsrv1d main/kvsrv1d.go
cd kvsrv1 && go test -v -race
=== RUN TestReliablePut
One client and reliable Put (reliable network)...
kvsrv_test.go:25: Put err ErrNoKey
--- FAIL: TestReliablePut (0.31s)
...
$
可靠网络下的 key/value 服务器(简单)
第一个任务是在无丢包时实现正确行为。你需要在 client.go 的 Clerk Put/Get 方法中加入发送 RPC 的代码,并在 server.go 中实现 Put 和 Get 的 RPC 处理函数。
当通过测试套件中的 Reliable 测试时,该任务即完成:
$ cd src
$ make RUN="-run Reliable" kvsrv1
go build -race -o main/kvsrv1d main/kvsrv1d.go
cd kvsrv1 && go test -v -race -run Reliable
=== RUN TestReliablePut
One client and reliable Put (reliable network)...
... Passed -- time 0.0s #peers 1 #RPCs 5 #Ops 5
--- PASS: TestReliablePut (0.12s)
=== RUN TestPutConcurrentReliable
Test: many clients racing to put values to the same key (reliable network)...
... Passed -- time 6.3s #peers 1 #RPCs 11025 #Ops 22050
--- PASS: TestPutConcurrentReliable (6.36s)
=== RUN TestMemPutManyClientsReliable
Test: memory use many put clients (reliable network)...
... Passed -- time 29.0s #peers 1 #RPCs 50000 #Ops 50000
--- PASS: TestMemPutManyClientsReliable (52.91s)
PASS
ok 6.5840/kvsrv1 60.732s
$
每个 Passed 后的数字依次为:实际时间(秒)、常数 1、发送的 RPC 数(含客户端 RPC)、执行的 key/value 操作数(Clerk Get 和 Put 调用)。
用 key/value clerk 实现锁(中等)
许多分布式应用中,不同机器上的客户端通过 key/value 服务器协调。例如 ZooKeeper 和 Etcd 允许客户端用分布式锁协调,类似于 Go 程序中线程用锁(如 sync.Mutex)协调。Zookeeper 和 Etcd 用条件 Put 实现这种锁。
你的任务是用 key/value 服务器存储锁所需的每把锁的状态,从而实现锁。可以有多把独立的锁,每把锁有各自的名称,作为 MakeLock 的参数。锁支持两个方法:Acquire 和 Release。规范是:同一时刻只有一个客户端能成功 acquire 某把锁;其他客户端须等第一个客户端用 Release 释放后才能 acquire。
我们在 src/kvsrv1/lock/ 中提供了骨架和测试。你需要修改 src/kvsrv1/lock/lock.go。你的 Acquire 和 Release 应通过调用 lk.ck.Put() 和 lk.ck.Get() 在 key/value 服务器中存储每把锁的状态。
若客户端在持有锁时崩溃,锁将永远不会被释放。在比本实验更复杂的设计中,客户端会为锁附加 lease,lease 过期后锁服务器会代客户端释放锁。本实验中客户端不会崩溃,可忽略该问题。
实现 Acquire 和 Release。当你的代码通过以下测试时,该练习即完成:
$ cd src
$ make RUN="-run Reliable" lock1
go build -race -o main/kvsrv1d main/kvsrv1d.go
cd kvsrv1/lock; go test -v -race -run Reliable
=== RUN TestReliableBasic
Test: a single Acquire and Release (reliable network)...
... Passed -- time 0.0s #peers 1 #RPCs 4 #Ops 4
--- PASS: TestReliableBasic (0.13s)
=== RUN TestReliableNested
Test: one client, two locks (reliable network)...
... Passed -- time 0.1s #peers 1 #RPCs 17 #Ops 17
--- PASS: TestReliableNested (0.17s)
=== RUN TestOneClientReliable
Test: 1 lock clients (reliable network)...
... Passed -- time 2.0s #peers 1 #RPCs 477 #Ops 477
--- PASS: TestOneClientReliable (2.14s)
=== RUN TestManyClientsReliable
Test: 10 lock clients (reliable network)...
... Passed -- time 2.2s #peers 1 #RPCs 5704 #Ops 5704
--- PASS: TestManyClientsReliable (2.36s)
PASS
ok 6.5840/kvsrv1/lock 5.817s
$
若尚未实现锁,前两个测试也会通过。
该练习代码量不大,但比前一练习需要更多独立思考。
- 每个锁客户端需要一个唯一标识;可调用 kvtest.RandValue(8) 生成随机字符串。
存在丢包时的 key/value 服务器(中等)
本练习的主要挑战是网络可能重排、延迟或丢弃 RPC 请求和/或回复。为从丢弃的请求/回复中恢复,Clerk 必须不断重试每个 RPC 直到收到服务器回复。
-
若网络丢弃了 RPC 请求,客户端重发请求即可:服务器只会收到并执行一次重发的请求。
-
但网络也可能丢弃 RPC 回复。客户端无法区分哪种情况,只能观察到没收到回复。若是回复被丢弃且客户端重发 RPC 请求,服务器会收到两份请求。对 Get 没问题,因为 Get 不修改服务器状态。用相同 version 重发 Put RPC 也是安全的,因为服务器按 version 条件执行 Put;若服务器已收到并执行过该 Put RPC,会对重传的同一 RPC 回复
rpc.ErrVersion而不会再次执行。
一个棘手情况是:Clerk 重试后,服务器用 rpc.ErrVersion 回复。此时 Clerk 无法确定自己的 Put 是否已被执行:可能是第一次 RPC 已被执行但服务器发出的成功回复被网络丢弃,所以服务器仅对重传的 RPC 回复了 rpc.ErrVersion;也可能是另一个 Clerk 在该 Clerk 的第一次 RPC 到达前更新了 key,所以服务器两次都没执行该 Clerk 的 RPC,并对两次都回复 rpc.ErrVersion。因此,若 Clerk 对重传的 Put RPC 收到 rpc.ErrVersion,Clerk.Put 必须向应用返回 rpc.ErrMaybe 而不是 rpc.ErrVersion,因为请求可能已执行。应用负责处理这种情况。若服务器对首次(非重传)Put RPC 回复 rpc.ErrVersion,则 Clerk 应向应用返回 rpc.ErrVersion,因为该 RPC 确定未被服务器执行。
若 Put 能实现恰好一次(即没有 rpc.ErrMaybe 错误)会对应用开发者更友好,但在不为每个 Clerk 在服务器维护状态的情况下难以保证。本实验最后一个练习中,你将用 Clerk 实现锁,以体会在至多一次 Clerk.Put 下如何编程。
现在应修改 kvsrv1/client.go,在 RPC 请求或回复被丢弃时继续重试。客户端 ck.clnt.Call() 返回 true 表示收到了服务器的 RPC 回复;返回 false 表示未收到回复(更准确地说,Call() 在超时时间内等待回复,超时内未收到则返回 false)。你的 Clerk 应持续重发 RPC 直到收到回复。请牢记上面关于 rpc.ErrMaybe 的讨论。你的方案不应要求修改服务器。
在 Clerk 中加入未收到回复时的重试逻辑。当你的代码通过 kvsrv1 的全部测试时,该任务即完成:
$ make kvsrv1
go build -race -o main/kvsrv1d main/kvsrv1d.go
cd kvsrv1 && go test -v -race
=== RUN TestReliablePut
One client and reliable Put (reliable network)...
... Passed -- time 0.0s #peers 1 #RPCs 5 #Ops 5
--- PASS: TestReliablePut (0.12s)
=== RUN TestPutConcurrentReliable
...
=== RUN TestUnreliableNet
One client (unreliable network)...
... Passed -- time 4.0s #peers 1 #RPCs 268 #Ops 422
--- PASS: TestUnreliableNet (4.13s)
PASS
ok 6.5840/kvsrv1 64.442s
$
- 重试前客户端应稍等;可使用 Go 的 time 包并调用 time.Sleep(100 * time.Millisecond)。
不可靠网络下用 key/value clerk 实现锁(简单)
修改你的锁实现,使其在网络不可靠时能与修改后的 key/value 客户端正确配合。当你的代码通过 lock1 的全部测试时,该练习即完成:
$ make lock1
go build -race -o main/kvsrv1d main/kvsrv1d.go
cd kvsrv1/lock; go test -v -race
=== RUN TestReliableBasic
...
=== RUN TestOneClientUnreliable
Test: 1 lock clients (unreliable network)...
... Passed -- time 2.1s #peers 1 #RPCs 66 #Ops 57
--- PASS: TestOneClientUnreliable (2.18s)
=== RUN TestManyClientsUnreliable
Test: 10 lock clients (unreliable network)...
... Passed -- time 4.1s #peers 1 #RPCs 778 #Ops 617
--- PASS: TestManyClientsUnreliable (4.23s)
PASS
ok 6.5840/kvsrv1/lock 12.227s
$