概述
实习安排
本人是在大三暑假参加实习,完成此项目耗时一个多月。
受时间所限,没有进一步完善项目和文档,若有理解不当之处,还望指出。
衷心感谢前辈们的耐心指导,相信这门课一定会吸引更多人参与进来!
经验总结
工具提示
- 修改日志打印模块,增加控制开关。
- 对于不容易复现的 bug,需要将输出定向到文件中。建议输出信息尽可能详细,若文件很大,可以使用 grep 等工具查看。
- 可以修改
kv/test_raftstore/cluster.go
文件下的dbPath
以及Makefile
的 clean 规则,这样同时跑多个测试就不会发生文件冲突。
- 可以考虑 Goland 自带的远程开发,自动代码同步非常丝滑,跑很久可以挂 tmux 里。评测机器越多自然越方便(尤其本人的 3b 在远程机器上正确率明显提升)。
Project1 StandaloneKV
Project1 难度不大,主要帮助我们熟悉代码框架,掌握 go 语法,了解Column family
等概念。
Standalone
说明是单一结点,尚未进入分布式阶段,我们只需要调用提供的底层 API 和工具函数,包装实现基础的键值操作服务即可。
badger
并不支持 CF ,因此我们需要对它做一层封装。
需要注意的是,engine_util
包中提供了很多方便的工具函数,熟悉它们的使用即可。
Project2 RaftKV
概述
由官方提供的框架图,可将 TinyKV 项目分为三层:
- Server
- 从
TinySQL
客户端接收请求,解析出 raft 请求,并交由 Storage 层处理。
- 得到 Storage 层返回的数据,响应客户端。
- Storage
- 收到 Server 层的 raft 请求,形成日志,在集群中进行同步,对应
Propose
阶段。
- 拿到同步的日志,对日志的请求内容进行应用,对应
Apply
阶段。
- Engine
- 底层存储引擎。应用请求内容的读写数据,需要使用 Engine 层提供的接口。
Project 2a
这部分主要回归论文,一些实现细节可以参考 etcd 源码。
2a 的测试非常宽松,但如果实现不准确,会给之后的调试带来很多麻烦。
一定要仔细读论文!
照着论文实现并不困难,但还是有一些要注意的点:
- 根据测试集,可以发现,当 Candidate 收到的拒绝票超过半数时,应直接转回 Follower 状态,因为本轮已经不可能竞选成功。
- 在具体实现时,状态改变对应三个 become 函数。它们的实现不能缺少必要的重置,例如 Lead, votes 等。同时需要注意的是,状态变更后并不是所有数据都要重置,否则会导致超时等 bug。
例如,我们只需要在必要的时候对选举计时器进行重置:
- 收到 Leader 的心跳。
- 收到 AppendEntry RPC 请求。
而在 becomeFollower 时,无需重置,否则在 2b 的一些测试点中可能会导致选举超时。
- RaftLog 在内存中缓存了日志信息,因此理清各个索引的含义是至关重要的。
Project 2b
这一部分涉及 Multi-Raft,可以看官网的文章:Muti-Raft。
虽然实现的代码很少,但要看的源码就很多了,涉及到更多上层细节。
调试阶段感觉全在调 2a 的 bug。
RaftStore
向上提供接口,向下对底层数据库进行读写,完成服务器请求的接收和响应的发送。
RaftStorage
创建一个RaftStore
来驱动Raft
。
RaftStore 负责管理 Peer 结构,消息传递等。
RaftStore 在启动时,会创建一些 Workers,创建全局信息并保存,从底层引擎中加载所有的 Peer,并在 router 中注册。
RaftWorker
RaftWorker 运行 Raft command。
它通过 PeerMsgHandler
来处理消息。而 PeerMsgHandler 的相应方法则是我们需要补充实现的。
在其最重要的启动方法 run
中,就是一个select
循环,不停地做以下操作直到收到 closeCh
传来的结束信号:
从router
的peerSender
,也就是自身的raftCh
中拿到消息,依次产生 PeerMsgHandler 处理消息,进行 Propose 等,处理后调用 HandleRaftReady
,拿到 Ready 信息并处理。
Project 2c
论文提的挺少,代码框架里实现压缩和分块传输的逻辑也都不用写,所以实现起来很快,但调试起来遇到的问题就更多了。
在以下情况会需要产生 snapshot
- 当 leader 给 follower 发送日志时发现该日志已经被压缩时。
- 当一个新结点启动或需要恢复时。
因此 Raft 层驱动了 snapshot 的产生。
在 RaftWorker 的循环中,每次 Tick 时会对日志进行检查。在 RaftGCLogTick 中,若应用的日志条数 (applied - first
) 已经超过了RaftLogGcCountLimit
,则会产生一个CompactLogRequest
。
会直接调用proposeRaftCommand
来 propose 这个压缩日志的Admin
请求。
而 proposeRaftCommand 会调用 Raft 层的 Propose,将请求封成日志同步。
待日志提交后,可以在 handleRaftReady 中拿到它,对该 AdminRequest 进行处理、应用和响应。
应用时会更新写入 applyState
状态信息,调用ScheduleCompactLog
,向raftLogGCTaskSender
发送一个raftLogGCTask
,让其完成相应任务。
Project 3 MultiRaftKV
Project3 强依赖于 Project2 的实现,测试的环境也更加复杂,测试时间长,调试难度也加大了。
Project 3a
这部分只需要了解领导人变更和成员变更的概念,只在 Raft 层实现 leader transfer 和 confChange,因此还是较为容易的。
Project 3b
需要实现 2b 中未处理的AdminRequest
,因此依旧是 Propose 和 Apply 的过程。代码量不大,但调试最为麻烦,也会发现之前许多实现不当之处。
ConfChange
成员变更节点需要注意PendingConfIndex
的设置,它能保证同一时刻只有一条confChangeEntry
在执行,从而防止成员变更带来的错乱。
主要考虑一些特殊情况,例如移除的节点是自己时如何处理。
Split
注意在执行指令前,需要进行一系列严格的检查。
Project 3c
这一节需要实现上层调度器Scheduler
, 跟着文档来就不会有问题。
调度器阶段性地从各 region 接收心跳,以维护 region 的信息,这样才能做出有效的调度。同时,调度器会检查,在 store 中是否有过多的 region,尝试在心跳中回复转移命令等来均衡。
Project4 Transaction
本节实现代码不多,但需要了解很多新知识,否则会比较陌生。
如果实现了 TinySQL 应该会对其了解更深。
TinyKV 设计遵循 Percolator 论文,其核心为两阶段提交协议(2PC)。
建议先仔细阅读论文,再照着任务书实现。