6.584-Lab4B

6.584-Lab4B

Lab4B:当底层的 Raft 中的日志 log 过大时,上层的 K/V Server 会生成快照SnapShot,并将底层 Raft 的 log 中已经被 K/V Server生成快照的部分截断,从而减短底层 Raft 的 log 的长度,减少内存压力。
本次文章旨在记录 参考Reference Blog实现相关代码过程中的理解,具体代码请参考上面的 【Reference Code】。

K/V Server 如何控制 Raft 生成快照

func (kv *KVServer) GenSnapShot() []byte {
	w := new(bytes.Buffer)
	e := labgob.NewEncoder(w)

	e.Encode(kv.db)
	e.Encode(kv.historyMap)
	serverState := w.Bytes()
	return serverState
}

// 加载 K/Vserver 的快照(加锁使用)
func (kv *KVServer) LoadSnapShot(snapshot []byte) {
	if snapshot == nil || len(snapshot) == 0 {
		return
	}

	r := bytes.NewBuffer(snapshot)
	d := labgob.NewDecoder(r)

	var db map[string]string
	var historyMap map[int64]*result
	if d.Decode(&db) != nil || d.Decode(&historyMap) != nil {
		DPrintf("server %v LoadSnapShot 加载快照失败\n", kv.me)
	} else {
		kv.db = db
		kv.historyMap = historyMap
	}
}

这是 K/V Server 实现的生成快照GeSnapShot 和 加载快照LoadSnapShot函数。
GeSnapShot:将 K/V Server 应用 Apply 过命令的本地数据库 db 和历史记录进行编码,持久化保存。
LoadSnapShot:将传入的快照 snapshot 进行解码然后取出快照中的 db 和 历史记录。

// 每收到一个log,判断是否需要生成快照
			if kv.maxraftstate != -1 && kv.persister.RaftStateSize() >= kv.maxraftstate/100*95 {
				// 当达到95%容量时生成快照
				snapShot := kv.GenSnapShot()
				kv.rf.Snapshot(log.CommandIndex, snapShot)
			}

在 kvraft/server.go 的函数ApplyHandler中有触发生成快照的条件:当底层 Raft 的 State 的大小超过 阈值kv.maxraftstate的95%时生成快照。博主香草美人这样设置的原因是:K/V Server 会处理底层 Raft 通过通道kv.applych传来的绵绵不断的log,而且 RPC 会有一定的延迟,所以用kv.maxraftstate * 95%作为新的阈值。
K/V Server 将生成的快照和快照最后一条 log 的下标传入函数kv.rf.Snapshot
rf.Snapshot

func (rf *Raft) Snapshot(index int, snapshot []byte) {
	// Your code here (2D).
	rf.mu.Lock()
	defer rf.mu.Unlock()

	if rf.commitIndex < index || index <= rf.lastIncludedIndex {
		DPrintf("server %v 拒绝了 Snapshot 请求, 其index=%v, 自身commitIndex=%v, lastIncludedIndex=%v\n", rf.me, index, rf.commitIndex, rf.lastIncludedIndex)
		return
	}

	DPrintf("server %v 同意了 Snapshot 请求, 其index=%v, 自身commitIndex=%v, 原来的lastIncludedIndex=%v, 快照后的lastIncludedIndex=%v\n", rf.me, index, rf.commitIndex, rf.lastIncludedIndex, index)

	// 保存snapshot
	rf.snapShot = snapshot

	rf.lastIncludedTerm = rf.log[rf.RealLogIdx(index)].Term
	// 截断log
	rf.log = rf.log[rf.RealLogIdx(index):] // index位置的log被存在0索引处
	rf.lastIncludedIndex = index
	if rf.lastApplied < index {
		rf.lastApplied = index
	}

	rf.persist()
}

func (rf *Raft) persist() {
	// TODO: 持久化lastIncludedIndex和lastIncludedTerm时, 是否需要加锁?
	// DPrintf("server %v 开始持久化, 最后一个持久化的log为: %v:%v", rf.me, len(rf.log)-1, rf.log[len(rf.log)-1].Cmd)

	w := new(bytes.Buffer)
	e := labgob.NewEncoder(w)
	// 2C
	e.Encode(rf.votedFor)
	e.Encode(rf.currentTerm)
	e.Encode(rf.log)
	// 2D
	e.Encode(rf.lastIncludedIndex)
	e.Encode(rf.lastIncludedTerm)
	raftstate := w.Bytes()

	rf.persister.Save(raftstate, rf.snapShot)
}

kvraft/persister.go

type Persister struct {
	mu        sync.Mutex
	raftstate []byte
	snapshot  []byte
}
func (ps *Persister) Save(raftstate []byte, snapshot []byte) {
	ps.mu.Lock()
	defer ps.mu.Unlock()
	ps.raftstate = clone(raftstate)
	ps.snapshot = clone(snapshot)
}

