重谈分布式一致性:zk和kafka中的副本

这篇文章翻译自:https://www.confluent.fr/blog/distributed-consensus-reloaded-apache-zookeeper-and-replication-in-kafka/,有兴趣的可以看原文,由于水平有限翻译过程中难免有些错误,请大家指正,一定要多拍拍。

这篇文章是由Apache Kafka的共同创建者Neha Narkhede和Apache ZooKeeper的共同创建者Flavio Junqueira共同撰写的。

目前,我们构建和使用的许多分布式系统都依赖于Apache ZooKeeper,Consul以及etcd,甚至依赖于Raft的自制版本。尽管这些系统在其公开的功能上有所不同,但是核心是相同的并且所有分布式系统必须解决的:共识(agreement)。分布式系统中的进程需要和master达成共识,和group中的其他成员达成共识,和锁的拥有者达成共识,和配置达成共识,等等。这些都是分布式系统设计中常见的问题,使用Apache ZooKeeper,Consul以及etcd等中的任何一个都是成功的,这些从根本上解决了分布式共识问题。使用其中的任何一致性解决方案,都可以使分布式系统可以以更有效的方式协调分布式系统中的各个进程。例如:管理kakfa中的副本。在这篇文章中,我们将重点集中在此类系统通常如何公开共识以及在Apache Kafka的复制方案中的典型应用。

以解决共识为核心的服务,被称为“共识服务”。但是,“共识服务”这个名字可能是一个糟糕的选择,因为这些服务实际上都没有公开显式解决共识的方法。如果为我们提供了锁服务,那么我们希望API提供获取和释放锁的功能。但是,我们所讨论的服务并未公开的共识的API,因此将其称为“共识服务”会产生误导。

如果是这样,那么为什么人们将其称为共识服务?主要是因为服务通常用于达成某些协议:和锁,和主服务器,和配置等。在ZooKeeper的上下文中,我们选择将其称为协调内核[3] [4]。名称的依据如下,该服务本身公开了类似于文件系统的API,以便客户端可以操纵简单的数据文件(znode)。这些文件可以具有一些特殊的属性,例如临时的或顺序的,但它们都是小型数据文件。我们想到了诸如文件系统,数据库,键值存储之类的术语,但由于以下原因,它们并不完全适合:

  • 它并不是真正的文件系统,因为它没有提供典型文件系统所具有的所有属性(例如,部分读取和写入文件)。
  • 它并不是真正的数据库,因为它实际上并不处理批量数据,也没有复杂的运算符来操纵数据。
  • 它实际上不是键值存储,因为它比典型的键值存储提供的功能(例如,订购,层次结构)还多。
  • 它并不是真正的锁服务,因为API不会直接公开锁。

因此,我们决定根据它用来干什么而不是能干什么来命名它,所以“协调”这个称呼很合适。我们还决定将其称为内核,因为API像分布式锁一样对原语进行了实现,
但是像Chubby系统一样没有直接暴露原语[5]。内核公开了少量功能,足以实现主选举,锁定,成员管理等。

然而,共识问题对于理解类似ZooKeeper这样的系统如何工作以及可以提供什么确实是至关重要的。这个名字的麻烦部分在于,ZooKeeper不是一个盒子,您可以简单地问“我们达成了什么?”。比这更复杂的是,这篇文章的目的是阐明共识问题在分布式系统中的表现方式和一些注意事项。作为本练习的一部分,我们将讨论共识是如何在Apache Kafka复制方案中体现的以及如何利用ZooKeeper简化其操作。但是首先,需要有一些共识的背景知识。

共识简短背景

在分布式系统设计的上下文中,共识通常被宽松地用来表示某种形式的协议。但是,分布式共识问题是一个定义明确的问题。如Fischer,Lynch和Paterson [2,6]在著名论文中所描述的,共识协议是具有n个进程的系统,因此每个进程都有一个初始值和一个输出值,一旦设置,该值将不再更改。一旦进程确定了其输出值。进程确定了其输出值,我们就说进程已经决定,而进程一旦决定,就无法改变。

这个定义难道不是很狭义吗?为什么进程无法更改其决策值?这种一致性的要求有点像事物提交[2]。如果某个进程提交

