由于近期工作要接触一些Kafka的内容,所以周末花了些时间查询了一些相关的资料,总结出一篇Kafka入门简介,方便日后的学习,也希望能给未接触过Kafka的同学带来些帮助。本文内容主要翻译自Kevin Sookocheff的《Kafka in a Nutshell》一文,并在此基础上加入了一些自己的见解,欢迎各位交流指教。

引言

Kafka是一个消息传递系统。 仅此而已,那么为什么大受人们追捧呢? 实际上,消息传递是在系统之间传递数据非常重要的基础架构。 要了解原因,让我们看一下没有消息系统的数据管道是什么样的。

该系统使用Hadoop进行存储和数据处理。 Hadoop离开数据就失去了意义,因此使用Hadoop的第一步是获取数据。

Bringing Data in to Hadoop

到目前为止,貌似没什么大不了的。 不过,在现实世界中,数据可能存在于许多并行的系统中,所有这些系统都需要与Hadoop进行交互,也会彼此进行交互。 这种情况就会很快变得非常复杂,一个系统处理结束后,接着可能会有更多系统通过多条通道相互通信。而且每一个通道都需要有自己的协议和通信方法,在这些系统之间传递数据是一件很复杂的事情。

Moving Data Between Systems

让我们再看一下这张图,使用Kafka作为中央消息传递总线。 所有传入数据首先会放入Kafka中,并且所有传出的数据也都是从Kafka中读取。 Kafka集中了数据生产者和数据消费者之间的通信。

Kafka是什么?

Kafka是一种分布式消息系统,可通过发布-订阅模型提供快速、高度可扩展的冗余消息传递。 Kafka的分布式设计为其带来了许多优势。 首先,Kafka允许大量的永久或临时消费者。 其次,Kafka具有高可用性,可应对节点故障,并支持自动恢复。 在现实世界的数据系统中,这些特性使Kafka非常适合大型数据系统组件之间的通信和集成。

Kafka术语

Kafka的基本体系结构围绕几个关键术语进行组织:TopicsProducersConsumersBrokers

所有Kafka消息均按主题(Topic)区分。 如果希望发送消息,则将其发送到特定Topic,如果希望读取消息,则从特定Topic中读取。 消费者将消息从Kafka主题中取出,而生产者将消息送入Kafka Topic中,Kafka作为分布式系统在集群中运行。 集群中的每个节点都称为Kafka Broker

那么为什么要将Kafka消息划分为不同主题呢?

我们可以理解为Kafka就是一个消息队列,生产者会将新产生的消息存放到该队列中,消费者从队列中读取消息。但Kafka提供的是持久性的存储(存储时间可配置),不会因某一个消费者读取过数据,就将此数据丢弃,各个消费者会根据自身的Offset进行特定位置的读取,使彼此互不干扰,下节中会对此做出讲解。对于不同的消费者,可能需求的消息类型是不一样的,如果将所有类型的消息都放在同一个队列中,那么各个消费者就会出现读取慢、吞吐量低等缺点,所以Topic就是将不同种类的消息进行分类,存储在不同的“队列”中,提高读写速度。

Kafka Topic简介

Kafka中消息是以Topic进行分类的,生产者生产消息,消费者消费消息,都是面向Topic的。生产者和消费者都连接一个主题,要指定主题是谁,要往哪个主题发送消息,要从哪个主题拉取数据等。

Kafka的主题分为多个分区(Partitions)。 分区允许你将一个主题中的数据进行拆分,存放在多个Broker中,实现主题的并行化。每个分区都可以放置在单独的结点上,从而允许多个使用者并行地从一个主题中读取信息。 使用者也可以并行化,以便多个使用者可以从一个主题的多个分区中读取内容,从而实现非常高的消息处理吞吐量。

分区中的每个消息都有一个被称为偏移量(Offset)的标识符。Kafka会为维护消息进入的顺序, 消费者可以通过设定Offset从特定的位置开始读取消息,也可以将其作为指针根据Offset指定的位置进行随机读取。此项特性允许消费者在他们认为合适的任何时间点加入集群。 Kafka群集中的每条特定的消息都可以由元组唯一标识,该元组由(Topic, Partition, Offset)组成。

Log Anatomy

查看分区的另一种方法是通过日志。 数据源将消息写入日志,一个或多个消费者在他们选择的时间点从日志中读取消息。 在下图中,数据源正在写入日志,而消费者A和B正在以不同的偏移量从日志读取。

Data Log

