6.824 Lab2D

前置芝士

Task

完成 Raft 的快照功能(涉及到较多的与 Service 层的交互。

  • 为什么要有 snapshot?
    1. snapshot 可以将 log 压缩,比如将 10 个 log 压缩成 9 个(当两个 log 修改的 key 值一样时)。
    2. snapshot 可以减少 Raft 层 log 的长度,帮助进度较慢的 Raft 节点快速恢复状态机(减短 raft 中 log 的长度)。snapshot 区别于持久化 log,后者主要是不让宕机的 raft 丢失太多日志。
  • 在 2D 中,snapshot 会涉及到 raft 层与 service 层的多次交互,看这个 diagram of Raft interactions  或许可以帮助理解 Raft 协议不同层次的功能与特性。
  • snapshot 作用于每一个 Raft 节点,我们需要记录 snapshot 最后一个 index 和 term,用于一致性检查。

Step

  1. Snapshot() 被 service 层调用,约莫 10 次 commit 调用一次,用于保存快照。需要注意的是快照是 service 传给 raft 层的,而不是我们在 raft 层写入日志创建的,我们不需要创建快照,仅需要处理 Service 层传进来的 snapshot,进行日志截断和快照持久化。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (rf *Raft) Snapshot(index int, snapshot []byte) {
	rf.mu.Lock()
	defer rf.mu.Unlock()
	if rf.lastIncludedIndex() >= index {
		return
	}
	term := rf.logAt(index).Term
	rf.log = rf.log[index-rf.lastIncludedIndex():]
	rf.setLastIncludedIndex(index)
	rf.setLastIncludedTerm(term)
	rf.persister.SaveStateAndSnapshot(rf.getEncodeStateL(), snapshot)
	DPrintf(dSnap, "S%v last: %v", rf.me, index)
}

snapshot 2. InstallSnapshot:

  1. Leader 方面,当$nextIndex[Follower] \leq Leader.lastIncludeIndex$的时候,由于 Leader 已经没有这部分的日志,Leader 会发送 InstallSnapshot RPC 给 Follower,请求 Follower 安装快照(快速追赶),并且在成功收到返回值后,Leader 会更新 nextIndex 和 matchIndex

  2. Follower 方面,如果 Follower 收到 Leader 的任期没有过期的话,只能执行。与论文不同的是:不需要考虑分块发送 snapshot,所以offsetdone都不需要考虑,相应的,在实现的时候也不需要考虑第 2,3,4 点 implementation。最后另起一个 go routine 传 snapshot 以及其他必要参数给 applyCh 让 service 更新状态机(发送给 service 后 service 不会立刻拿 snapshot 更新状态机,会先调用 CondInstallsnapshot 来询问 Follower 在这期间有没有 commit,若没有,才会用 snapshot 更新状态机。

snapshotRPC

  • CondInstallSnapshot:若 $lastIncludedIndex\leq commitIndex$ ,则返回false,此时 service 不会应用 snapshot;否则剪短自己的 log,并更新 snapshot、lastIncludedTerm 和 lastIncludedIndex,并进行持久化。

tips:

  1. 在持久化的时候考虑 lastIncludedIndex 和 lastIncludedTerm,并应用到 commitIndex 和 lastApplied。

  2. 因为我们要删掉已经被 snapshot 的 log,所以需要改变 log 的索引方式。

  3. 对 condInstallsnapshot 的解释:Follower 收到 InstallSnapshot,向服务器请求应用 snapshot 的时候,如果服务器发送 condInstallSnapshot 的那一刻 lastCommit 还没被修改的话,就保证了原子操作。为什么需要保证原子操作?因为节点通过 applychan 向 service 通信的情况有两种,一种是 commit 到状态机,另一种是 snapshot 到状态机,两者是并行的,互不干扰。只要 raft 节点在发送 snapshot channel 到 service 接收到这个 channel 之间有 commit channel,也就是当 $lastIncludedIndex\leq commitIndex$ 的时候,snapshot 发送的 log 就很有可能被应用到状态机了。

updatedupdated2024-04-302024-04-30