高可用的 Key-Value 服务

第三个 Lab, 相对比较轻松的一个, 目标是在 Lab2 实现的 Raft 协议基础上, 实现一个高可用的键值对服务, raft 在这里的职责就是负责这个强一致性服务的高可用, 通过复制和选举, 对应到 kv 服务的任务就是将读写请求发给 raft 复制, 并且由于要求强一致性, 所有的读写只能通过 leader 节点日志复制后才能得到响应 (完全线性化)

这个 kv server 也是一个集群, 集群里每个节点都对应一个 raft 节点 , 在启动 kv server 里一个节点的时候, 同时也要启动一个 raft 节点

这样之后, 当某个 kv 节点的 raft 节点为 leader 时, 这个 kv 节点就对外提供读写服务

这里的客户端实现不考虑性能, 直接轮询所有的 kv 节点, 如果是 leader 就向其发出请求

重点是 kv 节点和 raft 节点的交互, 它们之间是异步的, kv 节点会通过一个 channel 收到 raft 节点成功的响应, 因此需要开一个协程1负责监听这个 channel ,

又因为kv 客户端和 kv server 节点的交互是同步的, 势必需要阻塞在等待 raft 响应的地方, 等待协程 1 通知结果

协程 1 又如何确定哪个结果是哪个请求的呢, 因此就需要给每个请求都附加一个唯一 ID, 并且和一个 channel 关联上, 到时候通知就根据唯一 ID 取到对应的 channel, 并从这个 channel 发送结果给kv 客户端

关于请求防重 (Duplicate request) 的要求

场景: 网络不可靠的情况, 例如client 发给 leader 请求后, 响应超时, 重试

request id 设为每个 client 各有一个单调递增的request id 计数, 在 server 端会记录下最近一次完成的请求 request id , 这样子如果是之前旧的, 重复的请求, 就会小于等于当前的 request id

最新的一个结果会记录下来, 有重复的返回

leader可能会更换, 因此请求相关的属性也要作为日志的一部分复制到每个节点

关于 3B

日志压缩 / 快照

日志压缩的描述:

set a 1
set a 2

两条日志可以合并为
set a 2

raft 的职责是为上层提供复制服务, 所以除了通过日志的形式之外, 还可以通过快照的形式实现复制, 以此来节约空间

由于快照会比单条的日志大得多, 复制的开销更大, 所以快照只会在超过某个阈值 (日志过大) 的情况下触发, 并复制到其余大多数节点, 之后将已包含在快照里的日志删除

当一个新的 kv 节点 + raft 节点加入后, 日志的索引值一定小于 leader 节点现存的日志索引值, 这时候就需要先将快照发给 raft 节点进行保存和 apply, 之后再进行日志复制

问题记录

lab3 自己是用状态机的模型写的, 虽然能过但是在 putAppend 和 get 都加了锁,并发很低 , 去学习了别人的实现 , 发现设计思路实在是太优秀了 , 模仿了 raft 的 start()函数和 applyCh , 对每个 op 进行 start , 然后每个 op 分配一个 channel 等待结果 , 并发无敌 , 感叹自己实在是想不到这种写法 (模型也套娃 ?)

lab3a 先把guide的hint都理解了 复述一遍,不然浪费时间 ,tryapply取出来 容忍kv状态机延后获取 ,忘记对follower的状态机进行指令复述操作了

每次都会多append一个

重复请求?

未复制到大多数的commit ?

guide 阅读笔记

强一致性 leads to linearizability
每个server都有一个关联的raft peer
put , append , get 只会发给是leader的raft server
每个op都要提交到raft
每个kvserver都要execute op from raft log in order
ck不知道谁是leader, 如果发到非leader,需要重试发给其他server,如果server已经提交op到raft (这也意味着apply到kv状态机中了)  , 该server(leader)就可以返回结果给客户端
如果op commit失败 ( 比如leader换人了 ) , server告知wrong leader , client重试其他server
kvserver之间只通过raft沟通
确保lab2 reliably pass
允许在ApplyMsg和AppendEntries中加字段
go test -race 检查deadlock
网络和server问题 , ck可能发生同一个rpc多次 , 场景: leaderA提交了一个entry后失败了,ck没收到reply, 会把请求发给其他leader , 所以要保证put和append的幂等 , 要给command加上id , 重复id , 并保存到一个map中, 如果该请求已经commit过,那直接返回请求结果
解决重复请求,包括ck发请求到leaderA term1 , 等待reply , 没收到 , 然后发请求到leaderB term2的情况
go test - run 3A

需要解决的一个场景: leader 已经对某个ck rpc执行了start , 但是在commit这个log之前失去了leadership , ck需要重试其他server直到找到新leader , 一个做法就是server检测自己失去了leadership , 检测的方法是发现另一个不同的请求出现在同一index上?? , 或者raft term改变 ,
 如果前任leader发现了网络分区, 他不会知道谁是leader , that’s fine , 等待网络分区恢复
优化: 记住上一次的leader, 先发
op需要加unique id , 保证exact just once