RabbitMQ vs Kafka:正面交锋

2023年 8月 18日 68.2k 0

本文翻译自国外论坛 medium,原文地址:medium.com/better-prog…,原文作者:Eran Stiller

介绍

作为一名有着大量微服务系统处理经验的软件架构师,我经常遇到一个不断重复的问题:“我应该使用 RabbitMQ 还是 Kafka?”出于某种原因,许多开发人员认为这些技术是可以互换的。虽然在某些情况下确实如此,但 RabbitMQ 还是 Kafka 之间存在根本上的差异。

因此不同的场景需要不同的解决方案,选择错误的方案会严重影响我们的软件开发设计以及后续维护软件。

本文的第一部分将介绍基本的异步消息传递模式以及 RabbitMQ 和 Kafka 的内部结构实现。第二部分重点介绍了这些平台之间的关键区别、它们的各种优点和缺点,以及如何在两者之间进行选择。

第一部分:异步消息传递模式

异步消息传递是一种消息传递方案,其中生产者的消息生成与消费者的消息处理分离。在消息传递系统中,我们通常会分为两种主要的消息传递模式:队列模式和发布/订阅模式。

队列模式

在队列模式中,队列暂时将生产者与消费者解耦。多个生产者可以向同一个队列发送消息。然后当消费者处理消息时,消息会被锁定然后从队列中删除,并且不再可用。

队列模式通常就是一个消息只能被一个消费者处理。

消息队列

附带说明一下,如果消费者无法处理某个消息,消息平台通常会将消息返回到队列,以供其他消费者使用。除了解耦之外,队列还允许我们扩展生产者和消费者,并针对错误处理提供容错能力。

发布/订阅模式

在发布/订阅模式中,单个消息可以由多个订阅者同时接收和处理。

发布/订阅

例如,此模式允许发布者通知所有订阅者系统中发生了某些情况。在 RabbitMQ 中,主题是一种特定类型的 pub/sub 实现(确切地说是一种交换类型),但在本文中,我将主题称为整个 pub/sub 的表示。

一般来说,订阅有两种类型:

  • 临时订阅,其中订阅仅在使用者启动并运行时才有效。一旦消费者关闭,他们的订阅和尚未处理的消息就会丢失。
  • 持久订阅,只要未显式删除,订阅就会得到维护。当消费者关闭时,消息平台会维持订阅,稍后可以恢复消息处理。

RabbitMQ

RabbitMQ 是消息代理的一种实现 — 通常称为服务总线。它本身支持上述两种消息传递模式。消息代理的其他流行实现包括 ActiveMQ、ZeroMQ、Azure 服务总线和 Amazon Simple Queue Service (SQS)。所有这些实现都有很多共同点,本文中描述的许多概念适用于其中的大多数。

Queues

RabbitMQ 支持开箱即用的经典消息队列。开发人员定义命名队列,然后发布者可以将消息发送到该命名队列。反过来,消费者使用相同的队列来检索消息来处理它们。

Message exchanges

RabbitMQ 通过使用消息交换机来实现 pub/sub。发布者将其消息发布到消息交换机,不用知道这些消息的订阅者是谁。

每个订阅交换机的消费者都会创建一个队列,然后消息交换机将生成的消息排队以供消费者使用。它还可以根据各种路由规则过滤某些订阅者的消息。

RabbitMQ message exchange

值得注意的是,RabbitMQ 支持临时订阅和持久订阅。消费者可以通过 RabbitMQ 的 API 决定他们想要使用的订阅类型。

由于 RabbitMQ 的架构,我们还可以创建一种混合方法,其中一些订阅者形成消费者组,这些消费者组以特定队列上竞争消费者的形式共同处理消息。通过这种方式,我们实现了发布/订阅模式,同时还允许一些订阅者扩展以处理接收到的消息。

发布/订阅和队列相结合

Apache Kafka

Apache Kafka 是一个分布式流处理平台。

与基于队列和交换的 RabbitMQ 不同,Kafka 的存储层是使用分区事务日志实现的。 Kafka 还提供了 Streams API 来实时处理流,以及 Connectors API 来轻松与各种数据源集成。不过,这些超出了本文的范围。

云服务商为 Kafka 的存储层提供了替代解决方案。这些解决方案包括 Azure 事件中心,在某种程度上还包括 AWS Kinesis Data Streams。Kafka 的流处理功能还有特定于云的开源替代方案,同样,这些也超出了本文的范围。

