文章目录
说明
- 在实验二的基础上,实验三整体来说还比较简单。同样的,这里只描述实验完成的整体思路和问题解决方法,只有代码骨架,不填充具体逻辑。实验二ABCD我之前也写过了。如果需要代码可以留言。
总体方法
总体架构
- 我们首先要了解实现的整体架构是什么,关于这个,最好的展示就是网站中raft_diagram.pdf那张图。 一些核心的前提:KVServer(server.go) 是 State Machine, 每个KVServer对应一个Raft peer node, KVServer之间只通过Raft 服务来达成共识,不会直接交互。 根据paper Client interaction 的说明,KVServer应该指导请求来自哪个具体的客户端,且保存每个客户端的请求状态。即我们需要给每个客户端一个唯一的ID, 且其同一个请求需要有唯一的Seq, 两者结合来确定一个唯一性的请求。
- 客户端的ID用 nrand()生成即可,测试最多7个客户端,不会重复,生产环境下会用IP:PORT的方法去定位具体的Client. 每个Clinet 维护一个lastRequestID, 来提供每一次请求的Seq.
一次请求的流程
- 画了张图来描述它。
- Server(Leader)收到Client的Request
- 通过raft.Start() 提交Op给raft Leader, 然后开启特殊的Wait Channel 等待Raft 返回给自己这个Req结果。
- 底层共识达成后,Raft的所有peer会apply 这个Op, 每个Server会在单独的线程ApplyLoop 中等待不断到来的ApplyCh中的Op .
- 执行这个Op(GET全部执行,重复的PUT, APPEND不执行)
- Apply Loop 根据Op中的信息将执行结果返回给Wait Channel , 注意,只有Leader中才有Wait Channel 在等结果
- 返回执行结果给Request
- 我们可以进一步总结这个过程,这个过程在Lab4中也能使用,Lab4扩展了Lab3中的Get, Put, Append操作。
- 在一个Loop里,全员监听一个指标,当这个指标发生变化时,会触发操作
- 只有Leader能够触发这个条件,follower不主动触发,等待被同步
- 条件被触发后,Leader判定条件是否准确,交由Raft特定的Op Log Entry; 根据需要设置WaitChan等待结果,同时设置Timeout
- Raft将Leader Op 同步给大家,ApplyEntry给上层每个Server
- Servers(包括Leader)读取Raft Apply Op 进行如下操作
- Dupulication Detection
- 执行Write,Update这种类型的操作,Read类型操作不执行
- 判定Snapshot条件,满足,则SnapShot
- 如果有WaitChan在等待,返回结果给WaitChan
- Leader通过WaitChan接受结果,执行Read类型操作,返回结果给Client. 或者Leader WaitChan超时,返回Client结果。
关于Server对Raft请求的超时问题
- 有可能出现Server Start Command, 一直在等待的问题,其实超过一定时间,即说明Raft共识已经失败了。但无论是waitChan 还是labrpc中的Call方法,都没有“回调超时”的概念,会阻塞在哪里。
// Call() is guaranteed to return (perhaps after a delay) *except* if the
// handler function on the server side does not return.
- 因此,我们已经手动为请求添加Timeout, 在Client 或者Server端都可以,请求发出后,如果超时没有回复,则认为 ErrWrongLeader, 并按逻辑重发即可
Duplicate Request的处理
- 实验A的核心作用就是处理Duplicate Request . 保证重复的请求不会在状态机上被执行两边(同一ClientID:RequestID).
- 这里的核心思想是KVServer来处理Duplicate 问题,而不是交由Raft来处理。对于我们收到的请求,无论是否重复,我们都提交给Raft作为它的log. 而KVServer 负责在接受apply log时判定这个log代指的Request Op是不是重复的,是重复的,我们就不在状态机上执行,直接返回OK即可(实际上GET无需考虑这个问题,因为他不会改变状态机的存储内容)。
关于Leader路由问题
- 这里有一个可能坑人的地方,就是Clinet . servers[] 的序号和KVServers.me 不是 1 1对应的,而是随机shuffle过的。 但是KVServer.me 和 Raft.me 是对应的。 这就导致了Client 发送请求到一个不是LEADER的KVServer, 这个KVServer可以拿到raft.leader的序号并传回Client, 但Client 并不能通过client.servers[leaderNumber]来找到真正的Leader, 还是要随机访问另一个。
- 因此提示里说的Server传回leader位置的优化做不到,除非你把这个随机的shuffle 慢慢学出来。所以说我们收到ErrWrongLeader时候,只要再随机访问另一个就行了。
关于Snapshot & Raftstate
- 先说明概念:RateSate(currentTerm, voteFor, logEntries 和你要自己存储的状态)。 Snapshot (Server 维护的KeyValue数据库,其实就是内存中一个map, 和Server维护的关于每个Client Request的状态)。
同样的,我们画图来理解这个流程
对于Leader这里:
- Raft Apply new LogEntries( CommandValid : true ), 这一举动说明RaftStateSize 在增加
- Leader执行操作,并根据RaftStateSize 和阈值maxraftestatesize判断是否需要命令Raft进行Snapshot
- 如果需要,则将自身的KVDB,RequestID等信息制作成snapshot, 并调用Leader Raft peer node 的 Snapshot () 接口
- Leader 安装Snapshot , 这分为三部分,修剪log Entries [] , SnapShot 通过Persister进行持久化存储, 在Appendentries中将本次的SnapShot信息发送给落后的Follower
- 返回执行结果给WaitChannel
对于Follower :
1 . InstallSnapshot 处理Leader的RPC请求,获取 snapshot数据, 裁剪log, 通过ApplyCh上报给Server (SnapshotValid: true)
2 . Applyloop 收到请求,调用CondInstallSnapshot() 来询问是否可以安装snapshot
3 . CondInstallSnapshot() 判定snapshot安装条件,持久化snapshot, 并通知Server可以InstallSnapshot
[这里说明一下,我的CondInstallSnapshot()直接返回了true, 因为我再InstallSnapshot RPC Handler中已经进行裁剪log, 如果CondaInstallSnapshot() 返回false, 则会造成Raftstate 和 snapshot的不一致 ; 其实我也没理解好这个CondInstallSnapshot()有什么用,至少在我的实现逻辑中,它并没有返回false的必要,求教求教]
和实验二的联系
- 这里我的理解就是 【尽可能对Raft进行最少的修改】和 【即使通过实验二的测试,Raft也还是可能有问题】
- 在实验二的基础上,我再实现实验三的时候没有对Raft有任何逻辑上的大改动,只不过排查出了一个小问题,即我把 lastApplied 也当作Raftstate 进行持久化了,导致所有Server一起restart 之后无法apply之前的所有log.