在分布式系统设计的上下文中,共识通常被宽松地用来表示某种形式的协议。但是,分布式共识问题是一个定义明确的问题。如Fischer,Lynch和Paterson [2,6]在著名论文中所描述的,共识协议具有n个进程的系统,每个进程都有一个初始值和一个输出值,一旦设置,该值将不再更改。一旦了自己的一部分并随后决定中止,则可能会造成一些问题,因为该提交可能会产生外部影响(例如,客户已经提取了1,000,000美元)。因此,我们假设决策值一旦设置就无法更改。

从解决方案到达成共识,我们希望有一些特性:

  • 协议:我们希望所有不会崩溃的进程都同意相同的值
  • 有效性:该值必须是进程之一建议的值(否则,我们可以简单地决定中止并完成)
  • 终止:我们希望任何共识的执行都会终止。如果协议永不终止,则进程会毫无意义的商定同一件还未决定事。正如FLP不可能结果所示,这个终止属性实际上是破坏异步系统中共识的原因。

这样的共识协议与ZooKeeper这样的系统并不完全相同。事实证明,还有一个更酷的问题被证明等同于共识,而这正是ZooKeeper所实现的(实际上是该问题的变体,请参见[7]),这个问题就是原子广播[8]。

原子广播包括确保进程以相同的顺序(总顺序)传递相同的消息(协议)。这个属性对于复制类系统而言确实是很重要,因为如果我的消息是命令并且利用原子广播实现,那么我可以使用它向所有的副本广播命令,所有副本将以相同的顺序接收所有命令,并以收到的顺序执行命令。如果命令是确定性的,则可以确保所有副本之间的状态始终保持一致。这种观察是复制状态机的本质[9]。

ZooKeeper实际上并不广播命令,而是广播状态更新。使用状态更新是一种将客户端提交的命令转换为幂等事务的方法。例如,可以有条件地更新znode,并且在使用setData请求更新znode时,版本会自动增加。要将调用转换为幂等事务,我们需要计算新版本并传播znode的新状态。更具体一点,这是请求和相应状态更新的简化版本:

        <setData, path, data, expected version>  // setData request
        <path, new data, new version>            // corresponding txn

这在本文章之外进行讨论很有用,这在Zab的工作[6]中讨论了更多细节。

关于共识和原子广播,让我们通过一个简单的论点来了解为什么它们是等效的。使用共识协议实现,进程可以运行一系列共识实例以实现原子广播。每个共识实例的输入值是一组要广播的值。由于进程运行一致,因此它们在每个实例中传递相同的消息集。另一个方向也很简单。如果给我们一个原子广播实现,那么为了获得共识实现,每个进程都只是通过广播该值来提出一个值。进程传递的第一条消息包含决策值,该决策值对于所有进程都是相同的。

ZooKeeper和共识

在推理ZooKeeper时,考虑原子广播比共识更有意义。但是等等,它们不是等效的吗?是的,从简化的角度来看,它们是,但是它们仍然呈现不同的语义。下边一个错误的推理如何导致问题的简单示例。

假设我们有一个包含三个客户端的系统,一个配置器(C)和两个工作器(W1和W2)。配置器C告诉工作器应该消费的信息,并期望所有工作器都消费相同的信息。协调员选择的信息会随时间变化而变化,并且协调员通过ZooKeeper以容错的方式将其选择传达给工作人员。现在考虑以下步骤序列:

  1. C将香草写入ZooKeeper
  2. W1读香草
  3. C之后立即将巧克力写入ZooKeeper
  4. W2读巧克力,W1已经在食用香草

显然,由于他们读取了不同的值,工作器会消费不同的信息,但是到底出了什么问题呢?服务不应该让我们达成共识吗?事实证明,ZooKeeper提供的共识是对ZooKeeper状态更新的统一视图。如果两个或更多客户端能够观察到ZooKeeper服务的所有更新,则它们观察到以相同顺序应用的相同更新,但不一定同时。因此,它提供的那种协议不能与始终遵守相同状态混淆。保持一致并不意味着读取的值必须相同。