Kafka会将日志保留一段时间,时间长短是可以人为设定的,可根据不同的业务做出调整。 例如,如果将Kafka配置为保留一天的消息,如果消费者一天没有读取消息,则该消费者将丢失消息。但是,如果消费者一个小时没读取,它可以从其最后一个已知偏移量开始再次读取消息。从Kafka的角度来看,它不会感知消费者对消息的读取状态。

分区(Partitions)和Brokers

每个Broker都有多个分区,每个分区都可以是一个主题的领导者(Leader)或副本(Replica)。 对主题的所有写入和读取都需经过领导者,领导者在有新数据写入时会同步到其他副本。 如果领导者挂掉了,则其它的某一个副本将会成为新的领导者。

Partitions and Brokers

这里需要解释一点,一个Topic可以划分为多个分区,一个Broker中存在多个Topic。Topic是逻辑上的概念,并不实际存在,而Partition是物理上的概念,是实际存在的,每个Partition对应于一个log文件,该log文件中存储的就是Producer生产的数据。Producer生产的数据会被不断追加到该log文件的末端,且每条数据都有自己的Offset。消费者组中每个消费者,都会实时记录自己消费到哪个Offset,以便出错时恢复。

将Topic划分为多个分区的原因在于这样可以提高并发。

生产者(Producers)

生产者会向一个领导者进行写操作,这提供了一种负载平衡的生产方式,以便每次写操作都可以由单独的Broker和机器来进行。 在下图中,生产者正在向Partition 0的Leader结点(Broker 1中)写入消息,接着Broker 1中的分区0将新写入的消息复制到其它可用副本。

Producer writing to partition

在下图中,生产者正在向Partition 1写入消息,Partition 1 向将内容复制到其它副本。

Producer writing to second partition.

由于每台机器都负责各自的写操作,因此整个系统的吞吐量提高了。

消费者(Consumer )和消费者组(Consumer Groups)

消费者可以从任一个分区中读取数据,从而使你可以使消息消费也能像消息生产的方式一样,来扩展吞吐量。消费者也可以按给定主题分为多个消费者组——组中的每个消费者都从不相同分区中读取内容,每个组会将主题的所有分区都覆盖。 如果一个组内的消费者数量多于该主题的分区数,那么多出的消费者将处于空闲状态,因为它们没有要读取的分区。如果分区数比组内消费者数更多,那么一个消费者可以从多个分区接收消息。 如果您具有相同数量的消费者和分区,则每个使用者都从一个分区中按顺序读取消息。

Kafka官方文档中给出的图描述了单个主题的多个分区的情况。 服务器1拥有分区0和3,服务器2拥有分区1和2。我们有两个消费者组A和B。A由两个消费者(C1、C2)组成,而B由四个消费者(C3、C4、C5、C6)组成。 使用者组A使用两个消费者读取4个分区——每个消费者都从两个分区读取。 消费者组B具有与分区相同数量的消费者,每个消费者仅从一个分区读取。

Consumers and Consumer Groups

一致性和可用性

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

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

  1. 发送到主题分区的消息将按照发送顺序附加到提交日志中;
  2. 消费者实例会根据消息加入日志的顺序看到消息;
  3. 生产者写入消息时,当该Topic的所有副本分区都同步数据后,该消息才算“已提交”状态;
  4. 只要有一个副本处于存活状态,该分区下所有已提交的消息都不会丢失。

(1)和(2)确保为每个分区保留消息顺序。 但请注意,这不能保证整个主题的消息顺序。 (3)和(4)确保可以检索已提交的消息。 在Kafka中,被选为领导者的分区负责将收到的所有消息同步到副本。 副本确认消息后,该副本将被视为同步的。 为了进一步理解这一点,让我们仔细研究一下写入过程中发生的情况。

写入处理

与Kafka群集进行通信时,所有消息都会发送到分区的领导者处。 领导者负责将消息写到其自己的同步副本中,一旦提交了该消息,则负责将消息传播到其他Broker上的副本上。 每个副本都确认他们已收到消息,接着就可以进行同步了。

Leader Writes to Replicas

当集群中的每个Broker都可用时,消费者和生产者可以愉快地从主题的主要分区进行读写,而不会出现问题。 不幸的是,领导者或副本都可能会出现异常,我们需要处理每种情况。

异常处理

副本失败时会发生什么? 异常的副本将无法接收消息,也无法写入,与领导者的信息会越来越不同步。 在下图中,副本3不再接收来自领导者的消息。

First Replica Fails

如果第二个副本也失效时会发生什么? 第二个副本也将不再接收消息,并且也将与领导者不同步。

