原文地址
在本系列文章的第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对发送到队列或交换器的消息(exchange)提供很小的有序性保证。虽然消费者按照生产者发送消息的顺序处理消息似乎很明显,但这是非常具有误导性的。
RabbitMQ文档声明了以下关于它的顺序保证:消息在一个通道中发布、经过一个交换器、一个队列和一个传出通道后将以发送的相同顺序被接收。 换句话说,我们只有一个消息消费者的话,它就按顺序接收消息。然而,一旦有多个消费者从同一个队列读取消息,我们就无法保证消息的处理顺序。
这种缺乏顺序保证的原因可能是在消费者处理消息失败情况下,消息又重新发送到队列导致的。一旦消息返回队列,另一个消费者即使已经消费了之后的数据,也可以消费这个返回的消息。因此,消费者组无序地处理消息,如下图所示。
上图中因为消息1在消费者1中处理失败后返回队列,后面传递给消费者2处理。这样消息1就落后消息2,处理顺序被破坏。
当然我们可以限制消费者并发数为1来获得消息的有序处理。更准确地说,一个消费者内的线程数限制为1,因为并行处理消息是导致乱序的原因。但是限制消息处理的并发数会严重影响整个系统处理消息能力。因此,我们不应该草率地进行这种权衡。
另一方面,Kafka为消息处理提供了可靠的顺序保证。Kafka保证所有发送到同一个主题分区的消息都是按顺序处理的。 如果您还记得第1部分,在默认情况下,Kafka使用轮循分区器将消息放置在分区中。但是,生产者可以在每个消息上设置一个分区键,以创建逻辑数据流(例如来自同一设备的消息,或属于同一租户的消息)。然后,来自同一流的所有消息都被放置在同一个分区中,从而使它们按消费者组的顺序进行处理。
但是,我们应该注意,在一个消费者组中,每个分区由单个消费者的单个线程处理。因此,我们不能扩展单个分区的处理。 然而,在Kafka中,我们可以扩展一个主题内的分区数量,导致每个分区接收到更少的消息,并为额外的分区添加额外的消费者。
胜出者
Kafka显然是赢家,因为它允许按顺序处理消息。在这方面,RabbitMQ的保证很弱。
消息路由
RabbitMQ可以根据订阅者定义的路由规则将消息路由给订阅者。主题交换器可以基于一个名为routing_key的专用头来路由消息。或者,消息头交换器可以基于任意消息头路由消息。这两种交换都有效地允许订阅者指定他们感兴趣接收的消息类型,从而为解决方案架构师提供了极大的灵活性。
另一方面,Kafka不允许消费者在轮询消息之前过滤主题中的消息。订阅的消费者会接收分区中的所有消息。
作为开发者,你可以使用Kafka推送任务流,它从主题中读取消息,过滤它们,并将它们推送到消费者可以订阅的另一个主题。尽管如此,这需要更多的精力维护,并增加更多的组件。
胜出者
在路由和过滤消息时,RabbitMQ提供了更好的支持。
定时消息
RabbitMQ提供了各种关于消息发送到队列的定时功能:
消息存活时间TTL
发送到RabbitMQ的消息可以设置TTL属性。设置TTL可以直接由发布者完成,也可以作为队列本身的策略。
设置TTL值后,系统将对消息的有效时间进行限制。如果消费者没有在适当的时间处理它,那么它将自动从队列中删除(并转移到一个死信交换,稍后会详细介绍)。
TTL对于时间敏感的命令特别有用,这些命令在经过一段时间不进行处理后就变得无关紧要了。
消息延迟/调度
RabbitMQ通过使用插件来支持消息延迟/调度。当这个插件在消息交换中启用时,生产者可以向RabbitMQ发送消息,生产者可以延迟RabbitMQ将消息路由到消费者队列的时间。
这个特性允许开发人员提前安排任务,这并不意味着在此之前要处理。例如,当生产者碰到一些规则时,我们可能希望将特定命令的执行延迟。
Kafka不支持这些特性。当消息到达分区时,它会将消息写入分区,用户可以立即使用这些消息。
此外,Kafka没有为消息提供TTL机制,尽管我们可以在应用程序级别实现一功能。
我们还必须记住Kafka分区是一个附加事务日志。因此,它不能操作消息时间(或分区内的位置)。
胜出者
定时方面RabbitMQ轻而易举地赢得了这个胜利,kafka在这方面其实现本质限制它能力。
消息保留
一旦用户成功消费消息,RabbitMQ就会将其从存储中删除。此行为不可修改。它是几乎所有消息代理设计的一部分。
相反,Kafka通过为每个主题配置一个超时来持久化所有消息。关于消息保留,Kafka不关心它的消费者的消费状态,因为它被看成是一个消息日志。
消费者可以随心所欲地消费每条消息,并且可以通过操作其分区偏移量“及时”来回移动。Kafka会定期回顾主题中消息的时间,并清除那些过期的消息。
Kafka的性能不依赖于存储大小。因此,从理论上讲,可以在不影响性能的情况下无限地存储消息(只要节点足够大,可以存储这些分区)。
胜出者
Kafka是用来保留消息的,而RabbitMQ不是。这里没有竞争,kafka是胜利者。
故障处理
在处理消息、队列和事件时,开发人员通常认为消息处理总是成功的。毕竟,由于生产者将每个消息放置在队列或主题中,即使消费者未能处理消息,它也可以简单地重试,直到成功。
虽然这在表面上是正确的,但我们应该在这个过程中多加思考。我们应该承认,消息处理在某些场景中可能会失败。我们应该优雅地处理这些情况,即使由人工干预来处理。
在处理消息时,有两种类型故障:
1、暂时性故障——由于暂时性问题(如网络连接、CPU负载或服务崩溃)而发生的故障。我们通常可以通过重试来减少这种失败。
2、持久性故障——由于无法通过其他重试解决的永久问题而发生的故障。这些故障的常见原因是软件bug或无效的消息模式(即有害消息)。
作为架构师和开发人员,我们应该问自己:“对于消息处理失败,我们应该重试多少次?”在两次重试之间应该等待多长时间?我们如何区分短暂故障和持久故障?”
最重要的是:“当所有的尝试都失败了,或者我们遇到持久的故障时,我们该怎么办?”
虽然这些问题的答案是根据特定场景来定的,但消息传递平台通常为我们提供了实现解决方案的工具。
RabbitMQ提供了诸如重试投递和死信交换(DLX)等工具来处理消息消费失败场景。
DLX的主要思想是,RabbitMQ可以根据适当的配置自动将失败的消息路由到DLX,并在此交换中根据规则对失败的消息进一步的处理,包括延迟重试、重试计数和发送到“人工干预”队列。
本文提供了更多关于RabbitMQ中处理重试的可能模式。
这里需要记住的最重要的一点是,在RabbitMQ中,当一个消费者忙于处理和重试一个特定的消息时(甚至在将其返回到队列之前),其他消费者可以并发地处理它后面的消息。
当特定消费者重试特定消息时,整个消息处理不会停滞不前。因此,消息消费者可以根据自己的需要同步重试消息,而不会影响整个系统。
与RabbitMQ相反,Kafka没有提供任何开箱即用的机制。使用Kafka,我们需要在应用级别提供和实现消息重试机制。
此外,我们应该注意,当消费者忙于同步重试特定消息时,无法处理来自同一分区的其他消息。我们不能拒绝和重试特定的消息,并提交其后的消息,因为消费者不能更改消息的顺序。正如您所记得的,分区仅仅是一个附加日志。
可以将失败的消息提交到“重试主题”,并从那里处理重试;然而,在这种类型的解决方案中,我们失去了消息的顺序处理。
在Uber.com上找到的一个例子。如果消息处理延迟不是一个问题,那么带有充分的错误监控的Kafka解决方案可能就足够了。
胜出者
RabbitMQ在这方面上是赢家,因为它提供了一种开箱即用的机制来解决故障问题。
扩展
RabbitMQ和Kafka的性能方面有很多基准测试。虽然一般的基准测试对特定情况的适用性有限,但Kafka通常被认为比RabbitMQ有更好的性能。Kafka使用顺序磁盘I/O来提高性能。它的架构使用分区,这意味着它的水平扩展(向外扩展)比RabbitMQ更好,而RabbitMQ的垂直扩展(向内扩展)更好。大型Kafka部署通常每秒可以处理几十万条消息,甚至上百万条消息。
过去,Pivotal的RabbitMQ集群每秒处理100万条消息;然而,它是在一个30节点的集群上完成的,负载最优地分布在多个队列和交换上。
典型的RabbitMQ部署包括3到7个节点集群,这些节点集群并不一定能在队列之间优化分配负载。这些典型的集群通常可以预期每秒处理数万条消息的负载。
胜出者
虽然这两个平台都可以处理大量的负载,但Kafka通常可以比RabbitMQ更好地扩展和实现更高的吞吐量,从而赢得了这一轮的胜利。
但是,需要注意的是,大多数系统从来没有达到这些限制中的任何一个!所以,除非你正在构建下一个拥有数百万用户的软件系统,否则你不需要太在意规模,因为这两个平台都可以很好地为你服务。
消费者复杂度
RabbitMQ使用了智能代理和非智能消费者的方法。消费者注册到消费队列,RabbitMQ将消息推送给他们进行处理。RabbitMQ也有pull API;然而,它很少被使用。
RabbitMQ负责将消息分发给消费者,以及从队列(可能是dlx)中移除消息。消费者不需要担心这些。
RabbitMQ的结构也意味着,当负载增加时,队列的消费者组可以有效地从一个消费者扩展到多个消费者,而不需要对系统做任何改变。
另一方面,Kafka使用了一个非智能代理和智能消费者的方法。一个消费者组中的消费者需要协调他们之间的主题分区租约(以便一个消费者组中只有一个消费者监听特定的分区)。
消费者还需要管理和存储分区的偏移量索引。幸运的是,Kafka SDK为我们处理了这些,所以我们不需要自己管理它。
然而,当我们拥有较低的负载时,单个消费者需要并行地处理和跟踪多个分区,这在消费者端需要更多的资源。
此外,随着负载的增加,我们只能将消费者组扩展到消费者数量等于主题中的分区数量的程度。在此之上,我们需要配置Kafka来添加额外的分区。
然而,随着负载再次减少,我们不能删除已经添加的分区,从而增加了消费者需要做的工作。尽管如上所述,SDK处理这些额外的工作。
胜出者
RabbitMQ在设计上是为“非智能的消费者”设计的。因此,它是这一轮的赢家。
何时使用RabbitMQ或Kafka?
现在我们面临着一个非常重要的问题:“我们什么时候应该使用RabbitMQ,什么时候应该使用Kafka?”
如果我们总结上述差异,可以得出以下结论:
当我们有如下需求是,RabbitMQ更合适:
1、高级灵活的路由规则。
2、消息定时控制(控制消息过期或消息延迟)。
3、高级的故障处理功能,用于消费者更容易无法处理消息(暂时或永久)的情况。
4、简单的消费者实现。
Kafak更合适的情况:
1、严格的消息处理顺序
2、长时间的消息保留,包括重新使用处理过的数据。
3、 在传统解决方案无法满足需求时达到大规模的能力。
我们可以使用这两种平台实现大多数场景。然而,作为解决方案架构师的我们应该为特定场景选择最合适的工具。在做出这个选择时,我们应该同时考虑上面列出的区别以及非功能性约束。
这些约束包括以下内容:
1、开发者对平台的了解情况。
2、可用的托管云解决方案。
3、每个解决方案的操作成本。
4、sdk可用性。
在开发复杂的软件系统时,我们可能会试图使用相同的平台实现所有所需的消息传递功能。然而,根据我的经验,通常情况下,使用这两个平台都有很多优势。
例如,在一个基于事件驱动架构的系统中,我们可以使用RabbitMQ在服务之间发送命令,并使用Kafka实现业务事件通知。
这样做的原因是,事件通知通常用于事件来源、批处理操作(ETL风格),或者用于审计目的,因此Kafka的消息保留能力非常有用。
另一方面,命令通常需要消费者端的额外处理,这些处理可能会失败,需要高级的故障处理能力。
结束语
我在开始这个两篇文章的时候观察到很多开发者认为RabbitMQ和Kafka是可以互换的。我希望通过阅读这些文章能够帮助深入了解这两平台的实现,以及它们之间的技术差异。
这些差异反过来又会影响平台更好的为用户提供服务。这两个平台都很好,可以为多个场景提供服务。
然而,作为解决方案架构师,我们需要了解每个应用的需求,确定它们的优先级,并选择最合适的解决方案。