RabbitMQ 是一个消息代理中间件,而 Apache Kafka 是一个分布式流处理平台。这种差异可能看起来只是语义上的,但它会带来严重的影响,影响我们方便地实现各种系统功能。
例如 Kafka 最适合处理流数据,在同一主题同一分区内保证消息顺序,而 RabbitMQ 对流中消息的顺序只提供基本的保证。
不过 RabbitMQ 内置了对重试逻辑和死信交换的支持,而 Kafka 将此类逻辑实现留给了用户。
RabbitMQ 对发送到队列或交换器的消息的顺序性提供了很少的保证。虽然消费者按照生产者发送消息的顺序处理消息似乎很合理,但其实并不是这样。
RabbitMQ 文档声明了以下有关其消息顺序的内容:
“在一个通道中发布的消息,经过一个交换机、一个队列和一个传出通道后,将按照发送的顺序被接收。” — RabbitMQ Broker Semantics
换句话说,当我们只有一个消息消费者,它就会按顺序接收消息。然而一旦我们有多个消费者从同一个队列读取消息,我们就无法保证消息的处理顺序。
发生这种缺乏排序保证的情况是因为消费者可能会在读取消息后将消息返回(或重新传递)到队列(例如在处理失败的情况下)。
一旦消息返回,另一个消费者就可以拿起它进行处理,即使它已经消费了后面的消息。因此多个消费者之间无法有序处理消息,如下图所示。
使用 RabbitMQ 时丢失消息导致排序错误的示例
我们可以通过将消费者并发数限制为 1 来重新保证 RabbitMQ 中的消息顺序。更准确地说,单个消费者内的线程计数要限制为 1,因为任何并行的消息处理都可能导致消息乱序问题。
如果我们将自己限制为一个单线程消费者虽然能保证消息顺序,但这会严重影响我们系统扩展消息的处理能力,因此我们不应该轻易的这样做。
另一方面,Kafka 为消费者在消息处理时提供了可靠的排序保证。Kafka 保证发送到同一主题分区的所有消息都按顺序处理。
如果你还记得第 1 部分内容,默认情况下,Kafka 使用循环分区程序将消息放置在分区中。但是生产者可以在每个消息上设置分区键,以创建逻辑数据流(例如来自同一设备的消息,或属于同一租户的消息)。
来自同一数据流的所有消息都会被放置在同一分区中,从而使消费者组按顺序处理它们。
我们应该注意到,在消费者组中,每个分区都是由单个消费者的单个线程处理的。因此我们无法扩展单个分区的处理。
不过在 Kafka 中,我们可以扩展主题内的分区数量,从而使每个分区接收更少的消息,并为额外的分区添加额外的消费者。
赢家:Kafka 是明显的赢家,因为它允许消息按顺序处理。RabbitMQ 在这方面只有较弱的保证。
RabbitMQ 可以根据订阅者定义的路由规则将消息路由到消息交换机的订阅者。
主题交换(topic exchange)可以基于名为 routing_key 的专用标头来路由消息。
标头交换(headers exchange)可以基于任意消息标头路由消息。这两种交换都有效地允许消费者指定他们有兴趣接收的消息类型,从而为架构师选择消息平台提供了极大的灵活性。
exchange-headers 官网解释:
https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchange-headers
topic exchange 官网解释:
https://www.rabbitmq.com/tutorials/amqp-concepts.html#exchange-topic
Kafka 不允许消费者在轮询主题之前过滤主题中的消息。订阅的消费者无一例外地接收分区中的所有消息。
作为开发人员,你可以使用 Kafka 用于流作业,该作业从主题读取消息,过滤它们,然后将它们推送到消费者订阅的另一个主题。尽管也可以实现,但相比与 RabbitMQ 需要更多的努力和维护,并且需要更多的活动部件。
赢家:RabbitMQ 在路由和过滤消息供消费者使用时提供卓越的支持。
RabbitMQ 提供了有关延时消息发送到队列的各种功能:
消息生存时间 (TTL)
TTL 属性可以与发送到 RabbitMQ 的每条消息相关联。设置 TTL 可以由发布者直接完成,也可以作为队列本身的策略来完成。
指定 TTL 允许系统限制消息的有效期。如果消费者没有及时处理它,那么它会自动从队列中删除(并转移到死信交换,稍后会详细介绍)。
TTL 对于时间敏感但经过一段时间而没有处理后就变得无关紧要的命令特别有用。
延迟/定时消息
RabbitMQ 通过使用插件支持延迟/预定消息。当在消息交换上启用此插件时,生产者可以向 RabbitMQ 发送消息,并且生产者可以延迟 RabbitMQ 将此消息路由到消费者队列的时间。
此功能允许开发人员安排未来的命令,这些命令在此之前不应该被处理。例如当生产者遇到限制规则时,我们可能希望将特定命令的执行延迟到稍后的时间。
Kafka 不支持此类功能。当消息到达时,它将消息写入分区,消费者可以立即使用它们。
此外 Kafka 没有为消息提供 TTL 机制,尽管我们可以在应用程序级别实现一种机制。
我们还必须记住,Kafka 分区是一个仅追加的事务日志。因此它无法操纵消息时间(或分区内的位置)。
赢家:RabbitMQ 毫无疑问地赢得了这一项目的胜利。
一旦消费者成功消费消息,RabbitMQ 就会从存储中删除消息。此行为几乎是所有消息代理平台的一种设计,无法修改。
相比之下,Kafka 根据设计将所有消息保留至每个主题配置的超时时间。在消息保留方面,Kafka 不关心消费者的消费状态,因为它充当消息日志。
消费者可以根据需要消费每条消息,并且可以通过操纵分区偏移量“及时”来回移动。Kafka 会定期检查主题中消息的年龄,并驱逐那些足够老的消息。
Kafka 的性能不依赖于存储大小。因此从理论上讲,人们几乎可以无限期地存储消息,而不会影响性能(只要你的节点足够大来存储这些分区)。
赢家:Kafka 设计上就旨在消息保留,而 RabbitMQ 则不然。这里不需要竞争,Kafka 被宣布为获胜者。
在处理消息、队列和事件时,开发人员通常会认为消息处理总是成功。毕竟由于生产者将每条消息都放置在队列或主题中,即使消费者处理消息失败,它也可以简单地重试,直到成功为止。
虽然表面上确实如此,但我们应该对这个过程进行更多思考。我们应该承认,消息处理在某些情况下可能会失败。我们应该优雅地处理这些情况,即使部分情况下需要人为干预。
处理消息时可能出现两类错误:
作为架构师和开发人员,我们应该问自己:“消息处理失败时我们应该重试多少次?两次重试之间应该等待多长时间?我们如何区分暂时性故障和持续性故障?”
最重要的是:“当所有重试都失败或遇到持续失败时,我们该怎么办?”
虽然这些问题的答案是特定于领域的,但消息传递平台通常为我们提供解决工具。
RabbitMQ 提供了传递重试和死信交换 (DLX) 等工具来处理消息处理失败。
DLX 的主要思想是 RabbitMQ 可以根据适当的配置自动将失败的消息路由到 DLX,并在此交换中对消息应用进一步的处理规则,包括延迟重试、重试计数以及交付给“人工干预”队列。
下文提供了有关在 RabbitMQ 中处理重试的可能模式的更多见解。
https://engineering.nanit.com/rabbitmq-retries-the-full-story-ca4cc6c5b493?gi=3b2440cf4efd
这里要记住的最重要的事情是,在 RabbitMQ 中,当消费者忙于处理和重试特定消息时(甚至在将其返回到队列之前),其他消费者可以并发处理该消息之后的消息。
当特定消费者重试特定消息时,整个消息处理不会被卡住。因此消息使用者可以根据需要同步重试消息,而不会影响整个系统。
消费者1可以继续重试消息1,而其他消费者则继续处理消息
与 RabbitMQ 相反,Kafka 不提供任何开箱即用的此类工具。对于 Kafka 我们需要在应用程序中提供和实现消息重试机制。
另外我们应该注意,当消费者忙于同步重试特定消息时,无法处理来自同一分区的其他消息。
我们无法拒绝并重试特定消息并提交该消息之后的消息,因为消费者无法更改消息顺序。正如你所记得的,分区只是一个仅追加日志。
有一种类型的解决方案是应用程序可以将失败的消息提交到“重试主题”并从那里处理重试,不过这样我们就会失去了消息的顺序性。
Uber 工程部提供了解决此类问题的示例,可以在 Uber.com 上找到。如果消息处理的延迟不是问题,那么使用普通 Kafka 可能就足够了。
Uber.com 地址:https://eng.uber.com/reliable-reprocessing/
如果消费者在重试消息时遇到困难,则不会处理底部分区中的消息
赢家:RabbitMQ 是该项目上的赢家,因为它提供了一种开箱即用的解决此问题的工具。
有多个基准测试可以测试 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,
地址:
https://tanzu.vmware.com/content/blog/rabbitmq-hits-one-million-messages-per-second-on-google-compute-engine
典型的 RabbitMQ 部署包括三到七个节点集群,这些节点集群不一定能最佳地分配队列之间的负载。这些典型的集群通常只能每秒处理数万条消息的负载。
赢家:虽然这两个平台都可以处理大量负载,但 Kafka 通常比 RabbitMQ 具有更好的扩展性,并且可以实现更高的吞吐量,从而赢得了这一轮。
然而值得注意的是,大多数系统都不会达到每秒上万的消息处理量!因此除非你正在构建下一个拥有数百万用户的热门软件系统,否则你不需要太关心扩展性,因为这两个平台都可以很好地为你服务。
RabbitMQ 使用智能代理人(smart-broker)和愚蠢消费者(dumb-consumer)的方法。消费者注册到消费队列上,RabbitMQ 会在消息进入时向它们推送消息以进行处理。RabbitMQ 消费者还具有主动拉取的功能。不过它使用的比较少。
RabbitMQ 自动向消费者分发消息以及从队列(可能是 DLX)中删除消息。消费者无需担心这些。
RabbitMQ 的结构还意味着,当负载增加时,队列的消费者组可以有效地从一个消费者扩展到多个消费者,而无需对系统进行任何更改。
RabbitMQ 消费者有效地扩展和缩小
另一方面,Kafka 使用愚蠢代理人(dumb-broker)和聪明消费者(smart-consumer)的方法。消费者组中的消费者需要协调它们之间主题分区的约定(以便消费者组中只有一个消费者监听特定分区)。
消费者还需要管理和存储其分区的偏移索引。幸运的是 Kafka SDK 为我们处理了这些,所以我们不需要自己管理。
不过当我们的负载较低时,单个消费者需要并行处理和跟踪多个分区,这需要消费者端有更多的资源。
此外,随着系统负载的增加,我们只能将消费者组的消费者数量扩大到等于主题中分区数量的程度。不过我们可以通过增加分区数来增加消费者数。
当系统负载减少时,我们无法删除已经添加的分区,从而浪费了消费者资源。
Kafka分区无法删除,缩减规模后留给消费者更多的工作
赢家:RabbitMQ 在设计上就是为愚蠢消费者(dumb-consumers)而设计的。所以它成为了这一轮的胜利者。
现在我们面临一个价值百万美元的问题:“我们什么时候应该使用 RabbitMQ,什么时候应该使用 Kafka?”
如果我们总结以上差异,我们将得出以下结论:
一般情况下,RabbitMQ 是更好的选择:
当我们需要以下条件时,Kafka 是更好的选择:
我们可以使用这两个平台来用于大多数软件系统。然而作为架构师,我们有责任选择最适合这个系统的工具。在做出这种选择时,我们应该考虑如上所述的功能差异和非功能限制。
这些非功能限制包括:
当前平台的现有开发人员掌握知识。
托管云解决方案的可用性(如果适用)。
RabbitMQ 和 Kafka 的运营成本。
我们的目标技术栈中 SDK 的可用性。
在开发复杂的软件系统时,我们可能会倾向于只使用一个消息平台来实现所有必需的消息传递功能。然而根据我的经验,在同一个系统中,同时使用这两个消息平台会带来很多好处。
例如在基于事件驱动架构的系统中,我们可以使用 RabbitMQ 在服务之间发送命令,再使用 Kafka 来实现业务事件通知。
原因是事件通知通常用于事件溯源、批量操作(ETL 样式)或审计目的,因此 Kafka 的消息保留功能非常有价值。
命令通常需要在消费者端进行额外的处理,这些处理可能会失败并且需要高级的故障处理功能,这适用于 RabbitMQ。