解决此问题的一种方法是使用序列化更新,以使工作器就该值达成共识,本质上是像我们之前讨论的那样,使用原子广播实现共识。 W1读取一个值后,就可以建议它们消耗香草,因为配置程序已建议这样做。 W2的功能相同,但是由于他们提出了不同的值,因此需要打破平局。他们可以通过选择使用ZooKeeper顺序节点写入的第一个值来做到这一点。

与原始提案相比,该提案为何有效?因为每个工作器都“提出”了一个单一的价值,而且这些价值不会发生变化。随着时间的推移,配置器更改值,他们可以通过运行此简单协议的独立实例来商定不同的值。

请注意,这种情况只是为了说明围绕协议的推理和提供该协议的服务不正确时的问题。解决问题的真正方法将取决于应用程序的精确语义,并且会有多种实现方法。

通过服务达成共识

共识在分布式系统中起着重要作用,使用Apache ZooKeeper之类的服务可以使复制的某些方面更加简单。为了使论点非常具体,我们在这里集中讨论Apache Kafka的复制方案。 Kafka使用Apache ZooKeeper来存储元数据。此元数据有多种用途—持久保留组成topic的brokers,从为其数据提供写服务的副本中选择一个leader,并保留了一些子节点的信息。

在这里我们仅介绍了足以使该参数易于理解的内容。 Kafka暴露了topic的抽象:客户通过topic进行消息生成和消费。为了提高并发的性能,主题被进一步划分为partitions。partitions是Kafka中并行性和复制的基本单位。在分区的一组副本中,有一个会被选为领导者(使用ZooKeeper),其余为跟随者。领导负责分区的所有读取和写入。 Kafka还具有同步副本(ISR)的概念:副本的子集当前处于活动状态,并且已被领导者追上,也就是说leader和副本没有相差太多。

ISR动态更改,并且每次更改时,该集的新成员资格都将保留到ZooKeeper。 ISR有两个重要目的。首先,在领导者宣布它们已提交之前,此集合需要确认所有写入分区的记录。因此,ISR集必须至少包含f + 1个副本才能承受f次崩溃,并且f + 1所需的值由配置设置。其次,由于ISR拥有分区中以前提交的所有消息,为了保持一致性,新领导者必须来自最新的ISR。从最新的ISR中选出一位领导者对于确保在领导者过渡期间不会丢失任何已落实的消息来说很重要。当副本发生故障时,会将其从ISR中删除。当副本在崩溃后重新启动时,它会被告知当前的领导者和ISR(通过从ZooKeeper中读取),然后通过从当前领导者中拉出来同步其数据,直到其赶上并足以成为ISR的一部分为止。在这种复制方案中,ZooKeeper只处理复制的元数据(分区信息和ISR成员资格),实际数据的复制留给应用程序(Kafka)来考虑。

就持久性而言,Kafka的复制协议和ZooKeeper的复制协议之间还有一个重要区别。由于Kafka依赖ZooKeeper成为元数据的“真相之源”,因此ZooKeeper必须提供强大的持久性保证。因此,它只有在将数据同步到ZooKeeper仲裁服务器的磁盘后才会确认写请求。由于Kafka的brokers需要处理的数据量很大,因此无法承受做到这样,也无法将分区数据同步到磁盘。分区的消息将写入相应的文件,但是不会调用fsync/fdatasync,这意味着数据在写入后仍保留在操作系统页面高速缓存中,并不一定要刷新到磁盘介质中。这种设计选择会对性能产生巨大的积极影响,但是具有一个副作用,即正在恢复的副本可能没有先前确认的某些消息。

为什么选择ISR而不是多数仲裁的方式

在复制系统中使用仲裁的一个很大的优势是能够掩盖崩溃。来自足够大的副本子集(例如多数)的投票就足以提交。在以2f + 1方式复制的基于仲裁的系统中,如果有任何f个副本崩溃,则该系统仍可以透明地进行进度(可能会有一些小的问题)。每个写入操作都将到达所有副本,但是只有多数仲裁的响应才需要提交写入。查看下图:
在这里插入图片描述
多数仲裁的一个重要缺点是要求至少(n +1)/ 2个确认,所以节点的数量会随着n的增加而增加。这种仲裁系统方案可与ZooKeeper之类的系统很好地协作,因为它处理元数据:体积小且写入很少,通常常见的操作不在关键路径中。

