首页 > 其他分享 >Apache BookKeeper Insights Part 1 — External Consensus and Dynamic Membership

Apache BookKeeper Insights Part 1 — External Consensus and Dynamic Membership

时间:2022-11-13 10:45:04浏览次数:84  
标签:AQ Dynamic 条目 Consensus 复制 Raft BookKeeper Kafka

标题:Apache BookKeeper Insights Part 1 — External Consensus and Dynamic Membership
原文:https://medium.com/splunk-maas/apache-bookkeeper-insights-part-1-external-consensus-and-dynamic-membership-c259f388da21
时间:2021-11-10
社区翻译:https://mp.weixin.qq.com/s/er9CLpa2iex5MXapsba_lA

Series Introduction

BookKeeper复制协议非常有趣,它与人们在消息领域中使用的其他复制协议如Raft(RabbitMQ仲裁队列、Red Panda、NATS Streaming)或Apache Kafka复制协议有很大不同。但是,不同意味着人们往往不能完全理解它,当它以不期望的方式出现时,可能会被绊倒,或者没有充分利用它的潜力。

本系列旨在帮助人们了解BookKeeper与众不同之处的一些基本见解,并深入了解协议的一些细微差别。我们将深入了解协议背后的原因,以及这些设计决策所产生的后果。

我所知道的描述设计决策的最好方法之一是通过比较。比较一件事和另一件事是讨论取舍、优缺点和许多其他方面的好方法。

我将使用Raft和Apache Kafka作为比较点。我不会试图说服你,BookKeeper比其他协议更好,这不是一篇遮遮掩掩的营销文章。这篇文章是关于教授BookKeeper协议的机制及其影响。

还要注意的是,这并不是对Raft或Kafka的深入研究。我将为我的目标描述足够多的协议,但会掩盖大量的复杂性。如果你想更多地了解Raft和Apache Kafka,这些协议在其他地方都有很好的文档记录。

第一篇文章描述了BookKeeper和其他复制协议之间的最大区别。这种差异也影响了后来大多数关于协议细微差别的文章。

Integrated vs External Coordination

Raft是一个“集成”协议。我的意思是,控制面和数据面都集成到同一协议中,该协议由所有对等的存储节点执行。每个节点都将所有数据本地持久化存储。

Apache Kafka也是如此,尽管它使用了ZooKeeper保存元数据,但很快就会被删除(KIP-500)。

在Raft中,我们有一个稳定的状态,在这个状态下,复制正在执行,然后是触发选举的扰动周期。一旦选出领导者,领导者节点将处理所有客户端请求,并将条目复制到追随者。

通过Raft,领导者了解每个跟随者在日志中的位置,并开始根据他们的位置向每个跟随者复制数据。因为领导者在本地拥有所有的状态,所以无论跟随者落后多远,他都可以检索并传输该状态。

图1 集成复制协议,其中由承载数据的有状态节点执行复制

对于Kafka,追随者向领导者发送获取请求,请求包括他们当前的位置。领导者在本地拥有所有状态,只需检索下一个数据并将其发送回跟随者。

让有状态的节点执行复制的一个副作用是,集群成员是相对静态的。是的,你可以执行群集操作来添加和删除成员,但这些操作非常罕见,而且有限制。就协议的正常运行而言,可以认为Raft集群的成员和构成Kafka主题的副本是固定的。

BookKeeper不一样。它将共识算法和存储分开。存储节点很简单,基本上可以存储和检索它们被告知的内容。他们对复制协议本身几乎一无所知。复制协议位于存储节点外部,位于BookKeeper客户端中。是客户端执行对存储节点数据的复制。

图2 客户端执行复制

BookKeeper被设计成另一类数据系统,即分布式日志存储子系统,比如消息系统或数据库,比如Apache Pulsar。Pulsar broker使用BookKeeper存储主题和游标,每个broker使用BookKeeper客户端对这些BookKeeper节点进行读写操作。

客户端是外部的、无状态的,它具有许多级联效应,这些效应影响了协议其余部分的设计。例如,由于客户端在本地没有完整状态,因此需要以不同的方式处理故障。

使用Raft,如果一个节点在一个小时内不可用,我们不会有大问题。当节点恢复时,有状态的领导者只需从其停止的位置将数据复制到跟随者。BookKeeper客户端没有这么奢侈,如果它想继续提供服务,就不能在内存中存储最后X小时的数据,它必须做一些不同的事情。

