基于Raft的分布式Kv存储项目:raft.h
raft.h中只有init的声明 它相当于 Raft 节点的“构造完成阶段”绑定外部资源、初始化状态、恢复持久化数据并启动后台任务。一、四个参数peers集群中所有节点的 RPC 客户端数组。数组下标就是节点编号本节点位置是nullptr发送 RPC 时会跳过自己构造过程见me当前节点在peers中的下标也是节点 ID。persister持久化对象用于保存和恢复任期、投票、日志和快照信息。applyChRaft 向 KV 状态机提交日志的线程安全队列。二、绑定节点运行环境m_peers peers; m_persister persister; m_me me;这三行建立节点与外部环境的联系。后续m_peers[i]用于发送RequestVote、AppendEntries等 RPCm_persister用于崩溃恢复m_me用于标识自己、跳过给自己发送 RPC。这些赋值发生在加锁之前但此时 Raft 自己的后台任务还没有启动所以从init内部来看暂时没有竞争。三、建立初始 Follower 状态m_mtx.lock(); applyChan applyCh; m_currentTerm 0; m_status Follower; m_commitIndex 0; m_lastApplied 0; m_logs.clear(); m_votedFor -1;各字段含义如下applyChan保存与 KVServer 共用的提交队列。m_currentTerm 0默认从第 0 任期开始之后可能被磁盘状态覆盖。m_status Follower节点重启后一律作为 Follower不能恢复成 Leader。Leader 身份必须通过重新选举获得。m_commitIndex 0目前已知已经提交的最高日志下标。m_lastApplied 0已经交给状态机执行的最高日志下标。m_logs.clear()先清空内存日志再从持久化状态恢复。m_votedFor -1当前任期尚未投票。“先给默认值再读取磁盘覆盖”使首次启动和崩溃恢复可以共用同一套逻辑。四、初始化 Leader 专用数组for (int i 0; i m_peers.size(); i) { m_matchIndex.push_back(0); m_nextIndex.push_back(0); }这两个数组只在节点成为 Leader 后真正使用m_nextIndex[i]下一次应该向节点i发送的日志下标。m_matchIndex[i]已知节点i已成功复制的最高日志下标。这里主要是建立与集群节点数量相同的数组。初始值0并不是最终 Leader 状态节点当选 Leader 时会重新设置m_nextIndex[i] lastLogIndex 1; m_matchIndex[i] 0;一个细节是这里没有先执行m_nextIndex.clear()和m_matchIndex.clear()。因此同一个Raft对象如果多次调用init数组会不断增长。当前调用路径通常只初始化一次所以暂时不会暴露。五、初始化快照边界m_lastSnapshotIncludeIndex 0; m_lastSnapshotIncludeTerm 0;由于日志压缩m_logs不一定从日志下标 1 开始。这两个变量记录快照覆盖到哪一条日志快照最后一条日志所属的任期。例如snapshot 覆盖日志 1100 m_lastSnapshotIncludeIndex 100 m_lastSnapshotIncludeTerm 7 m_logs 中只保存 101 之后的日志所以本项目区分了“逻辑日志下标”和m_logs中的物理下标。六、初始化两个定时器m_lastResetElectionTime now(); m_lastResetHearBeatTime now();m_lastResetElectionTime最近一次重置选举定时器的时间m_lastResetHearBeatTimeLeader 最近一次发送心跳的时间。选举线程会用随机选举超时 m_lastResetElectionTime - 当前时间计算还需要睡多久。选举超时配置为 300500ms心跳间隔为 25m,初始化成当前时间可以避免节点刚启动就立刻发起选举。七、恢复持久化状态readPersist(m_persister-ReadRaftState());readPersist()会覆盖以下字段m_currentTerm m_votedFor m_lastSnapshotIncludeIndex m_lastSnapshotIncludeTerm m_logs它们对应 Raft 的持久状态重启后不能遗忘当前任期、已经投给谁以及日志内容。日志的恢复过程比较特别先由 Boost 反序列化出字符串数组再由 Protobuf 将每个字符串解析成LogEntrym_status不恢复因为角色是临时状态m_nextIndex、m_matchIndex也不恢复因为只有 Leader 使用而且可以重新计算。八、处理快照恢复边界if (m_lastSnapshotIncludeIndex 0) { m_lastApplied m_lastSnapshotIncludeIndex; }如果快照已经包含日志 1100就不能让 apply 线程再次从日志 1 开始执行。因此将m_lastApplied 100后续只应用 101 之后的日志。这里没有同时设置m_commitIndex m_lastSnapshotIncludeIndex;从语义上看快照包含的日志必然已经提交所以更常见的初始化是让二者至少等于快照下标。commitIndex不需要作为独立字段持久化但可以根据快照边界重建源码中的 TODO 正是在讨论这个问题。九、解锁后启动后台执行单元m_mtx.unlock(); m_ioManager std::make_uniquemonsoon::IOManager( FIBER_THREAD_NUM, FIBER_USE_CALLER_THREAD);先解锁再启动后台任务非常重要否则新任务一启动就可能等待m_mtx。当前配置创建一个 IOManager 工作线程并且不使用调用init的线程。IOManager 构造时就会启动调度器。随后加入两个协程任务m_ioManager-scheduler([this] { leaderHearBeatTicker(); }); m_ioManager-scheduler([this] { electionTimeOutTicker(); });leaderHearBeatTicker()节点是 Leader 时每隔约 25ms 调用doHeartBeat()。electionTimeOutTicker()Follower/Candidate 长时间没有收到 Leader 消息时调用doElection()。虽然两个函数都是无限循环而且 IOManager 只有一个线程但协程环境会 hookusleep()睡眠时让出执行权因此两个定时器可以交替运行。十、单独启动日志应用线程std::thread t3(Raft::applierTicker, this); t3.detach();applierTicker()不断检查m_lastApplied m_commitIndex如果存在已经提交但尚未应用的日志就构造ApplyMsg并写入applyChan。KVServer 在 [ReadRaftApplyCommandLoop (line 281)](/C:/Users/LENOVO/Desktop/KVstorageBaseRaft-cpp-main/src/raftCore/kvServer.cpp:281) 中阻塞读取这个队列然后真正修改 KV 状态机。它使用独立线程是为了避免应用流程影响选举和心跳的时间敏感任务。完整启动链路KvServer 创建 Raft、Persister 和 applyChan ↓ 建立到其他节点的 RPC 客户端 ↓ Raft::init() ↓ 绑定 peers / persister / applyChan ↓ 建立默认 Follower 状态 ↓ 从持久化数据恢复 term、vote、snapshot、logs ↓ 启动心跳协程、选举协程、apply 线程 ↓ 等待选举或接收其他节点 RPC