跳转到主要内容

使用 Raft 确保服务器节点间的一致性 🖧

如果你有非常重要的信息,需要保护它免受损坏,该怎么办?如果你无法承受失去对它的访问权,该怎么办?你应该如何存储它,以确保数据在保持完整性的同时,在你需要时始终可用?为了回答这些问题,我们 KeyDB 决定使用 Raft 算法,并计划在 KeyDB 的未来版本中实现它。

Raft 的吉祥物 (来源)

为了展示 Raft 的有效性,让我们从一个例子开始。假设你是一家金融机构的软件架构师,你需要一个数据库系统来存储账户余额信息。这些信息极其敏感,余额意外变动可能导致一些非常灾难性的后果(想象一下,因为数据库设计不佳而失去毕生积蓄)。用户也希望他们的账户能够 24/7 全天候可用。

该系统将这样运作

  1. 用户请求从他们的账户存入或提取资金。
  2. 系统会相应地增加或减少他们的账户余额。
  3. 系统会向用户回传一条消息,告知他们的交易是否成功。

你希望这个系统具备一些特性

  • 最短停机时间:你不想过于频繁地给用户带来不便。
  • 强一致性:一旦账户余额发生变化,任何新查看该账户的用户都将看到更新后的余额。
  • 高性能:你希望它在不牺牲上述标准的情况下尽可能快。

我们如何满足这些标准呢?让我们来探讨一些设计决策。

集中式还是分布式?#

第一个选择是在集中式和分布式方法之间做出决定。我们是希望用单个服务器存储整个数据库,还是希望将数据库分布在一个服务器网络上?对于单个服务器,如果它宕机,整个数据库都将无法访问。由于我们希望最大限度地减少系统的停机时间,所以单点故障是个坏主意。我们可以通过选择分布式方法来缓解这个问题,如果部分网络正常运行,我们仍然应该对账户信息有一定程度的访问权限。因此,我们将采用分布式方法。

我们如何拆分数据?#

既然我们已经决定让多台服务器来承担数据库的负载,那么我们该如何将其拆分到多台服务器上呢?有很多方法可以实现,例如分片,但在我们的案例中,**复制**最能满足我们的需求。

复制是一种数据库分布式方法,其中每台服务器都存储数据库的精确副本。在我们的案例中,这意味着我们网络上的每台服务器都拥有每个账户的余额信息。通过以这种方式复制数据,我们提供了一种直接的方法来容忍部分网络故障;如果一台服务器宕机,我们可以简单地转向另一台服务器获取数据。这使我们能够最大限度地减少用户可感知的停机时间,所以我们将采用这种方法。

复制:同步还是异步#

我们还要做出另一个选择,我们希望复制是同步进行还是异步进行?

同步复制是指命令首先传播到网络中的服务器,只有在大多数服务器收到该命令后,网络中的服务器才会处理它。在我们的例子中,当用户更新他们的余额时,会发生以下情况:

  1. 服务器接收到更新请求。
  2. 服务器将该请求传播到网络中的其余服务器,以期达到大多数。
  3. 服务器等待,直到收到确认,表明网络中大多数服务器已经看到了该请求。
  4. 该服务器和网络上的所有服务器将更新请求应用到它们的数据库副本中,在这种情况下,它们将用户的账户余额更新为正确的金额。
  5. 用户收到更新确认。

异步复制是指命令的处理与其向其他服务器的传播并行进行。对我们来说,这意味着在更新账户余额的同时,将请求传播到网络的其余部分。

异步复制速度更快,因为你不必等待命令被复制后再响应用户。然而,这种方法可能会遇到一致性问题。假设一个用户在某台服务器上进行了一笔交易。服务器响应了用户,但在将请求传播到网络中其余服务器之前宕机了。现在,当一台新服务器为用户的下一笔交易提供服务时,它会像那笔交易没有发生过一样操作,但用户会认为交易已经发生,从而导致状态不一致。而使用同步复制,你会在响应用户之前等待复制完成。只要新服务器是看到该请求的大多数服务器之一,这种情况就不会发生。尽管同步方法的关键路径耗时更长,但我们能保证复制已经发生,这使其更适合我们未来的目标。

需要多少个主节点?#

既然我们已经决定要进行同步复制,那么还有最后一个决定要做:我们的网络中需要多少个主节点?**主节点**(master)是可以对数据库进行读写操作(并将这些更改传播到网络其余部分)的服务器,而**副本**(replica)则是在主节点宕机时可以作为潜在备份的服务器。如果对一致性要求不那么严格,你也可以选择从副本读取以提高性能,但这在我们的案例中不适用。这里有两个选择:单主节点和多主节点。