由于复制和协调逻辑位于存储节点的外部(在客户端中),因此发生故障时,客户端可以自由更改ledger的成员。这种动态成员是一个基本的功能差异,也是BookKeeper最引人注目的功能之一。

像Pulsar这样拥有独立存储层的数据系统也有其缺点,比如在任何数据到达磁盘之前需要额外的网络跳数,并且必须操作一个独立的bookie集群。如果BookKeeper不提供一些真正有价值的功能,那么它将更多地成为一种负债而不是资产。对我们来说幸运的是,BookKeeper有许多奇妙的功能,使它值得。

现在我们已经设定了场景,我们将进一步深入探讨像Raft这样的集成、固定成员协议与BookKeeper这样的外部协商、动态成员协议的比较。

Commit Index

我们的三个协议都有提交索引(commit index)的概念,尽管它们有不同的名称。提交索引是日志中的一个偏移量,此偏移量及之前的所有条目都将在一定数量的节点故障后不丢失。

在每种情况下,条目必须达到某个复制因子才能被视为已提交:

  • 对于Raft,它是一个集群多数派协议,保证提交的条目在任何少数节点(N/2)永久失效的情况下不丢失。因此,Raft要求获得大多数集群成员确认的条目,才被认为是已提交条目。
  • 对于Kafka来说,这取决于各种配置。Kafka通过使用客户端配置acks=all和broker配置min-insync-replicas=[majority]来支持majority quorum行为。默认情况下,领导者需要在条目确认之前将它持久化下来。
  • 对于BookKeeper来说使用Ack Quorum(AQ),并且保证AQ-1个bookies失效的情况下不会丢失已提交条目。

注意:由于每个协议都不同,我将把一个条目被视为“已提交”所需的法定人数称为提交法定人数(Commit Quorum)。这是我为这篇文章发明的术语。

Raft将日志中的这一点称为commit index,Kafka将其称为High Watermark,BookKeeper将其称为Last Add Confirmed(LAC)。每个协议都依赖此提交索引来提供其一致性保证。

在Raft和Kafka中,这个提交索引在领导者和追随者之间传输,因此每个节点都有自己的当前提交索引信息。领导者总是知道提交索引的最新值,而追随者可能有一个过时的值,但这没关系。

图3 所有节点都有自己的当前提交索引视图,有时是过时的

对于Kafka,领导者通过在发给跟随者的fetch response中加入High Watermark。

对于BookKeeper,LAC包含在发送到存储节点的每个条目中。存储节点本身对LAC几乎没有什么用处,但它允许客户端后续可以检索这些重要信息。因此,向ledger写入的客户端知道当前的LAC,存储节点可能对LAC的变化稍微滞后,但没关系,协议可以处理这个问题,稍后再详细介绍。

图4 客户知道当前的LAC,bookies对LAC的感知通常有点陈旧

超过提交索引的读取将是脏读取,不能保证能够再次读取相同的条目。提交索引之外的条目可能会丢失或被其他条目替换。因此,每个协议都不允许读取超过这点的内容。

Raft/Kafka Properties and Behaviour

对于基于Raft的系统,复制因子决定了Raft集群成员数量。对于Kafka来说,复制因子决定了一个topic有多少个副本。

Fixed Membership

Raft成员和Kafka副本在稳定复制时是固定的。这种固定成员身份的一个成本是复制因子、可用性和延迟之间的权衡。

在一个理想的世界里,我们希望每个条目在被确认之前都被完全复制。但追随者可能会宕机,也可能运行变慢。由于单个节点不可用而导致群集无法写入,这是大多数人都无法接受的。因此,折衷方案是稍微降低安全性,以获得可用性并降低延迟。我们允许少数成员不可用,但仍然提供良好的数据安全性和持续可用性。

这就是为什么Raft和Kafka确实需要一个低于replication factor的commit quorum。

这种安全性降低可以通过简单地增加复制系数来缓解。因此,如果您希望保证提交的条目能够在丢失2个节点后继续存在,那么您需要将复制系数设置为5。你需要为存储和网络支付更多的费用,延迟也会受到一些影响,但你只需要4个追随者中最快的2个来确认一个条目,以便向客户端确认该条目。因此,即使有两个较慢的节点,您也有可接受的延迟,并达到满意的最小复制系数。