Second Replica Fails

此时,仅领导者处于同步状态。 在Kafka术语中,即使该副本恰好是该分区的领导者,我们仍然有一个同步副本。

那么如果领导者挂掉了怎么办?我们只剩下三个失效的副本了。

Third Replica Fails

副本1实际上仍处于同步状态——它无法接收任何新数据,但与可能接收的所有数据保持同步。副本2丢失了一些数据,副本3(第一个关闭)丢失了更多数据。在这种状态下,有两种可能的解决方案。第一种也是最简单的方案是等待领导者恢复后再继续。领导者恢复后,它将开始接收和写入消息,并且当副本恢复联机后,它们将与领导者同步。第二种方案是选举另一个Broker担任新的领导者。该Broker的数据将与现有的领导者不同步,新领导者离线之后的数据将丢失。随着其它Broker的恢复,可能会看到它们拥有新领导者上不存在的消息,这些消息会被丢弃。通过尽快选出新的领导者会尽可能的降低停机时间,但这有可能会以丢失消息为代价。

举个例子:Replica 1 为领导者,Replica 2,3为副本。在接收过一些消息后,Replica 3 停机,生产者继续发送了10个消息后,Replica 2 接着也停机了,此时只有Replica 1存活,并且Replica 2 比Replica 3 多10条消息,Replica 2 和 Replica 1此时同步,接着,生产者又发送了10条新消息进入Kafka,Replica 1也挂了,所有副本都停机,无法继续使用。这时就有两种方案,第一我们可以等待Leader(Replica 1)恢复后再继续,这样不会丢失任何信息,但是,可能等待时间会很长,如果我们想尽快恢复使用,则可采用方案二,从Replica 2,3 中选出新的领导者,例如选Replica 2为新的领导者,由于Replica 2宕机比Replica 1更早,在此期间生产者已经发送了10条消息,这些消息并没有记录在Replica 2中,所以这些消息将会丢失,当Replica 1,3 陆续上线后,都会与新的领导者Replica 2进行同步,由于原领导者Replica 1之前多接收过10条消息,但这10条消息并没有记录在新领导者(Replica 2)中,所以Replica 1会丢弃这10条消息。

那么让我们设想一下另一种情况,如果领导者掉线而其他副本仍存活时会怎样呢?

Leader Fails

在这种情况下,Kafka控制器将检测到领导者的丢失,并从同步副本池中选出新的领导者。 这可能需要花费几秒钟的时间,此过程中可能会导致客户端出现LeaderNotAvailable错误。 但是,只要生产者和消费者处理此可能性并适当地重试,就不会发生数据丢失。

Kafka客户端一致性

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

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

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

这三种方式各有优缺点,由系统实现者根据一致性和吞吐量等因素决定其系统的适当策略。

在消费者方面,我们只能读取已提交的消息(即已写入所有同步副本的消息)。 鉴于此,Kafka提供了三种作为使用者提供一致性的方法:

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

每个方案都值得单独讨论。

对于最多一次的消息传递,使用者从分区读取数据,提交已读取的偏移量,然后处理消息。 如果使用者在提交偏移量和处理消息之间崩溃,它将从下一个偏移量重新启动,不会再处理当前消息。这将有可能导致数据丢失。

更好的选择是至少一次消息传递。 对于至少一次传递,使用者先从分区读取数据,处理消息,然后提交已处理消息的偏移量。 在这种情况下,使用者可以在处理消息和提交偏移量之间崩溃,并且当使用者重新启动时,它将再次处理该消息。 这导致下游系统中出现重复消息,但不会出现数据丢失。

通过让消费者处理一条消息并将消息的输出以及偏移量同时提交给事务系统,可以保证恰好一次接收就可以了。 如果消费者崩溃了,它可以重新读取上一次提交的事务并从那里恢复处理。 这不会导致数据丢失和数据重复。 但是,实际上,“一次传递”意味着随着每个消息和偏移量作为事务提交而大大降低了系统的吞吐量。

实际上,大多数Kafka消费者应用程序选择“至少接收一次”,因为它在吞吐量和正确性之间提供了最佳平衡。 下游系统将以自己的方式处理重复的消息。

总结

Kafka成为越来越多公司的数据管道骨干。 通过将Kafka用作消息总线,我们可以在数据生产者和数据使用者之间实现高水平的并行化和去耦,从而使我们的体系结构更加灵活并适应变化。 本文提供了Kafka体系结构的鸟瞰图。 在这里,请参阅Kafka文档

参考文章

Kafka in a Nutshell