Lab 4: Fault-tolerant Key/Value Service
Lab 4: Fault-tolerant Key/Value Service
一、介绍
本次Lab将使用Lab3的Raft库构建容错键/值存储服务。您的KV服务将是一个复制状态机,由多个Server组成,每个Server维护一个KV数据库,就像Lab2一样,使用Raft保证Server状态一致。只要大多数服务器处于活动状态并且可以通信,你的键/值服务就应该继续处理客户端请求,而不管是否有故障或网络分区。Lab4中将实现Raft 交互图中的所有部分(Clerk、Service 和 Raft)。
客户端将用和Lab2相同的方式与服务器进行交互。三种RPC:
Put(key, value)
:替换数据库中Key的值Append(key, arg)
:将arg追加到到key对应的的值后面(如果key不存在,则将现有值视为空字符串)Get(key)
:获取Key对应的Value(若Key不存在,返回空字符串)
键和值都是字符串,Put
和Append
不返回值。客户端使用 Put/Append/Get
方法,通过Clerk
与Server
进行通信。Clerk管理与服务器的RPC交互
Server必须确保Get/Put/Append方法的调用是线性化的。如果每次调用都是顺序进行的,Get/Put/Append方法应该表现得好像系统只有一个状态副本,并且每次调用都应能观察到前面一系列调用对状态的修改。对于并发调用,返回值和最终状态必须相同,就像操作按某种顺序依次执行一样。如果两个调用在时间上重叠,则认为它们是并发的:例如,客户端X调用Clerk.Put(),客户端Y调用Clerk.Append(),然后客户端X的调用返回。在一个调用开始时,它必须能观察到所有在其之前完成的调用的效果。
对单个服务器提供线性化相对容易。如果服务是复制的,就比较困难,因为所有服务器必须使用相同的执行顺序来执行并发请求,必须避免使用非最新状态回复客户端,并且在故障恢复时保留所有已提交的数据。
本Lab分两部分。
- A: 使用Lab 3的Raft实现构建一个复制的键值服务,但不使用快照。
- B: 使用Lab 3D中的快照实现,这将允许Raft丢弃旧的日志条目。
建议复习Raft论文,特别是7,8节。
为了获得更广泛的视角,可以参考Chubby、Paxos Made Live、Spanner、Zookeeper、Harp、Viewstamped Replication 和 Bolosky 等人的研究。
入门
src/kvraft
提供了基础代码和相关测试。你需要修改kvraft/client.go
、kvraft/server.go
,也许还有kvraft/common.go
。
执行以下命令启动并运行。不要忘记使用git pull
来获取最新软件。
$ cd ~/6.5840
$ git pull
...
$ cd src/kvraft
$ go test
...
$
Part A:无快照的KVServers(中等/困难)
你的每个键值服务器都会有一个关联的Raft节点。客户端(Clerks)发送Put()
、Append()
,Get()
请求到Raft Leader所在kvserver。kvserver将Put/Append/Get
操作提交给Raft,Raft日志将这些操作记录下来。之后kvservers顺序执行日志中的操作,将操作应用到各自的键值数据库;从而让所有服务器保持相同的键值数据库副本。
有时Clerk不知道哪个kvserver是Raft Leader。如果Clerk将请求发送到了错误的kvserver,或者无法联系到kvserver,Clerk应尝试向其他kvserver发送请求。如果键值服务将操作提交到了它的Raft日志(并因此将该操作应用到键值状态机),领导者将通过响应RPC报告结果给客户端。如果操作未能提交(例如,Leader被替换),服务器将报告错误,客户端将尝试向另一个服务器发送请求。
kvserver之间只能通过Raft进行交互。
Task
第一个任务是实现一个没有消息丢失和服务器失败情况下的解决方案。
可以将Lab 2中的客户端代码(kvsrv/client.go)复制到kvraft/client.go 中。你需要添加逻辑,以决定每个请求应该发送到哪个kvserver。记住,Append()不再返回值。
继续在server.go中实现Put()、Append()和Get()的处理函数。这些处理函数应该使用Start()将一个操作(Op)加入Raft日志中;你需要在server.go中填充 Op结构体的定义,使其能够描述Put/Append/Get操作。每个Server应在Raft 提交操作时(即操作出现在applyCh上时)执行Op命令。RPC处理函数应该注意到 Raft何时提交了它的Op,然后回复该RPC请求。
当你通过第一个测试 “One client” 时,任务就完成了。
提示:
-
调用
Start()
后,你的kvservers需要等待Raft完成一致性协商。已达成一致的命令会到达applyCh
。你的代码需要不断读取applyCh
。同时Put()
、Append()
和Get()
处理函数使用Start()
向Raft日志提交命令。注意 kvserver与Raft库之间的死锁。 -
如果kvserver不属于多数派,它不应该完成
Get()
RPC(以避免提供过时的数据)。一个简单的解决方案是将Get()
也记录在Raft日志中。不需要实现第8节中描述的只读操作的优化。 -
你不需要向Raft的
ApplyMsg
或Raft RPC(例如AppendEntries
)添加任何字段,但你可以这么做。 -
最好从一开始就添加锁机制,因为避免死锁的需求有时会影响整体代码设计。通过运行
go test -race
检查你的代码是否是无竞态的。
接下来修改你的代码,使其能够在网络和服务器故障的情况下正常运行。将面临的问题是,Clerk可能需要多次发送RPC请求,直到找到正常运行的kvserver。如果Raft Leader刚提交一条日志到Raft Log后就失效了,Clerk可能无法收到回复,因此需要重新发送请求到另一个Leader。所有命令都只能执行一次,你需要确保服务器不会重复执行重新发送的请求。
Task
添加代码以处理故障和重复的Clerk请求,包括这样的场景:Clerk在某个任期内向kvserver的Leader发送请求,因等待回复超时,在新任期内又将请求发送给了新的Leader。这个请求只能被执行一次。这里的说明文档提供了有关重复检测的指导。你的代码需要通过
go test -run 4A
测试。
提示:
- 你需要处理这样的情况:一个Leader调用了Start()处理Clerk的RPC请求,但在该请求提交到日志之前失去了领导权。这种情况下,你应该让Clerk重新发送请求到其他服务器,直到找到新的Leader。可以通过以下方式实现:kvserver检测Leader是否失去领导权,比如发现Raft的任期发生了变化,或者在Start()返回的索引处出现了不同的请求。如果原来的Leader被网络分区隔离,它可能不知道新的Leader出现了;但同样处于该分区内的客户端也无法联系到新Leader,因此在这种情况下,允许服务器和客户端无限期等待,直到分区恢复。
- 你可能需要修改Clerk以记录上一次RPC请求中找到的Leader,并在下一次RPC 时首先将请求发送给该服务器。避免浪费时间去寻找Leader,从而更快地通过某些测试。
- 你应该使用类似于Lab 2的重复请求检测机制。该机制能够快速释放服务器内存,例如通过每个新的RPC可以默认表示客户端已经接收了上一次RPC的回复。你可以假设每个客户端一次只会向Clerk发起一个调用。你可能需要根据实际情况,修改 Lab 2中重复检测表里存储的信息。
你的代码应该通过Lab 4A测试,像这样
$ go test -run 4A
Test: one client (4A) ...
... Passed -- 15.5 5 4576 903
Test: ops complete fast enough (4A) ...
... Passed -- 15.7 3 3022 0
Test: many clients (4A) ...
... Passed -- 15.9 5 5884 1160
Test: unreliable net, many clients (4A) ...
... Passed -- 19.2 5 3083 441
Test: concurrent append to same key, unreliable (4A) ...
... Passed -- 2.5 3 218 52
Test: progress in majority (4A) ...
... Passed -- 1.7 5 103 2
Test: no progress in minority (4A) ...
... Passed -- 1.0 5 102 3
Test: completion after heal (4A) ...
... Passed -- 1.2 5 70 3
Test: partitions, one client (4A) ...
... Passed -- 23.8 5 4501 765
Test: partitions, many clients (4A) ...
... Passed -- 23.5 5 5692 974
Test: restarts, one client (4A) ...
... Passed -- 22.2 5 4721 908
Test: restarts, many clients (4A) ...
... Passed -- 22.5 5 5490 1033
Test: unreliable net, restarts, many clients (4A) ...
... Passed -- 26.5 5 3532 474
Test: restarts, partitions, many clients (4A) ...
... Passed -- 29.7 5 6122 1060
Test: unreliable net, restarts, partitions, many clients (4A) ...
... Passed -- 32.9 5 2967 317
Test: unreliable net, restarts, partitions, random keys, many clients (4A) ...
... Passed -- 35.0 7 8249 746
PASS
ok 6.5840/kvraft 290.184s
Passed后面的数字含义:
实际运行时间(以秒为单位)、节点数量、发送的RPC数量(包括客户端的 RPC),以及执行的键/值操作数量(Clerk的Get/Put/Append调用)。
Part B: Key/value service with snapshots (hard)
目前的KVServer不会调用Raft的Snapshot()
,因此重新启动的服务器必须重播完整的Raft日志才能恢复其状态。现在,使用Lab 3D中的Raft的Snapshot()
方法,修改kvserver与Raft配合使用,以节省日志空间并减少重新启动时间。
Tester将maxraftstate
传递给StartKVServer()
。maxraftstate表示持久 Raft状态的最大允许大小(单位为字节,包括日志,但不包括快照)。你应该将 maxraftstate与persister.RaftStateSize()
进行比较。
每当你的KVServer检测到Raft状态大小接近此阈值时,应通过调用Raft的Snapshot
来保存快照。如果maxraftstate
为-1,则不必快照。maxraftstate适用于你的Raft作为persister.Save()
的第一个参数传递的GOB编码字节。
修改你的kvserver,使其能够检测持久化的Raft状态是否过大,并将快照交给Raft。当kvserver重新启动时,它应该从persister读取快照,并从快照中恢复其状态。
提示
- 思考一下kvserver何时应该对其状态进行快照,以及快照中应包含哪些内容。Raft使用
Save()
将每个快照以及相应的Raft状态存储在persister
中。你可以使用ReadSnapshot()
读取最新存储的快照。 - kvserver必须能够在跨越检查点时检测到日志中的重复操作,因此用于检测这些操作的任何状态都必须包含在快照中。
- 将快照中存储的结构的所有字段必须以大写字母开头
- 您的Raft库中可能存在本实验中暴露的错误。如果您对 Raft 实现进行了更改,请确保它继续通过所有实验 3 测试。
- Lab 4 测试的合理时间是 400 秒实际时间和700秒 CPU 时间。此外,
go test -run TestSnapshotSize
应花费少于20秒的实际时间。
你的代码应该通过4B、4A、Lab 3测试
$ go test -run 4B
Test: InstallSnapshot RPC (4B) ...
... Passed -- 4.0 3 289 63
Test: snapshot size is reasonable (4B) ...
... Passed -- 2.6 3 2418 800
Test: ops complete fast enough (4B) ...
... Passed -- 3.2 3 3025 0
Test: restarts, snapshots, one client (4B) ...
... Passed -- 21.9 5 29266 5820
Test: restarts, snapshots, many clients (4B) ...
... Passed -- 21.5 5 33115 6420
Test: unreliable net, snapshots, many clients (4B) ...
... Passed -- 17.4 5 3233 482
Test: unreliable net, restarts, snapshots, many clients (4B) ...
... Passed -- 22.7 5 3337 471
Test: unreliable net, restarts, partitions, snapshots, many clients (4B) ...
... Passed -- 30.4 5 2725 274
Test: unreliable net, restarts, partitions, snapshots, random keys, many clients (4B) ...
... Passed -- 37.7 7 8378 681
PASS
ok 6.5840/kvraft 161.538s