一、持久化的总体设计这个项目把持久化分成两部分mermaid flowchart LR R[Raft 内存状态] -- RS[raftstatePersistN.txt] K[跳表 KV 状态机] -- SS[snapshotPersistN.txt] SS --|表示日志 1...snapshotIndex 的执行结果| K RS --|保存 snapshotIndex 后剩余日志| R 每个节点N对应两个相对当前工作目录的文件raftstatePersistN.txtRaft 自身状态和未被快照覆盖的日志。snapshotPersistN.txtKV 数据库快照和客户端去重信息。二、Raft State 保存什么Raft::persistData()会持久化五项内容currentTerm 当前任期 votedFor 当前任期投给了谁 lastSnapshotIncludeIndex 快照包含到哪个日志下标 lastSnapshotIncludeTerm 快照最后一条日志的任期 logs 快照之后剩余的 Raft 日志没有持久化的状态包括status Follower/Candidate/Leader commitIndex lastApplied nextIndex[] matchIndex[] 选举和心跳计时器这些属于 Raft 易失状态。节点启动后重新成为 FollowercommitIndex可以根据新 Leader 的消息逐步恢复。日志条目首先由 Protobuf 序列化message LogEntry { bytes Command 1; int32 LogTerm 2; int32 LogIndex 3; }然后把每个 Protobuf 字节串放进vectorstring最后使用 Boosttext_oarchive序列化整个 Raft 状态。三、什么时候保存 Raft State统一入口是void Raft::persist() { auto data persistData(); m_persister-SaveRaftState(data); }见 [raft.cpp (line 603)](/C:/Users/LENOVO/Desktop/KVstorageBaseRaft-cpp-main/src/raftCore/raft.cpp:603)。主要触发时机有Leader 收到新客户端命令将其加入日志后。Follower 通过AppendEntries收到或修改日志后。节点发起选举增加currentTerm并投票给自己后。节点在RequestVote中修改任期或投票后。收到更大任期的 RPC 回复退化为 Follower 后。安装快照并修改快照边界后。这样设计的目的是保证节点崩溃后不会忘记自己投过票也不会丢失已经接受的日志。不过这里每次persist()都会序列化全部剩余日志 → 清空旧文件 → 重新写入全部数据因此它不是追加式 WAL日志越大每次写入的成本越高。四、Persister 如何写文件SaveRaftState()先调用clearRaftState()m_raftStateOutStream.open( m_raftStateFileName, std::ios::out | std::ios::trunc );也就是清空旧文件然后写入新的完整状态[Persister.cpp (line 35)](/C:/Users/LENOVO/Desktop/KVstorageBaseRaft-cpp-main/src/raftCore/Persister.cpp:35)。m_raftStateSize记录当前序列化数据大小KvServer 用它判断是否需要制作快照。Persister的互斥锁只能保证同一进程内多个线程不会同时读写文件不负责跨进程同步。五、KV 快照保存什么KV 快照包含两部分m_skipList 当前全部 key-value m_lastRequestId 每个客户端最后执行的 RequestId为什么必须保存m_lastRequestId因为节点恢复后仍然要识别客户端重试请求否则一个已经成功的 Append 可能再次执行破坏线性一致性。快照使用两层 Boost 序列化跳表 → SkipListDump{keyDumpVt, valDumpVt} → m_serializedKVData → KvServer{m_serializedKVData, m_lastRequestId} → 最终 snapshot 字符串快照不保存跳表每个节点的随机层数只保存 key 和 value。恢复时重新插入元素并随机生成层数这是合理的因为跳表层级不影响逻辑数据。六、快照如何产生每条日志应用到状态机后KvServer 会检查 Raft 状态大小if (m_raftNode-GetRaftStateSize() m_maxRaftState / 10.0) { auto snapshot MakeSnapShot(); m_raftNode-Snapshot(raftIndex, snapshot); }Raft::Snapshot()执行以下工作验证index已经提交且比旧快照更新。记录该日志的index和term。删除index及之前的 Raft 日志。保留index 1之后的日志。同时保存新的 Raft State 和 KV 快照。最后调用m_persister-Save(persistData(), snapshot);假设快照包含到日志 100那么持久化状态可以理解成snapshotPersistN.txt日志 1100 执行后的 KV 数据 raftstatePersistN.txt快照边界 100/term以及日志 101最后七、落后节点如何安装快照如果 Leader 发现nextIndex[follower] lastSnapshotIncludeIndex说明 Follower 需要的日志已经被 Leader 删除无法再通过AppendEntries补齐只能发送快照。Leader 从持久层读取快照构造InstallSnapshotRequestLeaderId Term LastSnapshotIncludeIndex LastSnapshotIncludeTerm DataFollower 收到后会检查任期和快照下标。截断已经被快照覆盖的日志。更新commitIndex、lastApplied和快照边界。将快照封装成ApplyMsg发给 KvServer。同时持久化新的 Raft State 和快照。KvServer 反序列化快照并恢复跳表。八、节点重启时的预期恢复理论上的恢复顺序是创建 Persister → Raft::init() → ReadRaftState() → readPersist() → 恢复 term、投票、快照边界、剩余日志 → KvServer::ReadSnapshot() → 恢复跳表和 m_lastRequestId → 继续接收 Leader 日志