Distributed Systems课程一共包含Lab1-4共4个大作业,Lab1是实现Mapreduce原型,Lab2-4是实现Raft以及基于Raft实现分布式KV存储。
本次实现Raft,该实验(2020 年版本)分为三个部分,目标是开发一个容错的KV系统,分别是 Part 2A:leader 选举、Part 2B:日志同步、lab2C:状态备份。
Put(key, value)
:替换key
对应的value
;Append(key, arg)
:将arg
附加到key
对应到value
,如果不存在则等同于Put
;Get(key)
:获取key
对应的value
,如果不存在返回空串;避免同一个操作被执行多次的策略:为每个客户端设置一个唯一 Id,同时每个命令也有一个唯一 Id,由于单个客户端的请求是串行的,服务端只需要记录每个客户端最后执行的命令 Id 即可。设客户端数量为 $N$,服务器执行的总命令数量为 $M$,可知 $M \gg N$,采用这种策略可以将空间复杂度从 $O(M)$ 降为 $O(N)$
1 | func (ck *Clerk) Get(key string) string { |
由于只有 Leader 会接受用户请求,而 Follower 也需要同步状态,所以对 Raft 返回的 Command 进行 apply 应该作为一个后台 goroutine 自动循环 apply
调用 Raft 接口的 goroutine[0] 不能有阻塞 apply goroutine[1] 的风险,这样会导致:goroutine[0] 等待 Raft 返回,Raft 等待 goroutine[1] 读取 channel,goroutine[1] 被 goroutine[0] 阻塞,最终死锁
1 | func (kv *KVServer) applyCommand(op Op) { |
maxraftstate
为创建快照的 Raft 日志大小阈值,如果为 -1 则无需创建快照,通过persister.RaftStateSize()
获取 Raft 日志大小;persister.SaveStateAndSnapshot()
同时保存 Raft 状态和快照,以使得日志的删除与快照的存储作为一个原子操作;persister.ReadSnapshot()
来读取最新的快照;检查 Raft 日志大小的时机应该是 apply Command 后,因为定时检查无法适应 Raft 日志增长速度,可能在两次检查间有大量 Raft 日志写入,而 apply Command 的速度与 Raft 日志的写入速度是相关的
为了防止阻塞 apply goroutine 造成死锁,不能在 apply goroutine 直接调用 Raft 接口进行 Snapshot,应该开启一个后台 snapshot goroutine 待命
1 | type KVServer struct { |
1 | func (rf *Raft) snapshot(lastIncludedIndex, lastIncludedTerm int, snapshot []byte) { |