书接上文Raft Part C | MIT 6.824 Lab2C Persistence

实验准备

  1. 实验代码:git://g.csail.mit.edu/6.824-golabs-2021/src/raft
  2. 如何测试:go test -run 2D -race
  3. 相关论文:Raft Extended Section 7
  4. 实验指导:6.824 Lab 2: Raft (mit.edu)

实验目标

实现SnapshotCondInstallSnapshotInstallSnapshot RPC,并修改之前的代码以支持本次实验的内容。

一些提示

  1. 不要使用论文中的偏移机制为数据分片,每个分片作为一个快照。而是每次RPC发送全部数据作为一个快照。
  2. 丢弃旧日志的全部引用,以便GC回收。
  3. 由于保存快照要丢弃部分日志,不能再使用日志长度来作为索引日志的标准。
  4. 考虑是否需要持久化lastIncludeTermlastIncludeIndex
  5. 使用rf.persister.SaveStateAndSnapshot()持久化快照。

日志压缩

日志序列不断扩张,是无法全部存储在内存中的,对于已经应用到状态机的部分日志,就不再需要维护在Raft中。

但由于仍可能存在部分Follower的日志序列远远落后于Leader,因此这部分日志不能被Leader丢弃,在同步日志时,若Leader中原应被同步的日志在快照中,则将快照发送给Follower。

lastIncluedTerm & lastIncludeIndex

日志压缩后,Raft需要记录额外的两个信息,lastIncludeIndexlastIncludeTerm表示快照中最后一个log的index和Term。

此处设计新的log类型如下。

  1. type Log struct {
  2. Entries []LogEntry
  3. Base int
  4. }

需要注意的是,Log.Entries从1开始存储,因此Log.Entries[0].Term用于存储lastIncludeTermLog.Base表示Log.Entries[0]的逻辑位置,也是lastIncludeIndex的值。

本例中,lastIncludeIndex = 4,lastIncludeTerm = 2,snapshot = [1,1,1,2]。

为Log添加相关成员函数。

  1. func (l *Log) size() {
  2. return l.Base + len(l.Entries)
  3. }
  4. func (l *Log) get(i int) {
  5. return l.Entries[i-l.Base]
  6. }
  7. func (l *Log) set(i int, e LogEntry) {
  8. l.[i-l.Base] = e
  9. }

Snapshot()

Snapshot(index int, snapshot []byte)由状态机调用,传入的index表示lastIncludeIndexsnapshot由状态机生成,需要Raft保存,用于发送Follower时需要。

  1. func (rf *Raft) Snapshot(index int, snapshot []byte) {
  2. if index <= rf.log.Base {
  3. return
  4. }
  5. rf.log.Entries = rf.log.Entries[index-rf.log.Base:]
  6. rf.log.Base = index
  7. rf.snapshot = snapshot
  8. rf.saveStateAndSnapshot()
  9. }

index <= rf.log.Base说明传入的snapshot是一个旧的快照。

InstallSnapshot RPC

首先是heartbeat()应该新增如下逻辑,当Leader中应被同步到Follower的日志在快照中时,将快照发送给Follower。

  1. if next <= rf.log.Base {
  2. go rf.sendSnapshot(i, peer, InstallSnapshotArgs{
  3. Term: rf.currentTerm,
  4. LastIncludeIndex: rf.log.Base,
  5. LastIncludeTerm: rf.log.Entries[0].Term,
  6. Data: rf.snapshot,
  7. })
  8. }

sendSnapshot()和发送日志序列类似。

  1. func (rf *Raft) sendSnapshot(id int, peer *labrpc.ClientEnd, args InstallSnapshotArgs) {
  2. reply := InstallSnapshotReply{}
  3. ok := peer.Call("Raft.InstallSnapshot", &args, &reply)
  4. if !ok {
  5. return
  6. }
  7. if reply.Term > rf.currentTerm {
  8. rf.toFollower(reply.Term)
  9. return
  10. }
  11. rf.nextIndex[id] = args.LastIncludedIndex + 1
  12. rf.matchIndex[id] = args.LastIncludedIndex
  13. }

