基于Raft分布式Kv存储:日志寻找匹配

核心目标

Raft 的日志寻找匹配,本质上是在寻找 Leader 和 Follower 的最大公共前缀:

复制代码
找到最大的 index = M,使得:

Leader.log[M].term == Follower.log[M].term

根据 Raft 的日志匹配性质,只要同一位置的 index + term 相同,那么该位置之前的日志也应相同。找到 M 后,Leader 就可以把 M+1 之后的日志发送给 Follower。(usenix.org)

最朴素的回退

Leader 为每个 Follower 保存:

复制代码
nextIndex[i]

它表示下一条准备发送的日志位置。因此每次发送:

复制代码
prevLogIndex = nextIndex[i] - 1
prevLogTerm  = Leader.log[prevLogIndex].term
entries      = Leader.log[nextIndex[i] ...]

如果 Follower 回复不匹配,最简单的处理是:

复制代码
nextIndex[i]--;

然后重新发送。

假设:

复制代码
Leader.nextIndex = 1001
真正匹配位置      = 500

那么可能需要:

复制代码
1000 → 999 → 998 → ... → 501

也就是大约 500 次失败 RPC。论文中的基础算法就是逐项递减,优化版本则允许一次跳过整个冲突任期。

为什么可以按 Term 跳跃

Raft 日志中的任期具有分段特征:

复制代码
index:  1 2 3 | 4 5 | 6 7 8 | 9 10
term:   1 1 1 | 2 2 | 3 3 3 | 5 5

同一任期生成的日志通常连续出现。

如果 Follower 告诉 Leader:

复制代码
你探测的位置属于我的 term=4
term=4 在我的日志中从 index=20 开始

Leader 就没有必要对 Follower 的整个 term 4 区间逐个尝试,可以直接跳到这个任期的边界。

优化后,失败 RPC 数量通常从:

复制代码
和冲突日志条数成正比

降为:

复制代码
和冲突任期段数量成正比

标准加速回复

一个完整的快速回退回复通常包含:

复制代码
struct AppendEntriesReply {
    int term;
    bool success;

    int conflictTerm;
    int conflictIndex;
};

两个关键字段:

复制代码
conflictTerm
    Follower 在 prevLogIndex 位置上的任期。

conflictIndex
    conflictTerm 在 Follower 日志中第一次出现的位置。

Follower 有两种主要失败情况。

情况一:Follower 日志太短

例如:

复制代码
Leader 发送:
    prevLogIndex = 10

Follower:
    lastLogIndex = 6

Follower 根本不存在日志 10,因此返回:

复制代码
conflictTerm  = 无
conflictIndex = 7

Leader直接执行:

复制代码
nextIndex[follower] = 7;

这样一次就从 11 跳到 7,而不是执行:

复制代码
11 → 10 → 9 → 8 → 7

当前项目已经实现了这种优化:

复制代码
reply->set_updatenextindex(
    getLastLogIndex() + 1
);

Leader 收到后直接更新 m_nextIndex[server]

情况二:下标存在,但 Term 不同

假设 Leader 探测:

复制代码
prevLogIndex = 12
prevLogTerm  = 5

Follower 的日志 12 存在,但属于:

复制代码
Follower.log[12].term = 4

Follower 不只是返回失败,而是向前扫描整个 term 4:

复制代码
index:  8  9 10 11 12
term:   4  4  4  4  4

最终返回:

复制代码
conflictTerm  = 4
conflictIndex = 8

表示:

我的日志 12 属于 term 4,并且这一段 term 4 从日志 8 开始。

Leader 如何利用两个字段

Leader收到 conflictTerm=4 后,先在自己的日志里找 term 4。

如果 Leader 也有 term 4:

复制代码
Leader 最后一个 term 4 日志位于 index=6

则设置:

复制代码
nextIndex[follower] = 6 + 1;

下一次发送:

复制代码
prevLogIndex = 6
prevLogTerm  = 4

为什么找 Leader 中 term 4 的最后一个位置