Properties

不变量表示任何时候都必须为真。您可以随时查看系统的状态,并验证其状态是否符合不变量。例如,已提交的条目没有丢失就表示一个不变量。

Liveness告诉我们在某个时刻必须发生什么,例如,考虑到大多数节点最终都能正常工作,并且可以看到彼此,因此最终必须会选出一个领导者。

我们的集成日志复制协议包括以下不变量:

  1. 条目按时间顺序附加到领导者的日志中。
  2. 领导者按照与自己日志相同的顺序将条目附加到跟随者日志中。
  3. 只要不是大多数节点失效,提交的条目就永远不会丢失(对于Kafka需要配置成ack=all、min-insync-replicas=[majority])。
  4. 从提交索引往前,跟随者节点上的日志与当前领导者的日志相同。

一个liveness属性是,假设所有节点都是正常的,并且彼此可见,那么最终任何给定的提交条目都将被完全复制(只要日志的前缀也被完全复制)。换句话说,日志尾部的条目最终将达到所需的复制因子。

图5 Raft或Kafka日志的三个安全区

我们可以根据安全性将Raft复制日志分为三个区域。首先,在committed index之外的是危险区,这些条目没有保证,可能会丢失。然后,已提交日志可以分为两部分,黄色区域是条目到达多数确认但尚未完全复制的区域,绿色区域是完全复制的区域。

Prefix RF >= Entry RF >= Suffix RF

以上规则说明,对于日志中的任何给定偏移量,该点的前缀必须达到相同或更高的复制因子,该点之后的后缀必须达到相同或更低的复制因子。

这对管理员来说意味着什么?

当一切顺利时,我们会期待一个小的未承诺区(红色),一个小的承诺头部(黄色)和一个非常大的承诺尾部(绿色)。但事情并不总是进展顺利,提交的头/尾可以是任意长度,尾部长度可以是0,这意味着没有完全复制的条目。这可能是因为跟随者的速度太慢(以及过去的数据保留),也可能意味着跟随者灾难性地宕机,然后恢复到空状态。

关键是,复制因子不是保证,而是期望的目标,唯一的保证是要满足commit quorum。因此,commit quorum是复制的最低保证。作为一名管理员,你需要规划你的程序,而不仅仅是复制因子。

Recovery from failure

使用集成复制协议的系统使得从磁盘完全失效中恢复“相对”简单(这里指换磁盘)。任何空的跟随者都可以从当前的领导者那里重新填充,方式与大部分处于追赶中的跟随者完全相同,复制可以节省时间。

Easy to reason about

所有这些特性使得关于Raft/Kafka日志状态的推理相对简单:

  • 成员是固定的,所以我们知道数据在哪里。
  • 我们知道只有日志的头部可能存在没有完全复制的条目。
  • 我们知道,如果有节点失效,它可以通过复制协议从其他对等节点复制数据,从而重新加入到集群中。
  • 我们还必须承认,复制因子是一个目标,而不是一个保证,因为提交的头和尾可以是任意长度。

现在让我们来看看BookKeeper。

BookKeeper Properties and Behaviour

BookKeeper对所需的复制因子和commit quorum有类似的配置。

注意:我将假设Ensemble Size等于Write Quorum,因为分散化写入降低了读取性能,不推荐在实践中使用。

Write Quorum是我们的复制因子,Ack Quorum是我们的commit quorum。一般简单地将Ack Quorum设置为大多数,因此当Write Quorum为3时,Ack Quorum设置为2。可以合理地预期,使用WQ=3和AQ=2的quorum值将转化为与Raft或Kafka相同的行为。

但是WQ和AQ没有映射到Raft或Kafka中它们的等价物上,要理解原因,我们需要更仔细地研究该协议及其外部共识和动态成员。

External, Stateless Client

复制和共识逻辑存在于客户端中。客户端是无状态的,它不能在bookie恢复可用之前在内存中保留任意长度的数据。因此它保持了灵活性,只需选择一个新的bookie来取代无法写入的bookie,就能持续正常工作。这种动态的成员变化称为ensemble change。

图6 在bookie3写入失败后,客户端执行ensemble change

