基于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] 用于发送 RequestVoteAppendEntries 等 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_nextIndexm_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
相关推荐
Sammyyyyy1 小时前
如何利用本地技术栈构建 0 成本 AI SaaS 雏形
开发语言·人工智能·python·ai·servbay
冻柠檬飞冰走茶1 小时前
PTA基础编程题目集 7-13 日K蜡烛图(C语言实现)
c语言·开发语言·数据结构·算法
csdn_aspnet1 小时前
C# 从凸包中删除点(Deleting points from Convex Hull)
开发语言·c#
cts6182 小时前
Python全栈claude.md文档
开发语言·python
wWYy.2 小时前
基于Raft分布式Kv存储:AppendEntries
分布式
Quz2 小时前
QML RangeSlider 组件:范围选择与双滑块样式
qt
忘路之远近i2 小时前
受够阿里云自带终端后,我用 Cursor + grill-me 做了个运维面板
服务器·开发语言·人工智能·python·阿里云·云计算
Tim_102 小时前
【C++】020、野指针&悬空指针
java·开发语言
luoyayun3612 小时前
【Qt for Harmony】 Qt 5.12.12 鸿蒙版 Windows 交叉编译
qt·harmonyos·qt for harmony
吃好睡好便好2 小时前
MATLAB中图像格式的转换
开发语言·图像处理·学习·计算机视觉·matlab