可以看到底层 Raft 会根据传入的快照的下标去截断自己的 log,并将 K/V Server 传入的快照进行持久化保存。其中 K/V Server 传入的快照 snapshot 持久化在 ps.snapshot;底层 Raft 的一些需要持久化的状态经过编码后持久化在ps.raftstate
所以 ps.snapshot 中保存的一直都是 K/V Server 的db & historyMap

快照生成&流向kv.raft.statesize >= 0.95*kv.maxraftstate(生成快照条件)–> K/V Server 将db & historyMap生成快照传入rf.Snapshot --> 底层 Raft 根据传来快照的下标截断自己的 log,并将自己的一些状态和传来的 K/V Server的快照 分别持久化在本地的 ps.raftstateps.snapshot.

K/V Server 如何从 Raft 加载快照

kvraft/server.go:

func (kv *KVServer) ApplyHandler() {
	for !kv.killed() {
		log := <-kv.applyCh // Raft层处理完负责的部分(选举、生成日志、Snapshot等),Raft将提交的Cmd通过通道应用到K/V的db(数据库)
		if log.CommandValid {
			...
		} else if log.SnapshotValid {
			// 传过来的log中包含的不是一个cmd命令而是是个 快照snapshot
			kv.mu.Lock()
			if log.SnapshotIndex >= kv.lastApplied {
				kv.LoadSnapShot(log.Snapshot)
				kv.lastApplied = log.SnapshotIndex
			}
			kv.mu.Unlock()
		}
	}
}

能够看到,K/V Server在判断到从通道中拿到的是快照之后,会加载快照到 K/V Server。
现在去看一下什么情况下 K/V Server 拿到的是快照、什么情况下从通道中拿到的是包含 Cmd 的 log。
raft/raft.go:

func (rf *Raft) CommitChecker() {
	// 检查是否有新的commit
	for !rf.killed() {
		...
		for rf.commitIndex > tmpApplied {
			...
			msg := &ApplyMsg{
				CommandValid: true,
				Command:      rf.log[rf.RealLogIdx(tmpApplied)].Cmd,
				CommandIndex: tmpApplied,
				SnapshotTerm: rf.log[rf.RealLogIdx(tmpApplied)].Term,
			}
			msgBuf = append(msgBuf, msg)
		}
		for _, msg := range msgBuf {
			...
			rf.applyCh <- *msg
			...
		}
	}
}

在底层 Raft 检查到有 Commit 的 log 并没有 Apply 的话就会向通道发送一个包含 Cmd 的 log,去让上层的 K/V Server 接收到 Cmd 从而应用到 db。

raft/raft.go:

func (rf *Raft) InstallSnapshot(args *InstallSnapshotArgs, reply *InstallSnapshotReply) {
	rf.mu.Lock()
	defer rf.mu.Unlock()
	...
	msg := &ApplyMsg{
		SnapshotValid: true,
		Snapshot:      args.Data,
		SnapshotTerm:  args.LastIncludedTerm,
		SnapshotIndex: args.LastIncludedIndex,
	}
	...
	rf.applyCh <- *msg
	rf.persist()
}

此时底层 Raft 向通道中发送的就是一个快照,K/V Server接收到快照后会用来更新自己的db & historyMap

快照加载&流向:底层 Raft 中 Leader 检测到 Follower 缺失的 log 在快照中,会通过“心跳”向 Follower 发送快照 – > Follower 接收到 Leader 发来的快照后更新一下自己的字段,然后同时将接收到的快照通过通道向上发送给自己的 K/V Server --> Follower 的K/V Server 接收到发来的log(msg),通过log.SnapshotValid字段发现是一个快照,那么就加载这个快照中的db & historyMap更新自己。

修改之前lab中的代码

博主香草美人在 blog 中也修改了部分之前的代码,主要由一下部分:
Raft 层

  • 修复过多的 AppendEntries RPC
  • 修复过多的 InstallSnapshot RPC

K/V Server 层

  • 调用 Start 前过滤 Clerk 发送的重复的指令,而不是一股脑交给 Raft 层处理,然后通过返回的结果判断是否之前已经处理过。
  • 由于调用 Raft 层的 GetState 获得 Raft 层的大锁,增加 Raft 层对锁的争用,减少 Raft 层 GetState 的调用
  • 为了避免 Clerk 因网络等原因立即重发的 RPC 由会阻塞而超时导致再次重发 RPC,会在网络中积攒较多的 RPC,让 Clerk 先 sleep 再重试

具体修改位置和修改原因,请参考 Reference Blog

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值