Kafka的ISR方案不使用上边的多数仲裁的方式,而是要求当前ISR的所有成员做出响应:
在这里插入图片描述
为了提交写操作,ISR中的所有副本都必须以确认响应,而不仅仅是大多数响应。与经典仲裁方式不同,ISR的大小与副本集的大小脱钩,这为副本集的配置提供了更大的灵活性。例如,我们可以有11个副本,其最小ISR大小为3(f = 2)。对于大多数仲裁,具有11个副本将必然意味着大小为6的副本。

请记住,最小ISR的大小与系统提供的持久性保证直接相关。由于可以通过配置来调整对ISR最小大小的限制,ISR持久性保证类似于ZooKeeper提供的一种保证,即如果失败的副本数低于预期的仲裁大小,则不会进行写操作。如果系统不能保证可以执行新写入,则它在可用性上权衡了可用性,在持久性方面不接受新写入。在这方面,Kafka的复制方案非常灵活。通过让ISR的最小大小可配置,它使主题可以在可用性和耐用性之间进行权衡,反之亦然,而仲裁不需要包含大多数副本。

尽管ISR方案具有更高的可伸缩性并可以容忍更多的故障,但它对副本的某些子集(ISR)的性能也更加敏感。如果基于多数仲裁的方案仅忽略了最慢的副本,则该方案将暂停对分区的所有写入操作,直到最慢的副本(如果它是ISR的一部分)被删除。在大多数故障模式下,副本会被快速删除。对于软故障,在一定的超时后将删除无响应的副本。同样,如果慢速副本落后于配置所定义的值以后,它们也会被删除。

对于像Apache Kafka这样的数据系统,其复制方案的这种灵活性具有对持久性保证提供更细粒度控制的优点,事实证明,这在生产中存储大量数据时非常有用。作为另一个数据点,像Apache BookKeeper这样的系统也使用了类似的方案,其中副本和确认的数量是独立的。但是,BookKeeper不能像Kafka一样提供ISR。

有趣的是,该方案与PacificA的工作[10]中描述的方案很接近。在PacificA的工作中,该框架将副本组的配置管理与实际数据复制分开,就像Kafka一样。它还与Cheap Paxos共享一些属性[11]。使用Cheap Paxos,数据副本仅发送到f + 1个副本,而在Kafka中,数据副本发送到所有副本(以保持副本有效,而不是为了确保正确性)。但是,这两种协议都建议保持f +1副本的固定子集为最新,并在怀疑崩溃时重新配置该集合。 Cheap Paxos从协议内部执行重新配置,而Kafka依赖于外部复制状态机(ZooKeeper)。

所有这一切的共识在哪里?

在Kafka复制协议中,ISR更新协议掩盖了共识。由于ZooKeeper最终通过将ISR信息存储在ZooKeeper中而最终公开了一个原子广播原语,因此从本质上来说,可以保证对ISR更改的继承达成一致。当副本在崩溃后恢复时,它可以转到ZooKeeper,查找最新的分区元数据(领导者,ISR),并进行相应的同步以获取最新的提交消息。由于所有副本(通过ZooKeeper)都同意最新的ISR是什么,因此不可能出现裂脑情况。

让我们来看一些场景,以更好地了解其工作原理。假设我们有一个分区{A,B,C,D,E}的5个副本,并且ISR集最初包含所有副本。下图说明了这种情况
在这里插入图片描述
在某个时候,副本E崩溃。
在这里插入图片描述
一旦开始恢复过程,它将从ZooKeeper中读取分区的状态,并得知A是当前ISR的领导者。
在这里插入图片描述
完成同步后,将其添加回ISR。
在这里插入图片描述
如果不存在ZooKeeper,则副本E将需要与其他副本进行对话,以找出哪个副本进行同步。无法查询像ZooKeeper这样的“真相之源”,查询剩余那些节点会带来各种各样的麻烦,因为副本可能会崩溃或被分割掉,这是在没有ZooKeeper这样的依赖的情况下进行仲裁(多数选择)的关键原因。即使使用ZooKeeper,也可能需要注意一些竞争条件。

发布了223 篇原创文章 · 获赞 308 · 访问量 84万+

猜你喜欢

转载自blog.csdn.net/maoyeqiu/article/details/103243183
今日推荐