Kafka 架构简述

264 阅读13分钟

Kafka 简述

Kafka 是一个消息传递系统。仅此而已。那为什么它热度这么高呢?因为在现实中,消息传递系统是在系统之间移动数据时的一个非常重要的基础设施。我们以一个没有消息传递系统的数据管道为例来解释这一点。

这个系统从 Hadoop 开始,用于存储和数据处理。没有足够的数据时,Hadoop 就没有什么用处,所以使用 Hadoop 的第一阶段是获取数据。

data-in.png

到目前为止,没有出现什么大问题。不幸的是,在现实世界中,数据同时存在于许多的并行的系统,所有这些系统都需要与 Hadoop 交互,这些系统之间同样也要进行数据交互。情况很快变得非常复杂,最终形成的系统中,每个数据系统通过许多独立的通道和其他数据系统实现通信。每个通道都需要自己定制协议和通信方式,如此一来产生的巨大工作量,使得光是这些系统之间的数据移动成为了开发团队每天的工作。

moving-data.png

让我们再来看看这幅图,用 Kafka 作为中央消息总线。总线上,所有输入数据首先放在 Kafka 中,所有输出数据都从 Kafka 读取。Kafka 集中了数据生产者和数据消费者之间的通信。

kafka-organization.png

什么是 Kafka?

Kafka 是一个分布式信息传递系统,通过 pub-sub(订阅-发布) 模式提供快速、高度的可扩展性和冗余的消息传递。Kafka 的分布式设计为它提供了不少的优势。首先,Kafka 允许大量永久性或临时性的消费者接入。其次,Kafka 具有高可用性,和对节点故障的弹性,支持节点的自动恢复。在现实中,这些特性使得 Kafka 成为了大规模数据系统组件间进行通信和信息整合的理想选择。

Kafka 术语

Kafka 的基础架构是围绕几个关键术语组织的:topics(主题), producers(生产者), consumers(消费者), 和 brokers(代理)。

所有的 Kafka 消息都按照 主题 进行组织。如果你想发送一条消息,你就把它发送到一个特定的主题,如果你想读取一条消息,你得从特定的主题中进行读取。消费者从 Kafka 主题中提取消息,而生产者则将消息推送到 Kafka 主题中。最后,Kafka 作为一个分布式系统,运行在一个集群上。集群上的每一个节点都被称为代理。

Kafka 的 主题 剖析

Kafka 的每个主题被划分为若干个 partition(分区)。分区允许你通过将特定主题中的数据分割给多个代理来实现主题的并行化——每个分区都可以放在单独的机器上,这样可以允许多个消费者从一个主题中并行读取。消费者也可以被并行化,以便多个消费者可以从一个主题的多个分区中读取数据,从而实现非常大的消息处理吞吐量。

分区中的每条信息都有一个标识符,称为 offset(偏移量)。偏移量是消息的排序,是一个不可改变的序列。Kafka 会维护消息的先后次序。消费者可以从特定的唯一开始读取消息,也可以从他们选择的任意偏移点读取,并且允许消费者在它们认为合适的任何时间点加入集群。鉴于这些限制,Kafka 集群中的每个特定消息都可以都可以由一个元组来唯一识别,该元组包含消息的主题、分区和分区中的偏移量。

log-anatomy.png

另一种看待分区的方式是作为一个日志。一个数据源将信息写入日志,一个或多个消费者在他们选择的时间点从日志中读取信息。在下图中,一个数据源正在向日志写入,消费者 A 和 B 从日志的不同偏移点中读取。

data-log.png

Kafka 在可配置的时间段内保存信息,可以由消费者来相应地调整它们的行为。例如,如果 Kafka 被配置为保留消息一天,而一个消费者停机超过一天,那么这个消费者就会丢失消息。然而如果消费者仅仅瘫痪了一个小时,它可以从最后一个已知的偏移量开始重新读取消息。从 Kafka 的角度来看,他没有保存消费者在一个主题中读取信息的状态。

分区和代理