InstallSnapshot()AppendEntries()类似,args.LastIncludedIndex <= rf.log.Base也是一样的,表示一个旧的快照。

  1. func (rf *Raft) InstallSnapshot(args *InstallSnapshotArgs, reply *InstallSnapshotReply) {
  2. rf.lastRecv = time.Now()
  3. if args.Term > rf.currentTerm {
  4. rf.toFollower(args.Term)
  5. }
  6. reply.Term = rf.currentTerm
  7. if args.Term < rf.currentTerm || args.LastIncludedIndex <= rf.log.Base {
  8. return
  9. }
  10. rf.applyCh <- ApplyMsg{
  11. SnapshotValid: true,
  12. Snapshot: args.Data,
  13. SnapshotTerm: args.LastIncludedTerm,
  14. SnapshotIndex: args.LastIncludedIndex,
  15. }
  16. }

注意:快照是状态机中的概念,需要在状态机中加载快照,因此要通过applyCh将快照发送给状态机,但是发送后Raft并不立即保存快照,而是等待状态机调用CondInstallSnapshot(),如果从收到InstallSnapshot()后到收到CondInstallSnapshot()前,没有新的日志提交到状态机,则Raft返回True,Raft和状态机保存快照,否则Raft返回False,两者都不保存快照。

如此保证了Raft和状态机保存快照是一个原子操作。当然在InstallSnapshot()将快照发送给状态机后再将快照保存到Raft,令CondInstallSnap()永远返回True,也可以保证原子操作,但是这样做必须等待快照发送给状态机完成,但是rf.applyCh <- ApplyMsg是有可能阻塞的,由于InstallSnapshot()需要持有全局的互斥锁,这可能导致整个节点无法工作。

为什么要保证原子操作?因为负责将commit状态的日志提交到状态机的goroutine不负责快照部分,因此必须是先保存快照,再同步日志。

本系列文章给出的代码为了好读,没有考虑同步问题,正常来讲applyCh <- ApplyMsg这个操作是需要令起一个goroutine去做的。

如何判断InstallSnapshot()CondInstallSnapshot()之间没有新的日志提交到状态机呢?这里使用commitIndex来判断,当lastIncludeIndex <= commitIndex时,说明这期间原本没有的快照部分的日志补全了,虽然commit状态并不一定是apply状态,但这里以commit为准,更安全。

  1. func (rf *Raft) CondInstallSnapshot(lastIncludedTerm int, lastIncludedIndex int, snapshot []byte) bool {
  2. if lastIncludedIndex <= rf.commitIndex {
  3. return false
  4. }
  5. if lastIncludedIndex <= rf.log.size()-1 && rf.log.get(lastIncludedIndex).Term == lastIncludedTerm {
  6. rf.log.Entries = append([]LogEntry(nil), rf.log.Entries[lastIncludedIndex-rf.log.Base:]...)
  7. } else {
  8. rf.log.Entries = append([]LogEntry(nil), LogEntry{Term: lastIncludedTerm})
  9. }
  10. rf.log.Base = lastIncludedIndex
  11. rf.snapshot = snapshot
  12. rf.commitIndex = lastIncludedIndex
  13. rf.lastApplied = lastIncludedIndex
  14. rf.saveStateAndSnapshot()
  15. return true
  16. }

需要注意的是,这里截断rf.log.Entries的方式,如果使用s = s[i:]这样的方式,依然维持对底层数组全部元素的引用,是无法被GC回收的。

还有一点要注意的是,不要忘记在Make()中读取持久化的snapshot,并初始化lastApplied的值。

最后,为了证明我不是在乱写,附上我的测试结果。

