首页 > 后端开发 > Golang > 正文

了解 etcd 的 Raft 实现:深入研究 Raft 日志

Mary-Kate Olsen
发布: 2024-11-23 06:14:17
原创
144 人浏览过

介绍

本文将从Raft共识算法中的日志开始,介绍和分析etcd的Raft中Raft Log模块的设计和实现。目的是帮助读者更好地理解etcd的Raft实现,并为实现类似场景提供一种可能的方法。

筏日志概述

Raft 共识算法本质上是一个复制状态机,其目标是在服务器集群中以相同的方式复制一系列日志。这些日志使集群中的服务器能够达到一致的状态。

在这种情况下,日志指的是Raft Log。集群中的每个节点都有自己的Raft Log,Raft Log由一系列日志条目组成。日志条目通常包含三个字段:

  • 索引:日志条目的索引
  • 任期:创建日志条目时领导者的任期
  • 数据:日志条目中包含的数据,可以是特定的命令等

需要注意的是,Raft Log 的索引从 1 开始,只有 Leader 节点才能创建 Raft Log 并将其复制到 Follower 节点。

当日志条目持久存储在集群中的大多数节点(例如 2/3、3/5、4/7)上时,它被视为已提交

当日志条目应用于状态机时,它被视为已应用

Understanding etcd

etcd 的 raft 实现概述

etcd raft 是一个用 Go 编写的 Raft 算法库,广泛应用于 etcd、Kubernetes、CockroachDB 等系统。

etcd raft 的首要特点是它只实现了 Raft 算法的核心部分。用户必须自己实现网络传输、磁盘存储以及Raft流程中涉及的其他组件(尽管etcd提供了默认实现)。

与 etcd raft 库的交互有些简单:它告诉您哪些数据需要持久化以及哪些消息需要发送到其他节点。您的责任是处理存储和网络传输过程并相应地通知它。它不关心如何实现这些操作的细节;它只是处理您提交的数据,并根据 Raft 算法告诉您接下来的步骤。

在etcd raft的代码实现中,这种交互模型与Go独特的通道特性无缝结合,使得etcd raft库真正与众不同。

如何实现Raft日志

日志和 log_unstable

在etcd raft中,Raft Log的主要实现位于log.go和log_unstable.go文件中,主要结构是raftLog和unstable。不稳定结构也是 raftLog 中的一个领域。

  • raftLog负责Raft Log的主要逻辑。它可以通过提供给用户的Storage接口访问节点的日志存储状态。
  • 不稳定,顾名思义,包含尚未持久化的日志条目,即未提交的日志。

etcd raft通过协调raftLog和unstable来管理算法内的日志。

raftLog和unstable的核心字段

为了简化讨论,本文将仅关注日志条目的处理逻辑,而不涉及 etcd raft 中的快照处理。

type raftLog struct {
    storage Storage
    unstable unstable
    committed uint64
    applying uint64
    applied uint64
}
登录后复制
登录后复制
登录后复制

raftLog的核心字段:

  • storage:用户实现的存储接口,用于检索已经持久化的日志条目。
  • 不稳定:存储未持久化的日志。例如,当 Leader 收到来自客户端的请求时,它会使用其 Term 创建一个日志条目并将其附加到不稳定日志中。
  • 已提交:在Raft论文中称为commitIndex,它表示最后一个已知已提交日志条目的索引。
  • 正在应用:当前正在应用的日志条目的最高索引。
  • applied:在Raft论文中被称为lastApplied,它是已经应用到状态机的日志条目的最高索引。
type unstable struct {
    entries []pb.Entry
    offset uint64
    offsetInProgress uint64
}
登录后复制
登录后复制

unstable 的核心领域:

  • entries: 未持久化的日志条目,作为切片存储在内存中。
  • offset: 用于将entries中的日志条目映射到Raft Log,其中entries[i] = Raft Log[i offset]。
  • offsetInProgress: 表示当前正在持久化的条目。正在进行的条目由条目[:offsetInProgress-offset]表示。

raftLog 中的核心字段很简单,可以很容易地与 Raft 论文中的实现相关联。然而,unstable 中的字段可能看起来更抽象。以下示例旨在帮助阐明这些概念。

假设我们的 Raft 日志中已经保存了 5 个日志条目。现在,我们在unstable中存储了3个日志条目,并且这3个日志条目当前正在被持久化。情况如下图:

Understanding etcd