Topics

Kafka 没有实现队列的概念。Kafka 将记录集合存储在称为主题的类别中。

对于每个主题,Kafka 都会维护一个分区的消息日志。每个分区都是一个有序的、不可变的记录序列,其中不断附加消息。

Kafka 在消息到达时将其附加到这些分区。默认情况下,它使用循环分区器在分区之间均匀地传播消息。

生产者可以修改此行为以创建逻辑消息流。例如在多租户应用程序中,我们可能希望根据每条消息的租户 ID 创建逻辑消息流。在物联网场景中,我们可能希望将每个生产者的身份不断映射到特定分区。确保来自同一逻辑流的所有消息映射到同一分区,以保证它们按顺序传递给消费者。

Kafka producers

消费者通过维护这些分区的偏移量(或索引)并按顺序读取它们来消费消息。

单个消费者可以使用多个主题,并且消费者可以扩展,直至与可用分区数量一致。

因此,在创建主题时,应仔细考虑该主题的消息传递的预期吞吐量。共同消费某个主题的一组消费者称为消费者组。Kafka 的 API 通常负责消费者组中消费者之间分区处理的平衡以及消费者当前分区偏移量的存储。

Kafka consumers

使用 Kafka 实现消息传递

Kafka 的内部实现其实很好地反映了 pub/sub 模式。

生产者可以向特定主题发送消息,多个消费者组可以消费同一条消息。每个消费者组都可以单独扩展以处理负载。由于消费者维护其分区偏移量,因此他们可以选择持久订阅(在重新启动时维持其偏移量)或临时订阅(即丢弃偏移量并在每次启动时从每个分区中的最新记录重新启动)。

Kafka 其实是不太适合队列模式的消息传递。当然我们可以创建一个只有一个消费者组的主题来模拟经典的消息队列。但这有多个缺点,在本文第二部分我们将详细讨论。

值得注意的是,无论消费者是否消费了这些消息,Kafka 都会将消息保留在分区中直至预先配置的时间段内。这种保留意味着消费者可以自由地重读过去的消息。此外,开发人员还可以使用 Kafka 的存储层来实现事件溯源和审计日志等机制。

第二部分:RabbitMQ 和 Kafka 的显著区别

RabbitMQ 是一个消息代理中间件,而 Apache Kafka 是一个分布式流处理平台。这种差异可能看起来只是语义上的,但它会带来严重的影响,影响我们方便地实现各种系统功能。

例如 Kafka 最适合处理流数据,在同一主题同一分区内保证消息顺序,而 RabbitMQ 对流中消息的顺序只提供基本的保证。

不过 RabbitMQ 内置了对重试逻辑和死信交换的支持,而 Kafka 将此类逻辑实现留给了用户。

消息顺序

Photo by Arshad Pooloo on Unsplash

RabbitMQ 对发送到队列或交换器的消息的顺序性提供了很少的保证。虽然消费者按照生产者发送消息的顺序处理消息似乎很合理,但其实并不是这样。

RabbitMQ 文档声明了以下有关其消息顺序的内容:

“在一个通道中发布的消息,经过一个交换机、一个队列和一个传出通道后,将按照发送的顺序被接收。” — RabbitMQ Broker Semantics

换句话说,当我们只有一个消息消费者,它就会按顺序接收消息。然而一旦我们有多个消费者从同一个队列读取消息,我们就无法保证消息的处理顺序。

发生这种缺乏排序保证的情况是因为消费者可能会在读取消息后将消息返回(或重新传递)到队列(例如在处理失败的情况下)。

一旦消息返回,另一个消费者就可以拿起它进行处理,即使它已经消费了后面的消息。因此多个消费者之间无法有序处理消息,如下图所示。

使用 RabbitMQ 时丢失消息导致排序错误的示例

我们可以通过将消费者并发数限制为 1 来重新保证 RabbitMQ 中的消息顺序。更准确地说,单个消费者内的线程计数要限制为 1,因为任何并行的消息处理都可能导致消息乱序问题。

如果我们将自己限制为一个单线程消费者虽然能保证消息顺序,但这会严重影响我们系统扩展消息的处理能力,因此我们不应该轻易的这样做。

另一方面,Kafka 为消费者在消息处理时提供了可靠的排序保证。 Kafka 保证发送到同一主题分区的所有消息都按顺序处理。

