基于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--

相关推荐
ACP广源盛139246256732 小时前
YLB3118 存储桥接芯片完整机会点@ACP#联动曙光 8000
大数据·人工智能·分布式·单片机·嵌入式硬件
韩楚风18 小时前
【参天引擎】一次宕机后的数据恢复,让我把 Cantian 持久化与恢复的六大机制全搞明白了
服务器·网络·数据库·分布式·mysql·架构·cantian
AAA@峥1 天前
Ceph RBD 块存储全解析:原理与实操汇总
运维·分布式·ceph
LLL人无再少年1 天前
分布式链路追踪系统之二进制安装skywalking
分布式·skywalking
心念枕惊1 天前
分布式、服务化的ERP系统架构设计
分布式·系统架构
重庆小透明1 天前
分布式事务方案对比
分布式
向夏威夷 梦断明暄2 天前
从架构特点到功能缺陷,重新认识分析型分布式数据库
数据库·分布式·架构
AAA@峥2 天前
Ceph 集群配置管理完整指南
运维·数据库·分布式·ceph
心如鉄补2 天前
分布式链路追踪系统之docker-compose安装skywalking
分布式·docker·skywalking