每个代理都持有若干个分区,每个分区可以是一个主题的 leader(领导者)或是 replica(副本)。对于一个主题的所有读和写都要通过领导者来处理,由领导者来协调和更新其他副本中的数据。如果领导者故障了,它的一个副本将会作为新的领导者接管它的工作。

partitions.png

生产者

生产者写到一个领导者中,这提供了一种负载均衡生产手段,以便每个写操作都可以由一个单独的代理和机器来服务。在第一张图片中,生产者正在向主题的第 0 分区写入,第 0 分区的领导者将该写入复制到其可用的副本之中。

producing-to-partitions.png

在第二张图片中,生产者正在向主题的第 1 分区写入,第 1 分区的领导者将该写入复制到其可用的副本之中。

producing-to-second-partition.png

由于每台机器分别负责特定分区,并可以同时处理写入操作,整个系统的吞吐量就会增加。

消费者和消费者组

消费者可以从任意一个分区进行读取,这允许你以类似于消息生产的方式来进行消息的读取,提升消费者的吞吐量。消费者也可以为一个给定的主题组织成消费者组——组内的每个消费者都从一个特定的分区中读取消息,组作为一个整体来消费整个主题中所有分区的消息。如果你的消费者比分区多,那么一些消费者将被闲置,因为它们没有可以读取的分区。如果你的分区比消费者多,那么一个消费者就将从多个分区中接收消息。如果你有相同数量的消费者和分区,每个消费者都会按顺序从自己指定的那一个分区中读取消息。

下图来自 Kafka 文档,描述了一个主题的多个分区情况。服务器1持有分区0和分区3,服务器2持有分区1和分区2.我们有两个消费者组,A 由两个消费者组成,B由四个消费者组成。消费者组A的两个消费者有四个分区可以消费——每个消费者从两个分区中读取。另一方面,消费者组B有与分区数相同数量消费者,每个消费者正好从一个分区中读取消息。

consumer-groups.png

一致性和可用性

在开始讨论一致性和可用性之前,请记住,只要你向一个分区生产,从一个分区消费,这些一致性和可用性的保证就会成立。如果你使用两个消费者从同一个分区读取数据,或者使用两个生产者向同一个分区写入数据,那么所有的保证将会失效。

Kafka 对数据的一致性和可用性做出了如下的保证:

  1. 发送到主题分区的消息将按照发送的顺序追加到提交日志中
  2. 单个消费者实例将按照日志中出现的顺序看到消息
  3. 当所有同步副本将消息应用到它们的日志中时,该消息就被“提交”了
  4. 只要至少有一个副本还活着,任何提交的消息就不会丢失

第一和第二项保证确保了每个分区的消息排序得到保留。值得注意的是,整个主题的消息顺序是不被保证的。第三和第四项保证确保了已提交的消息可以被检索到。在 Kafka 中,被选为领导者的副本负责将收到的所有消息同步到其他副本。一旦一个副本确认了同步消息,这个副本就被认为是完成同步的。为了进一步理解这一点,让我们仔细看看在写的过程中会发生什么。

处理写入

当与 Kafka 集群进行通信时,所有的消息都会被发送到分区的领导者那里。领导者负责将消息写入自己的副本中。一旦消息被提交,领导者就负责将消息传播到不同代理的其他副本。每个副本都确认它们已经接收到消息,此时的状态可以称之为同步。

领导者写入副本

当集群中的每个代理都是可用时,消费者和生产者可以愉快地从每个主题的不同分区的领导者那里进行读写,这没有任何问题。不幸的是,领导者和副本都可能会故障,而我们需要处理这些情况。

故障处理

当一个副本故障时会发生什么?写操作将不会到达失败的副本,它将不再接收消息,慢慢和领导者失去同步。在下面的图片中,副本3不再接收来自领导者的消息。

first-failed-replica.png

紧接着当第二个副本故障时会发生什么?同样的,第二个副本将不再接收消息,它也会变得与领导者不同步。

second-failed-replica.png

在这个时间点上,只有领导者时同步的。用 Kafka 的术语来说,我们仍然有一个同步的副本,尽管这个副本正好时这个分区的领导者。

如果领导者也故障了会怎样?我们就会只剩下了三个故障的复制品了。