如果你还记得第 1 部分,默认情况下,Kafka 使用循环分区程序将消息放置在分区中。但是生产者可以在每个消息上设置分区键,以创建逻辑数据流(例如来自同一设备的消息,或属于同一租户的消息)。

来自同一数据流的所有消息都会被放置在同一分区中,从而使消费者组按顺序处理它们。

我们应该注意到,在消费者组中,每个分区都是由单个消费者的单个线程处理的。因此我们无法扩展单个分区的处理。

不过在 Kafka 中,我们可以扩展主题内的分区数量,从而使每个分区接收更少的消息,并为额外的分区添加额外的消费者。

赢家

Kafka 是明显的赢家,因为它允许消息按顺序处理。 RabbitMQ 在这方面只有较弱的保证。

消息路由

Photo by Webaroo on Unsplash

RabbitMQ 可以根据订阅者定义的路由规则将消息路由到消息交换机的订阅者。

主题交换(topic exchange)可以基于名为 routing_key 的专用标头来路由消息。

标头交换(headers exchange)可以基于任意消息标头路由消息。这两种交换都有效地允许消费者指定他们有兴趣接收的消息类型,从而为架构师选择消息平台提供了极大的灵活性。

exchange-headers 官网解释:www.rabbitmq.com/tutorials/a…

topic exchange 官网解释:www.rabbitmq.com/tutorials/a…

Kafka 不允许消费者在轮询主题之前过滤主题中的消息。订阅的消费者无一例外地接收分区中的所有消息。

作为开发人员,你可以使用 Kafka 用于流作业,该作业从主题读取消息,过滤它们,然后将它们推送到消费者订阅的另一个主题。尽管也可以实现,但相比与 RabbitMQ 需要更多的努力和维护,并且需要更多的活动部件。

赢家

RabbitMQ 在路由和过滤消息供消费者使用时提供卓越的支持。

消息计时

Photo by Oliver Hale on Unsplash

RabbitMQ 提供了有关延时消息发送到队列的各种功能:

消息生存时间 (TTL)

TTL 属性可以与发送到 RabbitMQ 的每条消息相关联。设置 TTL 可以由发布者直接完成,也可以作为队列本身的策略来完成。

指定 TTL 允许系统限制消息的有效期。如果消费者没有及时处理它,那么它会自动从队列中删除(并转移到死信交换,稍后会详细介绍)。

TTL 对于时间敏感但经过一段时间而没有处理后就变得无关紧要的命令特别有用。

延迟/定时消息

RabbitMQ 通过使用插件支持延迟/预定消息。当在消息交换上启用此插件时,生产者可以向 RabbitMQ 发送消息,并且生产者可以延迟 RabbitMQ 将此消息路由到消费者队列的时间。

此功能允许开发人员安排未来的命令,这些命令在此之前不应该被处理。例如当生产者遇到限制规则时,我们可能希望将特定命令的执行延迟到稍后的时间。

Kafka 不支持此类功能。当消息到达时,它将消息写入分区,消费者可以立即使用它们。

此外 Kafka 没有为消息提供 TTL 机制,尽管我们可以在应用程序级别实现一种机制。

我们还必须记住,Kafka 分区是一个仅追加的事务日志。因此它无法操纵消息时间(或分区内的位置)。

赢家

RabbitMQ 毫无疑问地赢得了这一项目的胜利。

消息保留

Photo by chuttersnap on Unsplash

一旦消费者成功消费消息,RabbitMQ 就会从存储中删除消息。此行为几乎是所有消息代理平台的一种设计,无法修改。

相比之下,Kafka 根据设计将所有消息保留至每个主题配置的超时时间。在消息保留方面,Kafka 不关心消费者的消费状态,因为它充当消息日志。

消费者可以根据需要消费每条消息,并且可以通过操纵分区偏移量“及时”来回移动。Kafka 会定期检查主题中消息的年龄,并驱逐那些足够老的消息。

Kafka 的性能不依赖于存储大小。因此从理论上讲,人们几乎可以无限期地存储消息,而不会影响性能(只要你的节点足够大来存储这些分区)。

赢家

Kafka 设计上就旨在消息保留,而 RabbitMQ 则不然。这里不需要竞争,Kafka 被宣布为获胜者。

故障处理

Photo by Sarah Kilian on Unsplash

在处理消息、队列和事件时,开发人员通常会认为消息处理总是成功。毕竟由于生产者将每条消息都放置在队列或主题中,即使消费者处理消息失败,它也可以简单地重试,直到成功为止。

