介绍
作为一名处理大量基于微服务的系统的软件架构师,我经常会遇到一个反复出现的问题:“我应该使用RabbitMQ还是Kafka?”
出于某种原因,许多开发人员认为这些技术是可以互换的。虽然在某些情况下确实如此,但这些平台之间存在各种潜在差异。
因此,不同的场景需要不同的解决方案,选择错误的解决方案可能会严重影响您设计、开发和维护软件解决方案的能力。
本系列的第 1 部分解释了RabbitMQ和Apache Kafka的内部实现概念。这一部分继续回顾这两个平台之间的显着差异,作为软件架构师和开发人员,我们应该注意这些差异。
然后,它继续解释我们通常尝试使用这些工具实现的架构模式,并评估何时使用每种工具。
注 1
如果您不熟悉 RabbitMQ 和 Kafka 的内部结构,我强烈建议您先阅读本文的第1 部分。如果您不确定,请随意浏览标题和图表,至少可以瞥见这些差异。
笔记2
继上一篇文章之后,一些读者问我关于Apache Pulsar 的问题。Pulsar 是另一个消息传递平台,旨在结合 RabbitMQ 和 Kafka 必须提供的一些最好的东西。
作为一个现代平台,它看起来非常有前途;但是,与任何其他平台一样,它也有其优点和缺点。我将在以后的文章中尝试解决 Apache Pulsar 的比较问题,因为这篇文章主要关注 RabbitMQ 和 Kafka。
RabbitMQ 和 Kafka 之间的显着差异
RabbitMQ 是一个消息代理,而 Apache Kafka 是一个分布式流平台。这种差异可能看起来是语义上的,但它带来了严重的影响,会影响我们轻松实现各种用例的能力。
例如,Kafka 最适合用于处理数据流,而 RabbitMQ 对流中消息的排序具有最低限度的保证。
另一方面,RabbitMQ 内置了对重试逻辑和死信交换的支持,而 Kafka 将这些实现留给了用户。
本节重点介绍这些不同平台之间的这些和其他显着差异。
消息排序
RabbitMQ 对发送到队列或交换器的消息的顺序几乎没有保证。虽然消费者按照生产者发送消息的顺序处理消息似乎很明显,但这是非常具有误导性的。
RabbitMQ 文档说明了以下有关其订购保证的内容:
“在一个通道中发布的消息,通过一个交换器、一个队列和一个传出通道,将按照它们发送的顺序被接收。” — RabbitMQ Broker 语义
换句话说,只要我们有一个消息消费者,它就会按顺序接收消息。但是,一旦我们有多个消费者从同一个队列中读取消息,我们就无法保证消息的处理顺序。
发生这种缺乏排序保证的情况是因为消费者可能会在读取消息后将消息返回(或重新传送)到队列中(例如,在处理失败的情况下)。
一旦返回一条消息,即使它已经消费了后面的消息,另一个消费者也可以选择它进行处理。因此,消费者组无序处理消息,如下图所示。
使用 RabbitMQ 时丢失消息排序的示例
当然,我们可以通过将消费者并发限制为 1 来重新获得 RabbitMQ 中的消息排序。更准确地说,单个使用者内的线程数应限制为 1,因为任何并行消息处理都可能导致相同的乱序问题。
但是,将自己限制在一个单线程消费者上会严重影响我们随着系统的增长扩展消息处理的能力。因此,我们不应轻率地进行这种权衡。
另一方面,Kafka 为消息处理提供了可靠的排序保证。Kafka 保证发送到同一主题分区的所有消息都按顺序处理。
如果您回忆一下第 1 部分,默认情况下,Kafka 将消息放置在具有循环分区器的分区中。但是,生产者可以在每条消息上设置分区键以创建逻辑数据流(例如来自同一设备的消息或属于同一租户的消息)。
然后,来自同一流的所有消息都放置在同一分区中,从而使消费者组按顺序处理它们。
然而,我们应该注意到,在一个消费者组内,每个分区由单个消费者的单个线程处理。因此,我们无法扩展单个分区的处理。
但是,在 Kafka 中,我们可以扩展主题内的分区数量,使每个分区接收更少的消息并为额外的分区添加额外的消费者。
优胜者
Kafka 是明显的赢家,因为它允许按顺序处理消息。RabbitMQ 在这方面只有很弱的保证。
消息路由
RabbitMQ 可以根据订阅者定义的路由规则将消息路由到消息交换的订阅者。一个话题交换基于名为专用头可以将消息路由routing_key
。
或者,头交换可以根据任意消息头路由消息。这两种交换有效地允许消费者指定他们有兴趣接收的消息类型,从而为解决方案架构师提供了极大的灵活性。
另一方面,Kafka 不允许消费者在轮询消息之前过滤主题中的消息。订阅的消费者无一例外地接收分区中的所有消息。
作为开发人员,您可以使用Kafka 流作业,它从主题中读取消息、过滤它们并将它们推送到消费者可以订阅的另一个主题。尽管如此,这需要更多的努力和维护,并且有更多的活动部件。
优胜者
RabbitMQ 在路由和过滤消息供消费者使用时提供了卓越的支持。
消息定时
奥利弗·黑尔( Oliver Hale)在Unsplash上的照片
RabbitMQ 提供了多种功能来对发送到队列的消息进行计时:
消息生存时间 (TTL)
甲TTL属性可以与发送到RabbitMQ的每个消息相关联。设置 TTL 要么直接由发布者完成,要么作为队列本身的策略。
指定 TTL 允许系统限制消息的有效期。如果消费者没有在适当的时候处理它,那么它会自动从队列中删除(并转移到死信交换,但稍后会更多)。
TTL 对于时间敏感的命令特别有用,这些命令在经过一段时间未经处理后变得无关紧要。
延迟/预定消息
RabbitMQ 通过使用插件支持延迟/调度的消息。在消息交换上启用此插件后,生产者可以向 RabbitMQ 发送消息,生产者可以延迟 RabbitMQ 将此消息路由到消费者队列的时间。
此功能允许开发人员安排未来的命令,在此之前不打算处理这些命令。例如,当生产者遇到限制规则时,我们可能希望将特定命令的执行延迟到稍后的时间。
Kafka 不支持此类功能。它在消息到达时将消息写入分区,供消费者立即使用。
此外,Kafka 没有为消息提供 TTL 机制,尽管我们可以在应用程序级别实现一个。
我们还必须记住,Kafka 分区是一个只能追加的事务日志。因此,它无法操纵消息时间(或分区内的位置)。
优胜者
RabbitMQ 赢得了这场胜利,因为其实现的性质限制了 Kafka。
消息保留
一旦消费者成功消费消息,RabbitMQ 就会从存储中驱逐消息。无法修改此行为。它是几乎所有消息代理设计的一部分。
相比之下,Kafka 通过设计将所有消息持久化,直到每个主题配置一个超时。在消息保留方面,Kafka 并不关心其消费者的消费状态,因为它充当消息日志。
消费者可以随心所欲地消费每条消息,并且他们可以通过操纵他们的分区偏移量来“及时”来回旅行。Kafka 会定期检查主题中消息的年龄并驱逐那些足够老的消息。
Kafka 的性能不依赖于存储大小。因此,理论上,几乎可以无限期地存储消息而不会影响性能(只要您的节点大到足以存储这些分区)。
优胜者
Kafka 旨在保留消息,而 RabbitMQ 则不是。这里没有竞争,卡夫卡被宣布为赢家。
故障处理
莎拉·基利安( Sarah Kilian)在Unsplash上的照片
在处理消息、队列和事件时,开发人员往往认为消息处理总是成功的。毕竟,由于生产者将每条消息放在一个队列或主题中,即使消费者处理消息失败,它也可以简单地重试直到成功。
虽然表面上是这样,但我们应该对这个过程进行额外的思考。我们应该承认消息处理在某些情况下可能会失败。我们应该优雅地处理这些情况,即使解决方案部分由人工干预组成。
处理消息时可能出现两种类型的错误:
- 瞬态故障 - 由于网络连接、CPU 负载或服务崩溃等临时问题而发生的故障。我们通常可以通过反复重试来缓解这种失败。
- 持久性故障 - 由于无法通过额外重试解决的永久性问题而发生的故障。这些故障的常见原因是软件错误或无效的消息模式(即有害消息)。
作为架构师和开发人员,我们应该问自己:“对于消息处理失败,我们应该重试多少次?我们应该在重试之间等待多长时间?我们如何区分暂时性故障和持久性故障?”
最重要的是:“当所有重试都失败或遇到持续失败时,我们该怎么办?”
虽然这些问题的答案是特定于领域的,但消息传递平台通常会为我们提供实施解决方案的工具。
RabbitMQ 提供了诸如传递重试和死信交换 (DLX) 等工具来处理消息处理失败。
DLX 的主要思想是 RabbitMQ 可以根据适当的配置自动将失败的消息路由到 DLX,并在此交换中对消息应用进一步的处理规则,包括延迟重试、重试计数和传递给“人工干预”队列。
本文提供了有关在 RabbitMQ 中处理重试的可能模式的更多见解。
这里要记住的最重要的一点是,在 RabbitMQ 中,当消费者忙于处理和重试特定消息(甚至在将其返回队列之前)时,其他消费者可以并发处理跟随它的消息。
当特定消费者重试特定消息时,整个消息处理不会卡住。因此,消息消费者可以根据需要同步重试消息,而不会影响整个系统。
消费者 1 可以继续重试消息 1,而其他消费者继续处理消息
与 RabbitMQ 不同,Kafka 没有提供任何这种开箱即用的机制。有了Kafka,我们就可以在应用层提供和实现消息重试机制。
另外,我们应该注意,当消费者忙于同步重试特定消息时,无法处理来自同一分区的其他消息。
我们不能拒绝并重试特定消息并提交其后的消息,因为消费者无法更改消息顺序。您还记得,分区只是一个只能追加的日志。
应用程序级解决方案可以将失败的消息提交到“重试主题”并从那里处理重试;然而,我们在这种类型的解决方案中丢失了消息排序。
可以在 Uber.com上找到 Uber Engineering 的此类实现示例。如果消息处理延迟不是问题,那么能够充分监控错误的 vanilla Kafka 解决方案可能就足够了。
如果消费者在重试消息时卡住,则不会处理底部分区中的消息
优胜者
RabbitMQ 在积分上是赢家,因为它提供了一种开箱即用的机制来解决这个问题。
规模
有多个基准测试可以检查 RabbitMQ 和 Kafka 的性能。
虽然通用基准测试对特定情况的适用性有限,但人们普遍认为 Kafka 的性能优于 RabbitMQ。Kafka 使用顺序磁盘 I/O 来提高性能。
它使用分区的架构意味着它的横向扩展(横向扩展)比 RabbitMQ 更好,后者纵向扩展(纵向扩展)更好。
大型 Kafka 部署通常可以每秒处理数十万条消息,甚至每秒处理数百万条消息。
过去,Pivotal 记录了每秒处理100 万条消息的 RabbitMQ 集群;然而,它在一个 30 节点的集群上做到了这一点,负载以最佳方式分布在多个队列和交换机上。
典型的 RabbitMQ 部署包括三到七个节点集群,这些集群不一定能在队列之间优化分配负载。这些典型的集群通常可以预期每秒处理数万条消息的负载。
优胜者
虽然这两个平台都可以处理海量负载,但 Kafka 通常比 RabbitMQ 具有更好的扩展性和更高的吞吐量,因此赢得了这一轮。
然而,重要的是要注意,大多数系统都不会达到这些限制中的任何一个!因此,除非您正在构建下一个拥有数百万用户的红极一时的软件系统,否则您不必太在意规模,因为这两个平台都可以很好地为您服务。
消费者的复杂性
约翰·巴基普( John Barkiple)在Unsplash上的照片
RabbitMQ 使用智能经纪人和哑消费者方法。消费者注册消费队列,RabbitMQ 将消息推送到队列中进行处理。RabbitMQ 也有一个pull API;然而,它很少使用。
RabbitMQ 管理向消费者分发消息以及从队列(可能到 DLX)中删除消息。消费者无需担心任何这些。
RabbitMQ 的结构也意味着当负载增加时,队列的消费者组可以有效地从一个消费者扩展到多个消费者,而无需对系统进行任何更改。
RabbitMQ 消费者有效地纵向扩展和缩减
另一方面,Kafka 使用了一种愚蠢的经纪人和智能消费者的方法。消费者组中的消费者需要协调他们之间主题分区的租约(以便消费者组中只有一个消费者侦听特定分区)。
消费者还需要管理和存储他们分区的偏移索引。幸运的是,Kafka SDK 为我们处理了这些,所以我们不需要自己管理。
但是,当我们的负载较低时,单个消费者需要并行处理和跟踪多个分区,这需要消费者端更多的资源。
此外,随着负载的增加,我们只能将消费者组扩展到消费者数量等于主题中分区数量的程度。在此之上,我们需要配置 Kafka 以添加额外的分区。
但是,随着负载再次降低,我们无法删除已经添加的分区,从而增加了消费者需要做的工作。尽管如上所述,SDK 会处理这些额外的工作。
Kafka 分区无法删除,缩小后给消费者留下更多工作
优胜者
RabbitMQ 在设计上是为愚蠢的消费者而构建的。结果,它是这一轮的赢家。
何时使用哪个?
保利乌斯·德拉古纳斯( Paulius Dragunas)在Unsplash上的照片
现在我们面临着百万美元的问题:“我们什么时候应该使用 RabbitMQ,什么时候应该使用 Kafka?”
如果我们总结上述差异,我们得出以下结论:
当我们需要时,RabbitMQ 更可取:
- 先进灵活的路由规则。
- 消息定时控制(控制消息过期或消息延迟)。
- 高级故障处理功能,以防消费者更可能无法处理消息(暂时或永久)。
- 更简单的消费者实现。
当我们需要时,最好使用 Kafka:
- 严格的消息排序。
- 长时间保留消息,包括重播过去消息的可能性。
- 在传统解决方案无法满足需求时达到大规模的能力。
我们可以使用这两个平台实现大多数用例。但是,作为解决方案架构师,我们需要为工作选择最合适的工具。在做出此选择时,我们应该同时考虑上面强调的功能差异和非功能性约束。
这些约束包括:
- 这些平台的现有开发人员知识。
- 托管云解决方案的可用性(如果适用)。
- 每个解决方案的运营成本。
- 适用于我们的目标堆栈的 SDK 的可用性。
在开发复杂的软件系统时,我们可能会倾向于使用同一平台实现所有必需的消息传递用例。尽管如此,根据我的经验,通常情况下,同时使用这两个平台可以带来很多好处。
例如,在基于事件驱动架构的系统中,我们可以使用 RabbitMQ 在服务之间发送命令,并使用 Kafka 来实现业务事件通知。
这样做的原因是事件通知通常用于事件溯源、批处理操作(ETL 样式)或用于审计目的,因此 Kafka 因其消息保留功能而非常有价值。
另一方面,命令通常需要在消费者端进行额外处理,这些处理可能会失败并需要高级故障处理能力。
在这里,RabbitMQ 因其功能而大放异彩。将来我可能会写一篇详细的文章,但您必须记住 - 您的里程可能会有所不同,因为适用性取决于您的具体要求。
闭幕式
我开始这个两篇系列文章是因为观察到许多开发人员认为 RabbitMQ 和 Kafka 是可以互换的。我希望查看这些帖子有助于深入了解这些平台的实施以及它们之间的技术差异。
反过来,这些差异会影响这些平台很好地服务的用例。这两个平台都很棒,可以服务于多个用例。
但是,作为解决方案架构师,我们有责任了解每个用例的需求,确定它们的优先级,并选择最合适的解决方案。