单主节点复制,顾名思义,在同一时间只允许网络上有一个主节点。类似地,多主节点复制允许网络上有多个主节点。多主节点复制可以实现更高的整体正常运行时间,因为如果一个主节点发生故障,网络上还有其他主节点可以处理传入的请求,从而实现零故障切换时间。相比之下,单主节点设置在主节点宕机时会遇到一些故障切换时间,因为必须将其中一个副本提升为主节点才能继续处理请求。

然而,对于多主节点复制,由于多个主节点都可以更改数据库(修改账户余额),因此更难实施我们的强一致性要求。相比之下,只有一个数据库更改源可以更容易地保证强一致性,因为我们不必处理多个主节点同时更新同一个账户余额的情况。因此,我们将采用单主节点设置。

现在,让我们来构建一个 Raft!#

既然我们已经决定我们的解决方案是一个单主节点、同步复制的数据库,那么我们该如何为其设计一个算法呢?答案是我们不需要,Raft 就是为此而生的!你可以在这里找到更深入的描述,在这里有一个很好的引导式可视化,并在这里获得更多资源。

简而言之,Raft 通过拥有一个单一的主节点——称为**领导者**(leader)——来保证系统保持一致,这个领导者只有在大多数服务器都看到了某个命令及其之前的所有命令后,才会将该命令应用到数据库中。在 Raft 的上下文中,大多数指的是网络中所有服务器(包括那些未运行的服务器)中严格超过半数的服务器。除了数据库副本外,每台服务器上还保留一个日志,以跟踪该服务器见证过的所有命令。这样,大多数副本——称为**追随者**(followers)——在任何给定时间都将与领导者完全同步。任何未完全同步的追随者都会从领导者那里按顺序定期接收更新,直到它们变得同步为止。

领导者是通过一次多数票选举产生的,当一个追随者在一段时间内没有收到领导者的响应时,就会触发选举。这次多数票选举被称为**选举**(election)。该追随者会投票给自己,然后向网络上的其他所有服务器发送投票请求,此时它被称为**候选者**(candidate)。每台服务器都会投票给第一个遇到的、至少和自己一样新的候选者,并且在一次选举中最多只投一次票。一旦某个服务器获得了大多数服务器的投票,它就被宣布为领导者,并向其他服务器广播一条消息,其他服务器随后降级为追随者(如果它们之前是候选者)。在集群启动的情况下,每台服务器都将以追随者身份启动。最终,一台或多台服务器将触发投票,从而选出一位领导者。

由于网络中大多数服务器在任何时候都是最新的,如果一个非最新的候选者试图成为领导者,那些最新的追随者(它们构成了多数)将不会投票给它,该候选者将永远无法获得多数票,因此也永远无法成为领导者。因此,唯一能成为领导者的候选者是那些最新的候选者。由此得出的推论是,如果大多数服务器与领导者不同步,而该领导者宕机了,就有可能选出一个过时的领导者(因为只有少数服务器不会投票给它)。这将导致我们丢失已经应用到数据库中的数据,从而违反强一致性要求。

在我们的案例中,这意味着所有请求都通过领导者。领导者然后处理这些请求并返回结果(账户余额或交易确认),但这只有在确保大多数追随者已经看到该请求之后才会发生。如果领导者在某个时刻宕机,将在最新的追随者中选出一位新领导者,它将从现在开始处理交易,直到需要选举下一位领导者为止。

通过使用 Raft,由于命令只有在大多数服务器见证后才被处理,并且只有这些服务器才有可能成为领导者(并随后被读取),因此不存在状态不一致的风险。此外,由于 Raft 的分布式特性,当领导者宕机时,网络中还有其他服务器可以充当领导者,从而最大限度地减少了停机时间,而不会导致已处理数据的丢失。这解决了三个问题中的两个,但第三个问题呢?

KeyDB 在其中扮演什么角色?#

考虑到每个请求,无论是读还是写,都必须通过单个领导者,Raft 的性能与其领导者的性能密切相关。如果那个数据库很慢,算法的性能就会因此受到影响。我们如何在满足其他要求的同时最大化我们的性能?这就是 KeyDB 发挥作用的地方。KeyDB 速度非常快,因此在提供高性能 Raft 集群解决方案的同时,保持其强一致性保证方面具有独特的优势。我们正在积极地将 Raft 集成到 KeyDB 中,敬请期待!

关注 KEYDB 的最新动态#

我们正在开发一些非常酷的功能。要了解我们的最新动态,请关注我们的以下渠道之一