虽然表面上确实如此,但我们应该对这个过程进行更多思考。我们应该承认,消息处理在某些情况下可能会失败。我们应该优雅地处理这些情况,即使部分情况下需要人为干预。

处理消息时可能出现两类错误:

  • 瞬时故障 — 由于临时问题(例如网络连接、CPU 负载或服务崩溃)而发生的故障。我们通常可以通过一遍又一遍地重试来缓解这种失败。
  • 持续性故障 — 由于无法通过额外重试解决的永久性问题而发生的故障。这些失败的常见原因是软件错误或无效的消息模式(即有害消息)。

作为架构师和开发人员,我们应该问自己:“消息处理失败时我们应该重试多少次?两次重试之间应该等待多长时间?我们如何区分暂时性故障和持续性故障?”

最重要的是:“当所有重试都失败或遇到持续失败时,我们该怎么办?”

虽然这些问题的答案是特定于领域的,但消息传递平台通常为我们提供解决工具。

RabbitMQ 提供了传递重试和死信交换 (DLX) 等工具来处理消息处理失败。

DLX 的主要思想是 RabbitMQ 可以根据适当的配置自动将失败的消息路由到 DLX,并在此交换中对消息应用进一步的处理规则,包括延迟重试、重试计数以及交付给“人工干预”队列。

下文提供了有关在 RabbitMQ 中处理重试的可能模式的更多见解。

engineering.nanit.com/rabbitmq-re…

这里要记住的最重要的事情是,在 RabbitMQ 中,当消费者忙于处理和重试特定消息时(甚至在将其返回到队列之前),其他消费者可以并发处理该消息之后的消息。

当特定消费者重试特定消息时,整个消息处理不会被卡住。因此消息使用者可以根据需要同步重试消息,而不会影响整个系统。

消费者1可以继续重试消息1,而其他消费者则继续处理消息

与 RabbitMQ 相反,Kafka 不提供任何开箱即用的此类工具。对于 Kafka 我们需要在应用程序中提供和实现消息重试机制。

另外我们应该注意,当消费者忙于同步重试特定消息时,无法处理来自同一分区的其他消息。

我们无法拒绝并重试特定消息并提交该消息之后的消息,因为消费者无法更改消息顺序。正如你所记得的,分区只是一个仅追加日志。

有一种类型的解决方案是应用程序可以将失败的消息提交到“重试主题”并从那里处理重试,不过这样我们就会失去了消息的顺序性。

Uber 工程部提供了解决此类问题的示例,可以在 Uber.com 上找到。如果消息处理的延迟不是问题,那么使用普通 Kafka 可能就足够了。

Uber.com 地址:eng.uber.com/reliable-re…

如果消费者在重试消息时遇到困难,则不会处理底部分区中的消息

赢家

RabbitMQ 是该项目上的赢家,因为它提供了一种开箱即用的解决此问题的工具。

扩展性

Photo by Graphic Node on Unsplash

有多个基准测试可以测试 RabbitMQ 和 Kafka 的性能。

虽然通用基准测试对特定情况的适用性有限,但 Kafka 通常被认为比 RabbitMQ 具有更好的性能。 Kafka 使用顺序磁盘 I/O 来提高性能。

它使用分区的架构意味着它的水平扩展(横向扩展)比 RabbitMQ 更好,而 RabbitMQ 的垂直扩展(纵向扩展)更好。

大型 Kafka 的集群部署通常每秒可以处理数十万条消息,甚至每秒处理数百万条消息。

过去的时间里,Pivotal 团队发布了 RabbitMQ 集群可以每秒处理 100 万条消息的文章如下。然而它是在 30 个节点的集群上实现的,负载以最佳方式分布在多个队列和交换器上。

RabbitMQ Hits One Million Messages Per Second on Google Compute Engine,地址:tanzu.vmware.com/content/blo…

典型的 RabbitMQ 部署包括三到七个节点集群,这些节点集群不一定能最佳地分配队列之间的负载。这些典型的集群通常只能每秒处理数万条消息的负载。

赢家

虽然这两个平台都可以处理大量负载,但 Kafka 通常比 RabbitMQ 具有更好的扩展性,并且可以实现更高的吞吐量,从而赢得了这一轮。

