枯木逢春不在茂
年少且惜镜边人
写在前面
lab3B需要实现一个快照功能,这需要对raft的代码需要进行更改,比如在发送心跳的时候,如果nextindex是快照里面的内容则就需要发送快照给peer,持久化也要进行修改,算是对raft加整个系统进行了大优化吧,要独立完成这个任务,很难(对我来说)。所以还是参考了大佬的代码。
实现过程
首先在Raft层引入了LastIncludedIndex和LastIncludedTerm两个持久化状态
kv层需要开启快照发送loop,这里注意判断raft log长度时不要加KV锁,否则会出现Raft和KV层死锁
func (kv *KVServer) snapshotLoop() {
for !kv.killed() {
var snapshot []byte
var lastIncludedIndex int
// 锁内dump snapshot
func() {
// 如果raft log超过了maxraftstate大小,那么对kvStore快照下来
if kv.maxraftstate != -1 && kv.rf.ExceedLogSize(kv.maxraftstate) {
// 这里调用ExceedLogSize不要加kv锁,否则会死锁
// 锁内快照,离开锁通知raft处理
kv.mu.Lock()
w := new(bytes.Buffer)
e := labgob.NewEncoder(w)
e.Encode(kv.kvStore) // kv键值对
e.Encode(kv.seqMap) // 当前各客户端最大请求编号,也要随着snapshot走
snapshot = w.Bytes()
lastIncludedIndex = kv.lastAppliedIndex
DPrintf("KVServer[%d] KVServer dump snapshot, snapshotSize[%d] lastAppliedIndex[%d]", kv.me, len(snapshot), kv.lastAppliedIndex)
kv.mu.Unlock()
}
}()
// 锁外通知raft层截断,否则有死锁
if snapshot != nil {
// 通知raft落地snapshot并截断日志(都是已提交的日志,不会因为主从切换截断,放心操作)
kv.rf.TakeSnapshot(snapshot, lastIncludedIndex)
}
time.Sleep(10 * time.Millisecond)
}
}
sever层检测raft日志长度,超过日志时,则直接打包快照,下降到让raft进行截断。
// 保存snapshot,截断log
func (rf *Raft) TakeSnapshot(snapshot []byte, lastIncludedIndex int) {
rf.mu.Lock()
defer rf.mu.Unlock()
// 已经有更大index的snapshot了
if lastIncludedIndex <= rf.lastIncludedIndex {
return
}
// 快照的当前元信息
DPrintf("RafeNode[%d] TakeSnapshot begins, IsLeader[%v] snapshotLastIndex[%d] lastIncludedIndex[%d] lastIncludedTerm[%d]",
rf.me, rf.leaderId==rf.me, lastIncludedIndex, rf.lastIncludedIndex, rf.lastIncludedTerm)
// 要压缩的日志长度
compactLogLen := lastIncludedIndex - rf.lastIncludedIndex
// 更新快照元信息
rf.lastIncludedTerm = rf.log[rf.index2LogPos(lastIncludedIndex)].Term
rf.lastIncludedIndex = lastIncludedIndex
// 压缩日志
afterLog := make([]LogEntry, len(rf.log) - compactLogLen)
copy(afterLog, rf.log[compactLogLen:])
rf.log = afterLog
// 把snapshot和raftstate持久化
rf.persister.SaveStateAndSnapshot(rf.raftStateForPersist(), snapshot)
DPrintf("RafeNode[%d] TakeSnapshot ends, IsLeader[%v] snapshotLastIndex[%d] lastIncludedIndex[%d] lastIncludedTerm[%d]",
rf.me, rf.leaderId==rf.me, lastIncludedIndex, rf.lastIncludedIndex, rf.lastIncludedTerm)
}
截断日志,并且更新快照raft的lastIncludedTerm和lastIncludedIndex信息。
// 日志是否需要压缩
func (rf *Raft) ExceedLogSize(logSize int) bool {
rf.mu.Lock()
defer rf.mu.Unlock()
if rf.persister.RaftStateSize() >= logSize {
return true
}
return false
}
raftstatesize=log+currentTerm+voteFor加起来的大小
kV层安装快照处理
func (kv *KVServer) applyLoop() {
for !kv.killed() {
select {
case msg := <-kv.applyCh:
// 如果是安装快照
if !msg.CommandValid {
func() {
kv.mu.Lock()
defer kv.mu.Unlock()
if len(msg.Snapshot) == 0 {
// 空快照,清空数据
kv.kvStore = make(map[string]string)
kv.seqMap = make(map[int64]int64)
} else {
// 反序列化快照, 安装到内存
r := bytes.NewBuffer(msg.Snapshot)
d := labgob.NewDecoder(r)
d.Decode(&kv.kvStore)
d.Decode(&kv.seqMap)
}
// 已应用到哪个索引
kv.lastAppliedIndex = msg.LastIncludedIndex
DPrintf("KVServer[%d] installSnapshot, kvStore[%v], seqMap[%v] lastAppliedIndex[%v]", kv.me, len(kv.kvStore), len(kv.seqMap), kv.lastAppliedIndex)
}()
} else {
// 如果是普通log
cmd := msg.Command
index := msg.CommandIndex
func() {
kv.mu.Lock()
defer kv.mu.Unlock()
// 更新已经应用到的日志
kv.lastAppliedIndex = index
// 操作日志
op := cmd.(*Op)
opCtx, existOp := kv.reqMap[index]
prevSeq, existSeq := kv.seqMap[op.ClientId]
kv.seqMap[op.ClientId] = op.SeqId
if existOp {
// 存在等待结果的RPC, 那么判断状态是否与写入时一致
if opCtx.op.Term != op.Term {
opCtx.wrongLeader = true
}
}
// 只处理ID单调递增的客户端写请求
if op.Type == OP_TYPE_PUT || op.Type == OP_TYPE_APPEND {
if !existSeq || op.SeqId > prevSeq {
// 如果是递增的请求ID,那么接受它的变更
if op

本文详细介绍了raft一致性算法中如何实现快照功能,包括在kv层开启快照发送loop,raft层的TakeSnapshot和ExceedLogSize函数,以及处理快照的applyLoop和appendEntriesLoop。此外,还讨论了快照处理可能导致的死锁问题及解决策略。
最低0.47元/天 解锁文章
758