这个ensemble change操作主要是更新ZooKeeper中的ledger元数据,以及将所有未提交的条目重新发送给新的bookie。

这些ensemble change的结果是,ledger可以被视为一系列小日志(我们称之为fragment),它们构成了一个更大的日志。每个fragment都有一系列连续的条目,其中每个条目共享相同的bookie集合(ensemble)。每次向bookie的写入失败时,客户都会进行ensemble change并继续,从而创建由一个或多个fragment组成的ledger。

图7 由4个fragment组成的ledger

如果我们查看每个单独的fragment,我们会看到类似于Raft log或Kafka主题分区的模式。当前fragment可以分为类似的三个区域:提交尾部、提交头部和未提交区域。

图8 active fragment的三个安全区域

当发生ensemble change时,当前fragment终止于提交头的头部(已达到Ack Quorum的条目)。新fragment从未提交区域的开头开始。

图9 ensemble change将未提交的条目移动到下一个fragment

注意上图example 1和example 2的区别。在example 1中,fragment 2中的消息9、10是原来写入b1的未确认消息,它们只需要重新复制到b2和b4;而在example 2中,fragment 2中的消息9、10是两条全新的消息,原来写入b1的消息已经丢失。

并且ensemble change过程不影响客户端继续写入数据,因为能满足AQ。

这可能会使非活动fragment中的条目副本数为Ack Quorum(即未完全复制)。与Raft或Kafka不同,BookKeeper复制协议最终不会复制这些AQ条目以达到WQ——它们将保持Ack Quorum。这些条目只能通过使用单独的恢复过程达到WQ,但该过程不是协议的核心部分(如果开启,默认情况下每天运行一次)。

这意味着ledger可以如下所示:

图10 Ensemble changes只会将未提交的条目移动到下一个片段中,将提交的条目保留在其原始片段中

这意味着,不是只有ledger的最新部分满足AQ个数的副本,还可以在求他部分看到较低复制系数的AQ个副本。

图11 Ensemble changes将AQ复制块保留在ledger中间

Ledger中间的部分可以只有AQ个副本,这一事实让许多人感到惊讶。大多数人可能会期待类似Raft/Kafka的模式,即只在日志头部才存在未完全复制的条目。

需要注意的是,Raft和Kafka日志可以具有任意长的提交头,其中条目只达到commit quorum,而没有达到replication factor(例如某个节点长时间宕机)。所以,无论你是Kafka的管理员还是BookKeeper管理员,事实上,commit quorum才是最重要的。

Ack Quorum Isn’t What You Probably Think It is

BookKeeper使用外部复制器(客户端)这一事实对我们选择commit quorum有很大影响。本质上,Ack Quorum与Raft和Kafka中的commit quorum并不完全相同。

如前所述,由于Raft和Kafka的成员是固定的,因此它们确实需要一个低于复制因子的commit quorum,否则会遇到很大的可用性和延迟问题。Commit quorum是安全性和可用性/延迟之间的折衷。

BookKeeper ledger则不同,它没有固定的成员。如果一台bookie不可用,我们就把它换成另一台,然后继续。这使得Ack Quorum不等于Raft的多数仲裁或Kafka配置的仲裁。

使用BookKeeper,我们可以将commit quorum设置为等于复制因子,即WQ=AQ。如果我们设置WQ=3,AQ=3,并且有一台bookie宕机,我们就选择一台新的bookie接替老的bookie继续服务。请注意,当WQ=AQ时,我们没有提交头/尾和未提交这三个区域。条目要么被提交(完全复制),要么未被提交。

图12 如果WQ=AQ,则要么完全复制条目,要么不提交条目,Ensemble changes使原始片段处于完全复制状态

这也意味着我们在ledger的中间不再有复制系数更低的部分。

这对数据安全性而言是个很好的特性。BookKeeper不需要多数法定人数来提供高可用性,我们可以让BookKeeper只确认完全复制的条目。

当然,在将AQ从majority quorum切换到replication factor之前,需要考虑一些限制和影响。