然而值得注意的是,大多数系统都不会达到每秒上万的消息处理量!因此除非你正在构建下一个拥有数百万用户的热门软件系统,否则你不需要太关心扩展性,因为这两个平台都可以很好地为你服务。

消费者复杂性

Photo by John Barkiple on Unsplash

RabbitMQ 使用智能代理人(smart-broker)和愚蠢消费者(dumb-consumer)的方法。消费者注册到消费队列上,RabbitMQ 会在消息进入时向它们推送消息以进行处理。RabbitMQ 消费者还具有主动拉取的功能。不过它使用的比较少。

RabbitMQ 自动向消费者分发消息以及从队列(可能是 DLX)中删除消息。消费者无需担心这些。

RabbitMQ 的结构还意味着,当负载增加时,队列的消费者组可以有效地从一个消费者扩展到多个消费者,而无需对系统进行任何更改。

RabbitMQ 消费者有效地扩展和缩小

另一方面,Kafka 使用愚蠢代理人(dumb-broker)和聪明消费者(smart-consumer)的方法。消费者组中的消费者需要协调它们之间主题分区的约定(以便消费者组中只有一个消费者监听特定分区)。

消费者还需要管理和存储其分区的偏移索引。幸运的是 Kafka SDK 为我们处理了这些,所以我们不需要自己管理。

不过当我们的负载较低时,单个消费者需要并行处理和跟踪多个分区,这需要消费者端有更多的资源。

此外,随着系统负载的增加,我们只能将消费者组的消费者数量扩大到等于主题中分区数量的程度。不过我们可以通过增加分区数来增加消费者数。

当系统负载减少时,我们无法删除已经添加的分区,从而浪费了消费者资源。

Kafka分区无法删除,缩减规模后留给消费者更多的工作

赢家

RabbitMQ 在设计上就是为愚蠢消费者(dumb-consumers)而设计的。所以它成为了这一轮的胜利者。

何时使用哪个?

Photo by Paulius Dragunas on Unsplash

现在我们面临一个价值百万美元的问题:“我们什么时候应该使用 RabbitMQ,什么时候应该使用 Kafka?”

如果我们总结以上差异,我们将得出以下结论:

一般情况下,RabbitMQ 是更好的选择:

  • 先进灵活的路由规则。
  • 消息计时控制(控制消息过期或消息延迟)。
  • 高级故障处理功能,以防消费者无法处理消息(暂时或永久)。
  • 更简单的消费者实现。
  • 当我们需要以下条件时,Kafka 是更好的选择:

  • 严格的消息排序。
  • 消息保留较长时间,包括重放过去消息的可能性。
  • 当传统解决方案无法满足我们对扩展性的需求时,Kafka 能够达到较高的规模。
  • 我们可以使用这两个平台来用于大多数软件系统。然而作为架构师,我们有责任选择最适合这个系统的工具。在做出这种选择时,我们应该考虑如上所述的功能差异和非功能限制。

    这些非功能限制包括:

  • 当前平台的现有开发人员掌握知识。
  • 托管云解决方案的可用性(如果适用)。
  • RabbitMQ 和 Kafka 的运营成本。
  • 我们的目标技术栈中 SDK 的可用性。
  • 在开发复杂的软件系统时,我们可能会倾向于只使用一个消息平台来实现所有必需的消息传递功能。然而根据我的经验,在同一个系统中,同时使用这两个消息平台会带来很多好处。

    例如,在基于事件驱动架构的系统中,我们可以使用 RabbitMQ 在服务之间发送命令,并使用 Kafka 来实现业务事件通知。

    原因是事件通知通常用于事件溯源、批量操作(ETL 样式)或审计目的,因此 Kafka 的消息保留功能非常有价值。

    另一方面,命令通常需要在消费者端进行额外的处理,这些处理可能会失败并且需要高级的故障处理功能,这适用于 RabbitMQ。

    最后

    我开始写这两篇文章系列的初衷是,许多开发人员认为 RabbitMQ 和 Kafka 是可以互换的。我希望大家看到这两篇文章后,能更加深入了解这些消息平台的内部构造以及它们之间的技术差异。这些差异反过来又会影响这些消息平台所服务的系统。RabbitMQ 和 Kafka 都很棒,可以适用于多种系统。

    最后作为架构师,我们有责任了解每个系统的需求,确定其优先级,并选择最合适的解决方案。

    关注公众号【waynblog】每周分享技术干货、开源项目、实战经验、国外优质文章翻译等,您的关注将是我的更新动力!

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论