基于Raft的分布式Kv存储项目:raft.h

raft.h 中只有 init 的声明, 它相当于 Raft 节点的"构造完成阶段":绑定外部资源、初始化状态、恢复持久化数据,并启动后台任务。

一、四个参数

peers:集群中所有节点的 RPC 客户端数组。数组下标就是节点编号;本节点位置是 nullptr,发送 RPC 时会跳过自己,构造过程见

me:当前节点在 peers 中的下标,也是节点 ID。

persister:持久化对象,用于保存和恢复任期、投票、日志和快照信息。

applyCh:Raft 向 KV 状态机提交日志的线程安全队列。

二、绑定节点运行环境

复制代码
m_peers = peers;
m_persister = persister;
m_me = me;

这三行建立节点与外部环境的联系。后续:

m_peers[i] 用于发送 RequestVote、AppendEntries 等 RPC;

m_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 覆盖日志 1~100
m_lastSnapshotIncludeIndex = 100
m_lastSnapshotIncludeTerm  = 7
m_logs 中只保存 101 之后的日志

所以本项目区分了"逻辑日志下标"和 m_logs 中的物理下标。

六、初始化两个定时器

复制代码
m_lastResetElectionTime = now();
m_lastResetHearBeatTime = now();

m_lastResetElectionTime:最近一次重置选举定时器的时间

m_lastResetHearBeatTime:Leader 最近一次发送心跳的时间。

选举线程会用:

复制代码
随机选举超时 + m_lastResetElectionTime - 当前时间

计算还需要睡多久。选举超时配置为 300~500ms,心跳间隔为 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;
}

如果快照已经包含日志 1~100,就不能让 apply 线程再次从日志 1 开始执行。因此将:

复制代码
m_lastApplied = 100

后续只应用 101 之后的日志。

这里没有同时设置:

复制代码
m_commitIndex = m_lastSnapshotIncludeIndex;

从语义上看,快照包含的日志必然已经提交,所以更常见的初始化是让二者至少等于快照下标。commitIndex 不需要作为独立字段持久化,但可以根据快照边界重建;源码中的 TODO 正是在讨论这个问题。

九、解锁后启动后台执行单元

复制代码
m_mtx.unlock();

m_ioManager =
    std::make_unique<monsoon::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 只有一个线程,但协程环境会 hook usleep(),睡眠时让出执行权,因此两个定时器可以交替运行。

十、单独启动日志应用线程

复制代码
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
相关推荐
Alluxio28 分钟前
Alluxio + OCI:为 AI 工作负载实现亚毫秒访问延迟与可扩展吞吐
运维·数据库·人工智能·分布式·缓存·ai·语言模型
fengci.1 小时前
CTF2MISC中等部分
开发语言·前端·学习
xys_6782 小时前
JavaEE通关手册:初识Java
java·开发语言
慧都产品智能化3 小时前
Qt 表格升级攻略:复用已有编辑器,减少重复开发
c++·qt·编辑器·qt6.3
ShineWinsu3 小时前
对于Qt:QTableWidget常见属性以及QTableWidgetItem类的解析
开发语言·c++·qt·gui·客户端·表格·qtablewidget
ShineWinsu5 小时前
对于Qt:QListWidget常见属性以及QListWidgetItem类的解析
开发语言·c++·qt·客户端·列表·qlistwidget·qlistwidgetitem
吴名氏.5 小时前
Java转AI Agent教程(第十二周)检索与切分
java·开发语言·人工智能·ai·ai agent
瑞雪兆丰年兮5 小时前
JavaWeb-day10-SpringBootWeb案例
java·开发语言·javaweb·web
Elastic 中国社区官方博客6 小时前
更少的位数,相同的召回率:DiskBBQ 更保守的自动校准扫描
大数据·运维·开发语言·人工智能·elasticsearch·搜索引擎
阿卡好可爱6 小时前
Lua 热更新不生效:改完文件线上仍是旧逻辑,package.loaded 与 upvalue 重绑定详解
数据结构·分布式·后端·架构·lua