MIT 6.824 Lab2D Raft之日志压缩的更多相关文章

  1. MIT 6.824 Llab2B Raft之日志复制

    书接上文Raft Part A | MIT 6.824 Lab2A Leader Election. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021 ...

  2. MIT 6.824 Lab2C Raft之持久化

    书接上文Raft Part B | MIT 6.824 Lab2B Log Replication. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021 ...

  3. MIT 6.824 Lab2A Raft之领导者选举

    实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021/src/raft 如何测试:go test -run 2A -race 相关论文:Raft Exte ...

  4. Sqlserver2008日志压缩

    SqlServer2008日志压缩语句如下: USE [master] GO ALTER DATABASE DBName SET RECOVERY SIMPLE WITH NO_WAIT GO ALT ...

  5. Raft 实现日志复制同步

    Raft 实现日志复制同步 本篇文章以 John Ousterhout(斯坦福大学教授) 和 Diego Ongaro(斯坦福大学获得博士学位,Raft算法发明人) 在 Youtube 上的讲解视频及 ...

  6. 我是如何利用Hadoop做大规模日志压缩的

    背景 刚毕业那几年有幸进入了当时非常热门的某社交网站,在数据平台部从事大数据开发相关的工作.从日志收集.存储.数据仓库建设.数据统计.数据展示都接触了一遍,比较早的赶上了大数据热这波浪潮.虽然今天的人 ...

  7. 图解Raft之日志复制

    日志复制可以说是Raft集群的核心之一,保证了Raft数据的一致性,下面通过几张图片介绍Raft集群中日志复制的逻辑与流程: 在一个Raft集群中只有Leader节点能够接受客户端的请求,由Leade ...

  8. Shell + crontab 实现日志压缩归档

    Shell + crontab 实现日志压缩归档 crontab # archive the ats log days. */ * * * * root /bin/>& shell #! ...

  9. MIT 6.824 lab1:mapreduce

    这是 MIT 6.824 课程 lab1 的学习总结,记录我在学习过程中的收获和踩的坑. 我的实验环境是 windows 10,所以对lab的code 做了一些环境上的修改,如果你仅仅对code 感兴 ...

随机推荐

  1. 团队Arpha5

    队名:观光队 组长博客 作业博客 组员实践情况 王耀鑫 **过去两天完成了哪些任务 ** 文字/口头描述 完成服务器连接数据库部分代码 展示GitHub当日代码/文档签入记录 接下来的计划 服务器网络 ...

  2. ZABBIX新功能系列1-使用Webhook将告警主动推送至第三方系统

    Zabbix5以来的新版本与以前的版本除UI界面变化较大外,在很多功能上也有许多亮点,我这里计划安排1个系列来和大家交流一些新功能的使用,这是第一篇:使用Webhook将告警主动推送至第三方系统. 首 ...

  3. Web安全学习笔记 SQL注入中

    Web安全学习笔记 SQL注入中 繁枝插云欣 --ICML8 权限提升 数据库检测 绕过技巧 一.权限提升 1. UDF提权 UDF User Defined Function,用户自定义函数 是My ...

  4. mysql外键,锁

    mysql外键: 场景:用于建立两个表之间的联系,让A表中一个字段,可以在另一个表中字段值的范围去查找 注意事项: (1)被参照表和参照表字段属性必须一致 (2)参照表必须设置主键 (3)必须选择支持 ...

  5. 【算法】基数排序(Radix Sort)(十)

    基数排序(Radix Sort) 基数排序是按照低位先排序,然后收集:再按照高位排序,然后再收集:依次类推,直到最高位.有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序.最后的次序就 ...

  6. Nexus5x 修改Android开机动画

    1.制作帧动画 这里随便从网上找了一个gif图片,导入PS中,打开后会形成很多帧图层,选择导航栏中的文件->脚本->将图层导出到文件可以将所有图层导出来.要注意文件命名,Android会按 ...

  7. MUI+html5+script 不同页面间转跳(九宫格)

    在点击图片/标题需要跳转到详情页面的使用场景中,首先定义图片元素的id为"tyzc",是同一类下的第一个图片 <img src="img/img3.png" ...

  8. 如何在 pyqt 中捕获并处理 Alt+F4 快捷键

    前言 如果在 Windows 系统的任意一个窗口中按下 Alt+F4,默认行为是关闭窗口(或者最小化到托盘).对于使用了亚克力效果的窗口,使用 Alt+F4 最小化到托盘,再次弹出窗口的时候可能出现亚 ...

  9. MyBatis - MyBatis的层次结构

    API接口层 规定了一系列接口,能够向外提供接口,对内进行操作. 数据处理层 负责SQL相关处理工作,如:SQL查找.SQL执行.SQL映射等工作. 基础支撑层 提供基础功能支撑,包括连接管理.事务管 ...

  10. 文字轮播与图片轮播?CSS 不在话下

    今天,分享一个实际业务中能够用得上的动画技巧. 巧用逐帧动画,配合补间动画实现一个无限循环的轮播效果,像是这样: 看到上述示意图,有同学不禁会发问,这不是个非常简单的位移动画么? 我们来简单分析分析, ...