Raft日志复制

本文详细阐述了Raft协议中的LogEntry组成,包括索引、任期号和命令。重点讲解了日志复制过程以及如何通过AppendEntries消息确保一致性,强调了索引和任期号在保证数据安全性和正确性中的关键作用。

Raft中的Log Entry包括以下内容
索引 Index 记录该日志条目在整个日志中的位置
任期号 Term 日志条目首次被领导者创建时的任期
命令 应用于状态机的命令

日志复制

Raft算法通过索引和任期号唯一标识一条日志记录

日志必须持久化存储,一个节点必须先把日志条目安全写入到磁盘中,才能向系统其他节点发送请求或回复请求。
如果一条日志条目被存储到超过半数节点上,则认为该记录已提交(committed)。如果一条记录已提交,状态机可以安全地执行该记录,这条记录就不能再改变了。
如图,第1条到第7条日志已提交,第8条未提交。
提交日志
Raft通过AppendEntries消息复制日志,与心跳消息共用同一个RPC,不过用于心跳时不附加日志信息。
Raft算法正常运行时,日志复制流程为:
(1) 客户端向领导者发送命令
(2) 领导者先将命令追加到自己的日志中,确保日志持久化存储
(3) 领导者向其他节点发送AppendEntries消息,等待相应
(4) 如果收到超过半数节点响应,则认为新的日志记录已提交。接着领导者将命令应用(apply)到自己的状态机,然后向客户端返回响应。此外,一旦领导者提交了一个日志记录,将在后续的AppendEntries消息中通过LeaderCommit参数通知跟随者,告知领导者已提交的最大日志索引,跟随者也提交索引小于LeaderCommit的日志,并将命令应用到自己的状态机。
(5) 如果跟随者宕机或请求超时,日志没有复制成功,那么领导者将反复尝试发送AppendEntries消息。
(6) 性能优化:领导者不必等待所有跟随者做出响应,只要半数的跟随者成功响应(日志已经存储到超过半数的节点上)就可以回复客户端了。这样保证即使存在一个很慢的或故障节点,也不会成为系统的瓶颈。

Raft算法的日志通过索引和任期号唯一表示一个日志条目,为了保证安全性,Raft维持以下两个特性:
(1) 如果两个节点的日志在相同的索引位置上的任期号相同,则认为它们具有一样的命令,并且从日志开头到这个索引位置之间的日志也完全相同
(2) 如果给定的日志已提交,那么之前所有的日志都已提交 .
在这里插入图片描述

一致性保证

Raft通过AppendEntries消息来检测之前的一个日志条目,每个AppendEntries请求包含新的日志条目之前的一个日志条目的索引和任期(记为prevLogIndex和prevLogTerm),追随者收到请求后,会检查自己最后一条日志的索引和任期号是否与请求消息中的prevLogIndex和prevLogTerm相同,相同则接收记录,否则拒绝
可以通过数学归纳法证明以上方式能保证AppendEntries消息之前的日志都是相同的。

代码框架

package go_raft

type LogEntry struct {
	Index   int
	Term    int
	Command interface{}
}

type AppendEntriesArgs struct {
	Term         int
	LeaderId     int
	PrevLogIndex int
	PrevLogTerm  int
	//需要复制的日志条目,发送心跳时为空
	Entries []LogEntry
	//领导者已提交最大日志索引,用于跟随着提交
	LeaderCommit int
}

type AppendEntriesReply struct {
	Term    int
	Success bool
}

func (rf *Raft) AppendEntries(args *AppendEntriesArgs, reply *AppendEntriesReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()

	reply.Term = rf.currentTerm
	reply.Success = false
	if args.Term < rf.currentTerm {
		return
	}

	if args.Term > rf.currentTerm {
		reply.Term = args.Term
		rf.currentTerm = args.Term
	}

	//重置选举超时时间
	rf.setState(Follower)

	//日志一致性检查
	lastLogIndex := len(rf.log) - 1
	if args.PrevLogIndex > rf.log[lastLogIndex].Index || rf.log[args.PrevLogIndex].Term != args.PrevLogTerm {
		return
	}

	reply.Success = true

	//处理重复的RPC请求
	//比较日志条目的任期,以确认是否能够安全地追加日志
	//否则会导致重复应用命令
	index := args.PrevLogIndex
	for i, entry := range args.Entries {
		index++
		if index < len(rf.log) {
			if rf.log[index].Term == entry.Term {
				continue
			}
			rf.log = rf.log[:index]
		}

		rf.log = append(rf.log, args.Entries[i:]...)
		break
	}

	if rf.commitIndex < args.LeaderCommit {
		lastLogIndex = rf.log[len(rf.log)-1].Index
		if args.LeaderCommit > lastLogIndex {
			rf.commitIndex = lastLogIndex
		} else {
			rf.commitIndex = args.LeaderCommit
		}
		//将命令应用到自己的状态机
		rf.apply()
	}

	//保险起见,再重置一次选举超时时间
	rf.setState(Follower)
	return
}

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值