third-failed-replica.png

副本1实际上还是同步的——它不能接收仍和新数据,但它于所有可能接收的数据是同步的。副本2丢失了一些数据,而副本3(第一个倒下的)甚至丢失了更多的数据。鉴于这种状态,有两种可能的解决方案。

第一种,也是最简单的一种,是等待,直到领导者恢复正常后再继续提供服务。一旦领导者恢复了,它就会立即开始接收和写入消息,随着副本的重新上线,它们将重新和领导者同步。

第二种方法是选举第二个代理作为新的领导者重新启动。新代理将和现有的代理不同步,从新代理宕机到它被选为新领导者之间的所有数据都将丢失。当其他代理恢复时,它们将看到它们已提交的消息在领导者上不存在,并放弃这些消息。通过尽快选举出一个可用的新领导者,尽管消息可能被丢弃,但我们将最大限度地减少停机的时间,因为任何新机器都可以成为领导者。

退一步讲,我们可以看到这样一种情况:领导者倒下了,而同步的副本仍然存在。

leader-fails.png

在这种情况下,Kafka 控制器将检测到领导者的缺失,并从同步的副本池中选出一个新的领导者。这可能会花费几分钟,并导致客户端出现 LeaderNotAvailable错误。然而,只要生产者和消费者处理这种可能性并适当地进行重试,就不会发生数据丢失。

Kafka 客户端的一致性

Kafka 的客户端有两种类型:生产者和消费者。每个客户端都可以配置不同的一致性级别。

对于生产者,我们有三种选择。在每个消息上,我们可以

  1. 等待所有同步的副本确认该消息
  2. 只等待领导者确认该消息
  3. 不等待确认。

每种方法都有其优点和缺点,需要由系统设计者根据一致性和吞吐量等因素来决定适合他们系统的策略。

对于消费者,我们只能读取已提交的消息(即那些已被写入所有同步副本的消息)。鉴于此,作为消费者,我们有三种提供一致性的方法,每种方法都值得单独讨论:

  1. 每个消息最多接收一次
  2. 每个消息至少接收一次
  3. 每个消息正好接收一次

对于最多以此的消息传递,消费者从一个分区中读取数据,提交它所读取的偏移量,然后处理消息。如果消费者在提交偏移量和处理消息之间崩溃了,重启后它将从下一个偏移量重新开始,而不曾处理过该消息。这将导致潜在的、我们不愿看到的消息丢失。

一个更好的选择是至少一次的消息传递。对于至少一次的消息传递,消费者从一个分区读取数据,处理消息,然后才提交它所处理的消息的偏移量。在这种情况下,消费者在处理消息和提交偏移量之间可能会崩溃,当消费者重新启动时,它将再次从头开始处理消息。这导致消费者下游系统中出现重复的消息,但没有数据丢失。

通过让消费者处理一个消息并将消息的输出和偏移量提交给事务系统,保证了精确的一次接收。如果消费者崩溃了,它可以重新读取最后提交的事务,并从那里恢复处理。这实现了无数据损失和无数据重复。然而,在实践中,由于每个消息和偏移量都是作为一个事务提交的,准确的一次接收意味着系统的吞吐量大大降低。

在实践中,大多数Kafka消费者应用程序选择了至少选择一次接收,这是因为它提供了吞吐量和正确性之间的最佳权衡。这将取决于下游系统要以他们自己的方式处理掉重复的消息。

结论

Kafka 正在迅速成为许多组织的数据管道的骨干——这是有原因的。通过使用 Kafka 作为消息总线,我们在数据生产者和数据消费者之间实现了高度的并行性和解耦性,使我们的架构更加灵活,能够适应更多的变化。这篇文章提供了一个 Kafka 架构的鸟瞰图。从这里开始,请查阅 Kafka 文档。请享受学习 Kafka 的乐趣,并将这一工具投入更多的使用!

翻译自:博客 《Kafka in a Nutshell - Kevin Sookocheff
勘误:译者认为在探究 Kafka 错误处理的部分中,图片存在小错误,图中的 Partition 应为 Broker