首先,只有存在足够多bookie时,使用WQ=AQ而不损失可用性才适用。如果集群只有3台bookie,并且使用WQ=3,那么成员就是固定的,和Raft一样。如果集群拥有4台bookie,那么一旦一台bookie失效,bookie个数减少到3台,再次变成固定成员。所以你可能需要的bookie个数远超过3台,然后选择更小更多的bookie ensemble集合,而不是更大更少的bookie集合。如果你有5台或更少的bookie,你可能需要更多的可调节空间来满足AQ<WQ。

当使用WQ=AQ时,可用性确实会受到小的影响,因为可用性现在也取决于操作ZooKeeper是否成功。一旦写bookie失败了,我们就必须能够完成一次ensemble change,以保证服务恢复并且条目得到确认。

然而,我认为我们已经在那条船上了。Ledger是小并且有界的日志,不同于Raft和Kafka理论上的无限日志。Ledger充当日志段的角色,因此它们会不断被创建和关闭,这需要成功的元数据操作,因此在任何情况下,如果元数据不能更改成功,就无法长时间正常工作。

写入延迟将有更多的差异,因为ensemble changes将导致更多的写入延迟。ensemble changes通常非常快,但如果ZooKeeper负载较高时,则缓慢的ensemble changes可能会导致写入延迟峰值。因此,如果保持恒定的低延迟非常重要,那么你可能会希望坚持AQ等于majority quorum。

不同的WQ和AQ的选择对数据安全性和可用性的影响:

  1. WQ > AQ:一台bookie挂了,ensemble change过程中不会影响可用性,客户端能继续写入,因为可以保证AQ台bookie正常工作,但数据安全性会受到影响,ensemble change可能在日志中间产生未完全复制的条目。
  2. WQ = AQ:一台bookie挂了,ensemble change过程中客户端无法继续写入,短暂的ensemble change也会影响写入延迟,可用性收到影响,但数据安全性更高,日志中间不会出现未完全复制的条目。

Replication Factor of 2

为什么我们不能有两个成员的Raft集群?因为单个节点的故障会使集群无法正常工作。我们仍然有冗余,但可用性比单个节点差。同样,对于Kafka,我们可以选择复制因子为1,也可以选择复制因子为3,但不能选择复制系数因子为2。如果选择复制因子为2,需要将min-insync-replicas设置为2,当一个副本失效时,我们会面临和Raft一样的问题。

但是对于BookKeeper,我们可以使用复制因子2而不会出现问题。我们只需设置WQ=2和AQ=2。我们可以获得冗余,并且在单个节点发生故障时也不会失去可用性。

设置WQ=AQ=2,并且保证可用性的前提应该是整个集群中bookie数量大于2。

Summary

在第一篇文章中,我们重点讨论了BookKeeper的外部共识协议和动态ledger成员集合,以及与更传统的完全集成协议(如Raft和Apache Kafka)的对比,这些协议具有固定的成员集合。

我们已经看到,BookKeeper的动态成员集合使得它避免了在安全性和可用性/延迟之间的妥协。如果Raft的保守配置可能会选择5的复制系数,以确保它能够在失去2个节点的情况下不丢数据,但是使用BookKeeper,我们可以在复制系数仅为3的情况下获得类似的结果(一共5个节点,即ensemble size=5)。我们甚至可以选择WQ=4,AQ=3,以减少缓慢的ensemble change带来的额外延迟。在设置Write Quorum和Ack Quorum时,您的自由度比您想象的要大一些。

我们还看到,当AQ<WQ时,您的ledger中间位置可能有一些块,这些块只能达到AQ个副本,这可能会让人们感到惊讶。在后面的帖子中,我们将研究可能改变这种行为的协议的潜在调整,以及为什么这种做法可能不值得,甚至是不安全的做法。

这绝不是BookKeeper区别于Raft和Kafka等integrated protocols的全部。在详细了解BookKeeper复制协议时,还有很多事情需要考虑。

在下一篇文章中,我们将研究BookKeeper复制协议的另一个方面,这是由于其外部共识算法的特性所造成的:处理客户端故障以及正确关闭ledger。

最后,和所有事情一样,这都是关于权衡的。Integrated protocols和BookKeeper做出了不同的权衡,两者不能说谁比谁更好,本篇文章甚至本系列文章都没有试图进行这种对比。

标签:AQ,Dynamic,条目,Consensus,复制,Raft,BookKeeper,Kafka
From: https://www.cnblogs.com/oyld/p/16885527.html

相关文章