offset=6表示unstable.entries中位置0、1、2的日志条目分别对应实际Raft Log中的位置6(0 6)、7(1 6)、8(2 6)。当offsetInProgress=9时,我们知道unstable.entries[:9-6],包括位置0、1和2的三个日志条目,都被持久化了。

在unstable中使用offset和offsetInProgress的原因是unstable并没有存储所有的Raft Log条目。

何时互动

由于我们只关注 Raft 日志处理逻辑,所以这里的“何时交互”是指 etcd raft 何时传递需要用户持久化的日志条目。

用户端

etcd raft 主要通过 Node 接口中的方法与用户交互。 Ready 方法返回一个通道,允许用户从 etcd raft 接收数据或指令。

type raftLog struct {
    storage Storage
    unstable unstable
    committed uint64
    applying uint64
    applied uint64
}
登录后复制
登录后复制
登录后复制

从此通道接收到的 Ready 结构包含需要处理的日志条目、应发送到其他节点的消息、节点的当前状态等等。

对于Raft Log的讨论,我们只需要关注Entries和ComfilledEntries字段:

  • 条目: 需要持久化的日志条目。一旦这些条目被持久化,就可以使用存储接口检索它们。
  • ComfilledEntries: 需要应用于状态机的日志条目。
type unstable struct {
    entries []pb.Entry
    offset uint64
    offsetInProgress uint64
}
登录后复制
登录后复制

处理完Ready传递过来的日志、消息等数据后,我们可以调用Node接口中的Advance方法来通知etcd raft我们已经完成了它的指令,让它可以接收并处理下一个Ready。

etcd raft 提供了 AsyncStorageWrites 选项,可以在一定程度上增强节点性能。然而,我们在这里不考虑这个选项。

etcd 筏侧

在用户端,重点是处理接收到的 Ready 结构体中的数据。在 etcd raft 方面,重点是确定何时将 Ready 结构传递给用户以及之后要采取的操作。

我在下图中总结了这个过程中涉及到的主要方法,图中展示了方法调用的大致顺序(注意,这仅代表大概的调用顺序):

Understanding etcd

可以看到整个过程是一个循环。这里我们先概述一下这些方法的大致功能,在后续的写流分析中,我们会深入研究这些方法是如何作用于raftLog和unstable等核心字段的。

  • HasReady: 顾名思义,它检查是否有一个 Ready 结构体需要传递给用户。例如,如果unstable中有未持久化的日志条目当前不在持久化过程中,HasReady将返回true。
  • readyWithoutAccept: HasReady 返回 true 后调用,该方法创建要返回给用户的 Ready 结构体,包括需要持久化的日志条目和标记为已提交的日志条目。
  • acceptReady: 在etcd raft 将readyWithoutAccept 创建的Ready 结构传递给用户后调用。它将Ready返回的日志条目标记为正在持久化和应用,并创建一个“回调”,当用户调用Node.Advance时调用,将日志条目标记为已持久化和应用。
  • Advance: 在用户调用 Node.Advance 后执行在 AcceptReady 中创建的“回调”。

如何定义承诺和应用

这里有两点需要考虑:

1。坚持≠承诺

正如最初所定义的,只有当日志条目被 Raft 集群中的大多数节点持久化时,该日志条目才被视为已提交。因此,即使我们通过 Ready 持久化 etcd raft 返回的条目,这些条目仍然无法被标记为已提交。

但是,当我们调用 Advance 方法通知 etcd raft 我们已经完成持久化步骤时,etcd raft 将评估集群中其他节点的持久化状态,并将一些日志条目标记为已提交。然后,这些条目通过 Ready 结构的 CommiedEntries 字段提供给我们,以便我们可以将它们应用到状态机。

因此,在使用 etcd raft 时,将条目标记为已提交的时间由内部管理,用户只需满足持久性先决条件即可。

内部通过调用 raftLog.commitTo 方法实现承诺,该方法会更新 raftLog.comfilled,对应 Raft 论文中的 commitIndex。

2。承诺≠应用

在 etcd raft 中调用 raftLog.commitTo 方法后,直到 raft.comfilled 索引的日志条目都被视为已提交。然而,索引在lastApplied

将条目标记为已应用的时间也在 etcd raft 内部处理;用户只需要将Ready中提交的条目应用到状态机即可。

另一个微妙之处是,在 Raft 中,只有 Leader 可以提交条目,但所有节点都可以应用它们。

写请求处理流程

