MIT 6.824 Lab2B Raft
一、前期准备
实验地址introduction部分有一些资料可以学习一下
课堂翻译强烈推荐(里面还有相应论文),虽然可能没有上课听老师讲那么好,但是对于英语渣渣的我还是非常有用的。
代码主要分为测试部分和编写部分,可以先去大致看一下config和test文件中的内容,毕竟程序准确性依赖测试文件,同时也能知道实验各个部分的运行过程。
论文中已经把算法基本上都已经实现好了,先把需求和代码看清楚再下手(课上老师好像也这么说了)
二、实验概述
该部分的功能除了实现心跳之外,还得加上日志的传递,同步日志(网络故障或者crash),以及日志的提交。
主体结构在前面一节也有提到,这个实验多的一个数据结构就是日志。
type LogEntry struct {
//日志索引
LogIndex int
//该命令对应的领导人任期号
LogTerm int
//可以执行的命令
LogCommand interface{}
}
并且rf.log[0]我们将他视为哨兵,保存快照节点信息,目前还用不到。
三、函数伪代码
3.1 广播请求函数
func (rf *Raft) broadcastAppendEntries() {
rf.mu.Lock()
defer rf.mu.Unlock()
last := rf.getLastIndex()
//采用批量提交 查看当前能够提交的command数量 通知线程提交 并且因为raft的一致性原则 只要有一个i 在该条件下2*num > len(rf.peers)不满足 后面的i一定也不满足 所以这里没有加break语句
for i := rf.commitIndex + 1; i <= last; i++ {
//判断该日志是否可以提交
}
// 有新日志提交 通知
//发送 有日志发日志,没有日志发送心跳,我们用nextIndex[]来保存当前需要发送到当前follower中的日志下表,matchIndex[]表示该follower当前与leader同步的日志下标
for i := range rf.peers {
if i != rf.me && rf.state == LEADER {
var args AppendEntriesArgs
//填充数据
go func(i int, args AppendEntriesArgs) {
var reply AppendEntriesReply
rf.sendAppendEntries(i, args, &reply)
}(i, args)
}
}
}
3.2 发送函数
func (rf *Raft) sendAppendEntries(server int, args AppendEntriesArgs, reply *AppendEntriesReply) bool {
ok := rf.peers[server].Call("Raft.AppendEntries", args, reply)
rf.mu.Lock()
defer rf.mu.Unlock()
if ok {
//如果当前状态变更 说明reply是没用的,直接返回(比如角色改变或者任期改变)
//如果reply中发返回的任期比自己的大,说明自己已经不是leader了
//返回reply.Success
//根据返回值更新nextIndex[] matchIndex[]
}
return ok
}
3.3 对端处理函数
// leader节点进行日志同步 raft强制要求 leader节点 覆盖到其他server节点来保持一致性
func (rf *Raft) AppendEntries(args AppendEntriesArgs, reply *AppendEntriesReply) {
rf.mu.Lock()
defer rf.mu.Unlock()
defer rf.persist()
//如果该leader任期 小于当前server节点的任期,则返回当前节点最新的索引(不认可该leader)
//收到leader 的心跳 重置定时器恢复正常
rf.chanHeartbeat <- true
//判断前一条日志是否匹配,课上老师说不匹配不能直接返回前一个,这样做效率太低会超时,这里我们直接一个for循环直接遍历,返回第一个不匹配位置。
//追加日志 然会成功消息
//因为args中还携带了leadercommindex,根据该信息,查看自己是否有command可以提交
return
}
3.4 处理提交
本方案大部分时间都是直接使用一把大锁,但是这边把锁拆开来了,原因后面再说,baseIndex实在快照中使用的。
go func() {
for rf.killed() == false {
select {
case <-rf.chanCommit:
rf.mu.Lock()
commitIndex := rf.commitIndex
baseIndex := rf.log[0].LogIndex
msgss := make([]ApplyMsg, 0)
for i := rf.lastApplied + 1; i <= commitIndex; i++ {
msg := ApplyMsg{CommandIndex: i, Command: rf.log[i-baseIndex].LogCommand, CommandValid: true, SnapshotValid: false, Snapshot: rf.persister.ReadSnapshot()}
msgss = append(msgss, msg)
}
rf.mu.Unlock()
for _, msg := range msgss {
rf.chanApply <- msg
rf.mu.Lock()
rf.lastApplied = msg.CommandIndex
rf.mu.Unlock()
}
}
}
}()
4.算法缺陷
4.1 缺陷1:因为client提交的指令每次都是和心跳一起发送,这就导致了client提交的指令不是实时的,会造成较大的延迟。
解决方案:多开一个线程,进行指令发送
4.2 缺陷2:可能出现极端的情况,导致单向的网络出现故障,进而使得Raft系统不能工作?
Robert教授:我认为是有可能的。例如,如果当前Leader的网络单边出现故障,Leader可以发出心跳,但是又不能收到任何客户端请求。它发出的心跳被送达了,因为它的出方向网络是正常的,那么它的心跳会抑制其他服务器开始一次新的选举。但是它的入方向网络是故障的,这会阻止它接收或者执行任何客户端请求。这个场景是Raft并没有考虑的众多极端的网络故障场景之一。
我认为这个问题是可修复的。我们可以通过一个双向的心跳来解决这里的问题。在这个双向的心跳中,Leader发出心跳,但是这时Followers需要以某种形式响应这个心跳。如果Leader一段时间没有收到自己发出心跳的响应,Leader会决定卸任,这样我认为可以解决这个特定的问题和一些其他的问题。
你是对的,网络中可能发生非常奇怪的事情,而Raft协议没有考虑到这些场景。