因为这是双方最有希望匹配的最靠后位置。如果匹配成功,可以保留更多已有日志,减少重复传输。

如果 Leader 完全没有 term 4,则设置:

复制代码
nextIndex[follower] = conflictIndex;

也就是直接跳到 Follower 的 term 4 之前,让 Leader 日志覆盖这段冲突日志。

完整例子

假设双方日志为:

复制代码
index:     1 2 3 4 5 6 7 8 9 10 11 12

Leader:    1 1 1 2 2 3 3 5 5  5  5  5
Follower:  1 1 1 2 2 2 2 2 4  4  4  4

Leader 初始:

复制代码
nextIndex = 13
prevLogIndex = 12
prevLogTerm  = 5

Follower 在日志 12 看到 term 4,返回:

复制代码
conflictTerm  = 4
conflictIndex = 9

Leader没有 term 4,于是:

复制代码
nextIndex = 9

第二次探测:

复制代码
prevLogIndex = 8
Leader.log[8].term   = 5
Follower.log[8].term = 2

Follower返回:

复制代码
conflictTerm  = 2
conflictIndex = 4

Leader存在 term 2,并且最后一个 term 2 位于日志 5,所以标准算法设置:

复制代码
nextIndex = 6
prevLogIndex = 5

双方日志 5 都是 term 2,匹配成功。Leader随后发送日志 6~12

整个过程只有两次失败:

复制代码
13 → 9 → 6

逐项回退则可能需要:

复制代码
13 → 12 → 11 → 10 → 9 → 8 → 7 → 6

当前项目实现了哪一部分

Follower 的冲突处理代码逻辑是:

复制代码
int conflictTerm =
    term(args->prevlogindex());

reply->set_updatenextindex(
    args->prevlogindex()
);

for (int index = args->prevlogindex();
     index >= snapshotIndex;
     --index) {
    if (term(index) != conflictTerm) {
        reply->set_updatenextindex(index + 1);
        break;
    }
}

也就是:

复制代码
1. 取得 Follower 在 prevLogIndex 上的本地 term。
2. 向前扫描。
3. 找出该 term 第一次出现的位置。
4. 通过 updateNextIndex 返回这个位置。

Leader端则直接执行:

复制代码
m_nextIndex[server] =
    reply->updatenextindex();

所以当前实现能够一次跳过 Follower 的整个冲突任期,而不是只执行 nextIndex--

相关推荐
IT大白鼠10 小时前
MySQL 分布式集群系列 · 第四篇——实操部署指南:从零搭建生产级 MySQL NDB 集群
数据库·分布式·mysql
IT大白鼠10 小时前
MySQL 分布式集群系列 · 第三篇——核心原理精讲:NDB 自动分片、多主写入与数据同步
数据库·分布式·mysql
星恒讯工业路由器21 小时前
分布式光伏站点分散难联网?4G/5G点对多点自组网方案解析
分布式·物联网·5g·分布式光伏·自组网·点对多点通信·4g/5g
qq_452396231 天前
第十一篇:《eBPF 进阶:自定义探针与生产级可观测性部署》
分布式
重庆小透明1 天前
Kafka 完全指南:从基础组件到核心原理(包含面试题)
java·分布式·微服务·架构·kafka
一本正经的不务正业1 天前
kafka与RocketMQ的不同
分布式·kafka·rocketmq
国科安芯1 天前
基于抗辐射MCU的卫星分布式控制系统CANFD总线架构研究
网络·人工智能·分布式·单片机·嵌入式硬件·架构·抗辐射
深蓝电商API2 天前
Redis 在分布式爬虫中的作用
redis·分布式·爬虫
凤山老林2 天前
高可用分布式任务调度架构:Spring Boot 集成 PowerJob 实战指南
spring boot·分布式·架构
海宇数据2 天前
零信任架构实战:基于海宇运营商近3个月平均账单构建自动化分布式租赁网关
人工智能·分布式·架构·自动化