在这里,我们将通过分析 etcd raft 处理写入请求时的流程来连接之前讨论的所有概念。

初始状态

为了讨论更一般的场景,我们将从一个 已经提交并应用了三个日志条目的 Raft 日志开始

Understanding etcd

图中,绿色代表raftLog字段和存储在Storage中的日志条目,而红色代表不稳定字段和存储在entry中的未持久化日志条目。

由于我们已经提交并应用了三个日志条目,因此已提交和已应用的日志条目都设置为 3。applying 字段保存前一个应用程序的最高日志条目的索引,在本例中也是 3。

此时,还没有发起任何请求,因此unstable.entries为空。 Raft Log 中的下一个日志索引是 4,偏移量为 4。由于当前没有日志被持久化,所以 offsetInProgress 也设置为 4。

发出请求

现在,我们发起一个请求,将两个日志条目追加到 Raft Log 中。

Understanding etcd

如图所示,附加的日志条目存储在unstable.entries中。在此阶段,核心字段中记录的索引值不会发生任何更改。

已准备好

还记得HasReady方法吗? HasReady 检查是否有未持久化的日志条目,如果有,则返回 true。

判断是否存在未持久化日志条目的逻辑是基于unstable.entries[offsetInProgress-offset:]的长度是否大于0。显然,在我们的例子中:

type raftLog struct {
    storage Storage
    unstable unstable
    committed uint64
    applying uint64
    applied uint64
}
登录后复制
登录后复制
登录后复制

表示有两个未持久化的日志条目,因此 HasReady 返回 true。

Understanding etcd

准备好但不接受

readyWithoutAccept 的目的是创建要返回给用户的 Ready 结构体。由于我们有两个未持久化的日志条目,readyWithoutAccept 会将这两个日志条目包含在返回的 Ready 的 Entries 字段中。

Understanding etcd

接受准备

acceptReady 在 Ready 结构传递给用户后被调用。

Understanding etcd

acceptReady 将正在持久化的日志条目的索引更新为 6,这意味着 [4, 6) 范围内的日志条目现在被标记为正在持久化。

进步

用户将Entries持久化为Ready后,调用Node.Advance来通知etcd raft。然后,etcd raft 就可以执行在acceptReady 中创建的“回调”了。

Understanding etcd

这个“回调”会清除unstable.entries中已经持久化的日志条目,然后将偏移量设置为Storage.LastIndex 1,即6。

提交日志条目

我们假设这两个日志条目已经被 Raft 集群中的大多数节点持久化,因此我们可以将这两个日志条目标记为已提交。

Understanding etcd

已准备好

继续我们的循环,HasReady 检测到是否存在已提交但尚未应用的日志条目,因此返回 true。

Understanding etcd

准备好但不接受

readyWithoutAccept 返回一个 Ready,其中包含已提交但尚未应用于状态机的日志条目 (4, 5)。

这些条目的计算方式为:低、高:= 在左开、右闭区间内应用 1、提交 1。

Understanding etcd

接受准备

acceptReady 然后将 Ready 中返回的日志条目 [4, 5] 标记为已应用于状态机。

Understanding etcd

进步

用户调用 Node.Advance 后,etcd raft 执行“回调”并将更新应用于 5,表明索引 5 及之前的日志条目已全部应用于状态机。

Understanding etcd

最终状态

这样就完成了写请求的处理流程。最终状态如下图,可以和初始状态进行对比。

Understanding etcd

概括

我们首先概述了 Raft Log,了解其基本概念,然后初步了解了 etcd raft 实现。然后我们深入研究了 etcd raft 中 Raft Log 的核心模块,并考虑了重要的问题。最后,我们通过对写入请求流的完整分析将所有内容联系在一起。

我希望这种方法可以帮助您清楚地了解 etcd Raft 实现,并形成您自己对 Raft Log 的见解。

本文到此结束。如有错误或疑问,欢迎私信或留言。

顺便说一句,raft-foiver 是我实现的 etcd raft 的简化版本,保留了 Raft 的所有核心逻辑,并根据 Raft 论文中的流程进行了优化。以后我会单独发一篇文章介绍这个库。如果您有兴趣,请随时 Star、Fork 或 PR!

参考

  • https://github.com/B1NARY-GR0UP/raft
  • https://github.com/etcd-io/raft

以上是了解 etcd 的 Raft 实现:深入研究 Raft 日志的详细内容。更多信息请关注PHP中文网其他相关文章!

来源:dev.to
本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板