MIT 6.824 Lab2D Raft之日志压缩
书接上文Raft Part C | MIT 6.824 Lab2C Persistence。
实验准备
- 实验代码:
git://g.csail.mit.edu/6.824-golabs-2021/src/raft - 如何测试:
go test -run 2D -race - 相关论文:Raft Extended Section 7
- 实验指导:6.824 Lab 2: Raft (mit.edu)
实验目标
实现Snapshot、CondInstallSnapshot、InstallSnapshot RPC,并修改之前的代码以支持本次实验的内容。
一些提示
- 不要使用论文中的偏移机制为数据分片,每个分片作为一个快照。而是每次RPC发送全部数据作为一个快照。
- 丢弃旧日志的全部引用,以便GC回收。
- 由于保存快照要丢弃部分日志,不能再使用日志长度来作为索引日志的标准。
- 考虑是否需要持久化
lastIncludeTerm和lastIncludeIndex。 - 使用
rf.persister.SaveStateAndSnapshot()持久化快照。
日志压缩
日志序列不断扩张,是无法全部存储在内存中的,对于已经应用到状态机的部分日志,就不再需要维护在Raft中。
但由于仍可能存在部分Follower的日志序列远远落后于Leader,因此这部分日志不能被Leader丢弃,在同步日志时,若Leader中原应被同步的日志在快照中,则将快照发送给Follower。
lastIncluedTerm & lastIncludeIndex
日志压缩后,Raft需要记录额外的两个信息,lastIncludeIndex、lastIncludeTerm表示快照中最后一个log的index和Term。
此处设计新的log类型如下。
type Log struct {
Entries []LogEntry
Base int
}
需要注意的是,Log.Entries从1开始存储,因此Log.Entries[0].Term用于存储lastIncludeTerm,Log.Base表示Log.Entries[0]的逻辑位置,也是lastIncludeIndex的值。
本例中,lastIncludeIndex = 4,lastIncludeTerm = 2,snapshot = [1,1,1,2]。
为Log添加相关成员函数。
func (l *Log) size() {
return l.Base + len(l.Entries)
}
func (l *Log) get(i int) {
return l.Entries[i-l.Base]
}
func (l *Log) set(i int, e LogEntry) {
l.[i-l.Base] = e
}
Snapshot()
Snapshot(index int, snapshot []byte)由状态机调用,传入的index表示lastIncludeIndex,snapshot由状态机生成,需要Raft保存,用于发送Follower时需要。
func (rf *Raft) Snapshot(index int, snapshot []byte) {
if index <= rf.log.Base {
return
}
rf.log.Entries = rf.log.Entries[index-rf.log.Base:]
rf.log.Base = index
rf.snapshot = snapshot
rf.saveStateAndSnapshot()
}
index <= rf.log.Base说明传入的snapshot是一个旧的快照。
InstallSnapshot RPC
首先是heartbeat()应该新增如下逻辑,当Leader中应被同步到Follower的日志在快照中时,将快照发送给Follower。
if next <= rf.log.Base {
go rf.sendSnapshot(i, peer, InstallSnapshotArgs{
Term: rf.currentTerm,
LastIncludeIndex: rf.log.Base,
LastIncludeTerm: rf.log.Entries[0].Term,
Data: rf.snapshot,
})
}
sendSnapshot()和发送日志序列类似。
func (rf *Raft) sendSnapshot(id int, peer *labrpc.ClientEnd, args InstallSnapshotArgs) {
reply := InstallSnapshotReply{}
ok := peer.Call("Raft.InstallSnapshot", &args, &reply)
if !ok {
return
}
if reply.Term > rf.currentTerm {
rf.toFollower(reply.Term)
return
}
rf.nextIndex[id] = args.LastIncludedIndex + 1
rf.matchIndex[id] = args.LastIncludedIndex
}
InstallSnapshot()和AppendEntries()类似,args.LastIncludedIndex <= rf.log.Base也是一样的,表示一个旧的快照。
func (rf *Raft) InstallSnapshot(args *InstallSnapshotArgs, reply *InstallSnapshotReply) {
rf.lastRecv = time.Now()
if args.Term > rf.currentTerm {
rf.toFollower(args.Term)
}
reply.Term = rf.currentTerm
if args.Term < rf.currentTerm || args.LastIncludedIndex <= rf.log.Base {
return
}
rf.applyCh <- ApplyMsg{
SnapshotValid: true,
Snapshot: args.Data,
SnapshotTerm: args.LastIncludedTerm,
SnapshotIndex: args.LastIncludedIndex,
}
}
注意:快照是状态机中的概念,需要在状态机中加载快照,因此要通过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为准,更安全。
func (rf *Raft) CondInstallSnapshot(lastIncludedTerm int, lastIncludedIndex int, snapshot []byte) bool {
if lastIncludedIndex <= rf.commitIndex {
return false
}
if lastIncludedIndex <= rf.log.size()-1 && rf.log.get(lastIncludedIndex).Term == lastIncludedTerm {
rf.log.Entries = append([]LogEntry(nil), rf.log.Entries[lastIncludedIndex-rf.log.Base:]...)
} else {
rf.log.Entries = append([]LogEntry(nil), LogEntry{Term: lastIncludedTerm})
}
rf.log.Base = lastIncludedIndex
rf.snapshot = snapshot
rf.commitIndex = lastIncludedIndex
rf.lastApplied = lastIncludedIndex
rf.saveStateAndSnapshot()
return true
}
需要注意的是,这里截断
rf.log.Entries的方式,如果使用s = s[i:]这样的方式,依然维持对底层数组全部元素的引用,是无法被GC回收的。
还有一点要注意的是,不要忘记在Make()中读取持久化的snapshot,并初始化lastApplied的值。
最后,为了证明我不是在乱写,附上我的测试结果。
MIT 6.824 Lab2D Raft之日志压缩的更多相关文章
- MIT 6.824 Llab2B Raft之日志复制
书接上文Raft Part A | MIT 6.824 Lab2A Leader Election. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021 ...
- MIT 6.824 Lab2C Raft之持久化
书接上文Raft Part B | MIT 6.824 Lab2B Log Replication. 实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021 ...
- MIT 6.824 Lab2A Raft之领导者选举
实验准备 实验代码:git://g.csail.mit.edu/6.824-golabs-2021/src/raft 如何测试:go test -run 2A -race 相关论文:Raft Exte ...
- Sqlserver2008日志压缩
SqlServer2008日志压缩语句如下: USE [master] GO ALTER DATABASE DBName SET RECOVERY SIMPLE WITH NO_WAIT GO ALT ...
- Raft 实现日志复制同步
Raft 实现日志复制同步 本篇文章以 John Ousterhout(斯坦福大学教授) 和 Diego Ongaro(斯坦福大学获得博士学位,Raft算法发明人) 在 Youtube 上的讲解视频及 ...
- 我是如何利用Hadoop做大规模日志压缩的
背景 刚毕业那几年有幸进入了当时非常热门的某社交网站,在数据平台部从事大数据开发相关的工作.从日志收集.存储.数据仓库建设.数据统计.数据展示都接触了一遍,比较早的赶上了大数据热这波浪潮.虽然今天的人 ...
- 图解Raft之日志复制
日志复制可以说是Raft集群的核心之一,保证了Raft数据的一致性,下面通过几张图片介绍Raft集群中日志复制的逻辑与流程: 在一个Raft集群中只有Leader节点能够接受客户端的请求,由Leade ...
- Shell + crontab 实现日志压缩归档
Shell + crontab 实现日志压缩归档 crontab # archive the ats log days. */ * * * * root /bin/>& shell #! ...
- MIT 6.824 lab1:mapreduce
这是 MIT 6.824 课程 lab1 的学习总结,记录我在学习过程中的收获和踩的坑. 我的实验环境是 windows 10,所以对lab的code 做了一些环境上的修改,如果你仅仅对code 感兴 ...
随机推荐
- [AcWing 796] 子矩阵的和
点击查看代码 #include<iostream> using namespace std; const int N = 1e3 + 10; int a[N][N], s[N][N]; i ...
- C++基础-1-内存管理(全局区、堆区、栈区)
1. 内存管理 1.1 全局区 1 #include<iostream> 2 using namespace std; 3 4 // 全局变量 5 int g_a = 10; 6 int ...
- 利用腾讯云函数部署.Net 5米游社原神每日签到功能
自从GitHub批量禁止滥用Action功能后,项目不得不考虑另外方案执行应用.其中腾讯云函数被大家作为不错的选择(虽然马上也要收费了). 但对于.Net的部署目前资源很少,而且我也没学过bash.在 ...
- 红旗 Linux 桌面操作系统11来了:支持国产自主CPU,全新UI风格设计,兼容面广...
链接:https://reurl.cc/g8ke9X 红旗Linux桌面操作系统11将于1月10日开放预览版的下载,新版本具有良好的硬件兼容,支持多款国产自主CPU品牌,同时还具有丰富的外设支持及海量 ...
- Linux进程总结
一个执着于技术的公众号 进程 进程,是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础.它的执行需要系统分配资源创建实体之后,才能进行.举个例子: ...
- css自定义省略实例2
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...
- Python-100-Days-master
跟着python100学习一下 100以内的素数 # 输出100以内的所有素数 # 想法:从1到100遍历,假设得到了i=17,那么此时从1到9遍历,如果找到了一个数用17能除尽则跳出循环 # 如果找 ...
- 渗透:dSploit
dSploit--开源的专业的Android平台安全管理工具包 只能在横屏模式下工作,即使你旋转你的设备也将继续保持横屏,如果你有一个应用程序,如旋转控制器,迫使每一个应用程序旋转,将导致dSploi ...
- 脚踏实地的Netty源码研究笔记——开篇
1. 脚踏实地的Netty源码研究笔记--开篇 1.1. Netty介绍 Netty是一个老牌的高性能网络框架.在众多开源框架中都有它的身影,比如:grpc.dubbo.seata等. 里面有着非常多 ...
- linux篇-图解cacti监控安装
1登录 admin admin 2点击devices localhost 3进入配置保存 4保存 http服务要启动哦 5一步步做 6graph tree 7执行/usr/bin/php /var/w ...