作为一名经验丰富的微服务系统架构师,我经常被问到“我应该选择RabbitMQ还是Kafka?”。出于某种原因,许多开发人员将这两种技术视为等同的。确实,在某些case场景下选择RabbitMQ还是Kafka并没有什么区别,但是这两种技术在底层实现上有很多区别。不同的场景需要不同的解决方案,选择错误的解决方案会严重影响你设计、开发和维护软件的能力。本文将首先介绍RabbitMQ和ApacheKafka内部实现相关的概念。那么我们将主要介绍这两种技术的主要区别以及各自的优缺点。最后,我们将解释如何选择这两种技术。1、异步消息模式异步消息可以作为消息的生产和处理解耦的解决方案。谈到消息系统,我们通常会想到两种主要的消息模式——消息队列和发布/订阅模式。1.消息队列使用消息队列可以解耦生产者和消费者。多个生产者可以向同一个消息队列发送消息;但是,当消息被消息生产者处理时,消息将在队列中被锁定或删除,其他消费者无法处理该消息。也就是说,一条特定的消息只能被一个消费者消费。应额外注意消息队列。如果某个消费者处理消息失败,消息系统一般会将消息放回队列中,以便其他消费者继续处理。除了提供解耦,消息队列还可以独立扩展生产者和消费者,并为错误处理提供容错能力。2.发布/订阅在发布/订阅(pub/sub)模式下,一条消息可以被多个订阅者并发获取和处理。发布/订阅例如,系统中生成的事件可以由发布者通过此模式传达给所有订阅者。许多队列系统中经常使用术语主题来指代发布/订阅模式。在RabbitMQ中,主题是发布/订阅模型的具体实现(更准确地说,是一种交换),但在本文中,我将主题和发布/订阅视为等价的看待。一般而言,有两种类型的订阅:1)临时订阅,仅在消费者启动并运行时存在。一旦消费者退出,相应的订阅和未处理的消息就会丢失。2)持久(durable)订阅,除非主动删除,否则会一直存在。消费者退出后,消息系统会继续维护订阅,后续的消息可以继续处理。2、RabbitMQRabbitMQ作为消息中间件的一种实现,常用作服务总线。RabbitMQ原生支持上述两种消息模式。其他一些流行的消息传递中间件实现是ActiveMQ、ZeroMQ、Azure服务总线和亚马逊简单队列服务(SQS)。这些消息中间件的实现有很多共同点,本文提到的大部分概念都适用于这些中间件。1.队列RabbitMQ支持典型的开箱即用的消息队列。开发者可以定义一个命名队列,然后发布者可以向这个命名队列发送消息。最后,消费者可以通过这个命名队列获取待处理的消息。2.消息交换器RabbitMQ使用消息交换器来实现发布/订阅模型。发布者可以在不知道这些消息有哪些订阅者的情况下将消息发布到消息交换器。每个订阅交换的消费者都会创建一个队列;然后消息交换器将生产的消息放入队列中供消费者消费。消息交换还可以根据各种路由规则为某些订阅者过滤消息。重要的是要注意RabbitMQ消息交换支持临时和持久订阅类型。消费者可以调用RabbitMQ的API来选择他们想要的订阅类型。根据RabbitMQ的架构设计,我们还可以创建一种混合的方式——订阅者组成一个团队,然后作为消费者在组内以竞争关系的方式处理特定队列上的消息。我们称之为消费者群体的群体。这样,我们实现了发布/订阅模型,同时我们可以扩容订阅者来处理接收到的消息。发布/订阅和队列的联合使用3.ApacheKafkaApacheKafka不是消息中间件的实现。相反,它只是一个分布式流媒体系统。与基于队列和交换的RabbitMQ不同,Kafka的存储层是使用分区事务日志实现的。Kafka还提供了用于实时流处理的StreamingAPI和用于更轻松地与各种数据源集成的连接器API;当然,这些都超出了本文的范围。云供应商为Kafka存储层提供了可选的解决方案,例如AzureEventHubsy和AWSKinesisDataStreams。还有针对Kafka流功能的云特定和开源解决方案,但是,话虽如此,它们也不在本文的讨论范围之内。1.主题Kafka没有实现队列这种东西。相应地,Kafka将记录集按类别存储,并将此类类别称为主题。Kafka为每个主题维护一个分区的消息日志。每个分区由一个有序的不可变记录序列组成,并且消息被连续附加。当消息到达时,Kafka将它们附加到分区的末尾。默认情况下,Kafka使用循环分区程序在多个分区之间一致地分发消息。Kafka可以改变创建逻辑消息流的行为。例如,在多租户应用程序中,我们可以根据每个消息中的租户ID创建消息流。在物联网场景下,我们可以根据生产者的身份,将生产者映射到一个恒定级别的特定分区。确保来自同一逻辑流的消息映射到同一分区可确保消息按顺序传递给消费者。Kafka生产者和消费者通过维护分区偏移量(或索引)顺序读取消息,然后消费消息。单个消费者可以消费多个不同的主题,消费者的数量可以扩展到它可以获取的最大分区数。所以在创建主题时,我们必须认真考虑创建主题的预期消息吞吐量。消费相同主题的多个消费者组成的一组称为消费者组。Kafka提供的API可以处理同一个消费者组中多个消费者之间的分区平衡,以及消费者当前分区偏移量的存储。Kafkaconsumer2、Kafka实现的消息模式Kafka的实现很适合发布/订阅模式。生产者可以向特定主题发送消息,然后多个消费者组可以消费同一条消息。每个消费者组都可以独立扩展以处理相应的负载。由于消费者维护自己的分区偏移量,因此他们可以选择持久订阅或临时订阅。持久订阅在重启后不会丢失偏移量,而临时订阅在重启后会丢失偏移量,并在每次重启后从分区中更新。记录开始被读取。但是,这种实现方案并不能完全等同于典型的消息队列方式。当然,我们可以创建一个主题,关联一个有消费者的消费者组,这样我们就模拟了一个典型的消息队列。然而,这有很多缺点,我们将在第二部分详细讨论。值得注意的是,Kafka是根据预先配置的时间,在分区中保留消息,而不是根据消费者是否消费这些消息。这种保留机制允许消费者自由地重读以前的消息。此外,开发者还可以利用Kafka的存储层实现事件追踪、日志审计等功能。尽管有时可以认为RabbitMQ和Kafka是等价的,但它们的实现却大不相同。所以我们不能把它们当作同一种工具;一个是消息中间件,一个是分布式流媒体系统。作为解决方案架构师,我们需要能够识别它们之间的差异,并尝试考虑在给定场景中使用哪种类型的解决方案。下面指出了这些差异并提供了何时使用哪种解决方案的指导。4.RabbitMQ和Kafka的显着区别RabbitMQ是一个消息代理,而ApacheKafka是一个分布式流系统。看似从语义上可以看出区别,但是它们的一些内在特性会影响到我们能否设计好各种用例。例如,Kafka最适合流式数据,但RabbitMQ很难保持流中消息的顺序。另一方面,RabbitMQ内置了重试逻辑和死信(dead-letter)交换,而Kafka只是将这些实现逻辑留给了用户。本节主要强调它们在不同系统之间的主要区别。1.消息顺序对于发送到队列或交换器的消息,RabbitMQ不保证它们的顺序。虽然消费者按顺序处理来自生产者的消息似乎合乎逻辑,但这非常具有误导性。RabbitMQ文档中有关于消息顺序保证的描述:“发布到一个通道(channel)的消息,通过一个exchange和一个queue以及一个exitchannel传递,最终将按照它们发送的顺序被接收。”——RabbitMQ代理语义(BrokerSemantics)也就是说,只要我们是单个消费者,收到的消息都是有序的。但是,一旦多个消费者从同一个队列中读取消息,就无法保证消息的处理顺序。由于消费者可能会在读取消息后将消息放回(或重传)到队列中(例如处理失败),因此无法保证消息的顺序。一旦一条消息被放回队列,另一个消费者可以继续处理它,即使这个消费者在放回消息后已经处理了消息。因此,消费者组处理消息的顺序是乱序的,如下表所示:使用RabbitMQ打乱消息顺序的例子当然,我们可以通过限制消费者的并发数为1来保证RabbitMQ中消息的顺序。更准确地说,将单??个消费者中的线程数限制为1,因为任何并行消息处理都会导致乱序问题。但是随着系统规模的增长,单线程的消费者模型会严重影响消息处理能力。因此,我们不应该轻易选择这个选项。另一方面,对于Kafka来说,它在消息处理中提供了可靠的顺序保证。Kafka可以保证发送到同一个主题分区的所有消息都可以按顺序处理。如前所述,默认情况下,Kafka会使用循环分区器将消息放在相应的分区上。但是,生产者可以为每条消息设置分区键(key)来创建逻辑数据流(例如来自同一设备的消息,或者属于同一租户的消息)。来自同一个流的所有消息都被放入同一个分区,以便消费者组可以顺序处理它们。但是,我们也应该注意到,在同一个消费者组中,每个分区由一个消费者的一个线程处理。结果是我们无法扩展单个分区的处理能力。然而,在Kafka中,我们可以扩展一个主题中的分区数量,这允许每个分区共享更少的消息,然后添加更多的消费者来处理额外的分区。赢家:显然,Kafka是赢家,因为它保证了消息的有序处理。RabbitMQ在这方面比较薄弱。2.消息路由RabbitMQ可以根据定义的订阅者路由规则将消息路由到消息交换器上的订阅者。主题交换器可以通过名为routing_key的特定标头路由消息。或者,标头交换可以根据任意消息标头路由消息。这两种交换都有效地允许消费者设置他们感兴趣的消息类型,从而为解决方案架构师提供了极大的灵活性。另一方面,Kafka不允许消费者在处理消息之前过滤主题中的消息。订阅消费者将毫无例外地接收分区中的所有消息。作为开发人员,您可能会使用Kafka流作业从主题读取消息、过滤消息并将过滤后的消息推送到消费者可以订阅的另一个主题。然而,这需要更多的工作和维护,也涉及更多的移动操作。获胜者:RabbitMQ为消息路由和过滤提供了更好的支持。3、消息定时(timing)在确定消息发送到一个队列的时间方面,RabbitMQ提供了多种能力:1)消息生存时间(TTL)每条发送到RabbitMQ的消息都可以关联一个TTL属性。发布者可以直接设置TTL,也可以根据队列的策略设置。系统可以根据设置的TTL限制消息的有效期。如果消费者没有在预期的时间内处理消息,则该消息会自动从队列中移除(并会被移至死信交换器,后续的所有消息都将以此方式处理)。TTL对时间敏感的命令特别有用,因为这些命令如果一段时间不处理就没有什么意义。2)延时/定时消息RabbitMQ可以通过插件支持延时或定时消息。在消息交换上启用该插件后,生产者可以向RabbitMQ发送消息,然后生产者可以延迟RabbitMQ将消息路由到消费者队列的时间。此功能允许开发人员安排未来的命令,即在此之前不应处理的命令。例如,当生产者遇到节流规则时,我们可能会将这些特定命令的执行延迟到稍后的时间。Kafka不提供这些功能。它在消息到达时将消息写入分区,因此消费者可以立即获取消息进行处理。Kafka也没有使用为消息提供TTL的机制,但是我们可以在应用层实现。但是,我们必须记住一件事,即Kafka分区是一个追加模式的事务日志。因此,它无法处理消息时间(或分区中的位置)。赢家:毫无疑问,RabbitMQ是赢家,因为这种实现本质上限制了Kafka。4.消息保留(retention)当消费者成功消费消息后,RabbitMQ会从存储中删除对应的消息。无法修改此行为。它是几乎所有消息代理设计的组成部分。相反,Kafka为每个主题配置了一个超时时间,没有达到超时时间的消息将被保留。在消息保留方面,Kafka只把它当做消息日志,并不关心消费者的消费状态。消费者可以无限次地消费每条消息,并且他们可以操纵分区偏移量来“及时”地来回处理这些消息。Kafka会定期检查分区中消息的保留时间,一旦消息超过设定的保留时间,就会被删除。Kafka的性能不依赖于存储大小。因此,理论上,将消息存储在其中几乎不会影响性能(只要您的节点有足够的空间来容纳这些分区)。获胜者:Kafka旨在存储消息,但RabbitMQ不是。所以这里没有比较,Kafka胜出。推荐:最全的Java面试提纲及答案解析五、容错处理在处理消息、队列、事件时,开发者往往认为消息处理总是成功的。毕竟,生产者将每条消息放入队列或主题后,即使消费者处理消息失败,也只需要重试,直到成功。虽然这种做法表面上看起来是正确的,但对于这种做法,我们还是应该多思考一下。首先,我们应该承认在某些场景下,消息处理会失败。因此,即使在解决方案部分需要人工干预的情况下,我们也必须优雅地处理这些情况。消息处理有两种可能的故障:1)瞬态故障——由于网络连接、CPU负载或服务崩溃等临时性问题而发生的故障。我们可以通过反复尝试来减轻这种失败。2)持久性故障——故障是由无法通过额外重试解决的永久性问题引起的。常见原因的示例是软件错误或无效的消息格式(例如,有毒消息)。作为架构师和开发人员,我们应该问自己:“对于消息处理失败,我们应该重试多少次?每次重试之间应该等待多长时间?我们如何区分暂时性失败和持久性失败?”最重要的是:“所有重试失败或遇到持久性故障后我们应该做什么?”当然,不同的业务领域有不同的答案,消息系统一般都为我们提供了实现自己解决方案的工具。RabbitMQ将为我们提供诸如传递重试和死信交换(DLX)之类的功能来处理消息处理失败。DLX的主要思想是根据适当的配置信息自动将路由失败报文发送给DLX,并在交换机上按照规则进一步处理,如异常重试、重试计数并发送给“人工干预”“队列。查看以下文章,其中提供了有关RabbitMQ如何处理重试的可能模式的更多视角。链接:https://engineering.nanit.com/rabbitmq-retries-the-full-story-ca4cc6c5b493在RabbitMQ中,我们需要记住的最重要的事情是消费者何时处理或重试消息(即使在将其返回给队列),其他消费者可以在这条消息之后并发处理其他消息。当消费者重新尝试处理一条消息时,整个消息处理逻辑不会被阻塞。因此,一个消费者可以同步重试处理一条消息,无论花费多长时间,都不会影响整个系统的运行。消费者1不断重试处理消息1,而其他消费者可以继续处理其他消息。与RabbitMQ相反,Kafka不提供这种开箱即用的机制。在Kafka中,我们需要在应用层提供并实现消息重试机制。另外需要注意的是,当一个consumer在同步处理某个特定的消息时,同一个partition上的其他消息是无法处理的。由于消费者无法更改消息的顺序,因此我们无法拒绝并重试特定消息并在其后提交消息。您只需要记住,分区只是附加模式下的日志。应用层解决方案可以将失败的消息提交到“重试主题”并处理来自该主题的重试;但是这样我们就会失去消息的顺序。可以在Uber.com上找到Uber工程师实施的示例。如果消息处理延迟不是问题,那么具有足够错误监控功能的Kafka解决方案可能就足够了。如果消费者在重试消息时被阻塞,那么底部分区的消息将不会被处理赢家:RabbitMQ是赢家,因为它提供了一种开箱即用的机制来解决这个问题。6.有多个扩展基准来检查RabbitMQ和Kafka的性能。Kafka通常被认为具有优于RabbitMQ的性能,尽管通用基准测试仅限于特定情况。Kafka使用顺序磁盘I/O来提高性能。从Kafka的分区架构来看,在横向扩展上会比RabbitMQ更好。当然,RabbitMQ在垂直扩展上会更有优势。Kafka的大规模部署通常可以每秒处理数十万条消息,甚至每秒处理数百万条消息。过去,Pivotal记录了一个Kafka集群每秒处理100万条消息的示例;然而,它是在一个30节点的集群上完成的,并且消息负载最佳地分布在多个队列和交换器上。链接:https://content.pivotal.io/blog/rabbitmq-hits-one-million-messages-per-second-on-google-compute-engine典型的RabbitMQ部署由3到7个节点的集群组成,并且这些集群也不需要将负载分散到不同的队列中。这些典型的集群通常可以预期每秒处理数万条消息。获胜者:虽然这两个消息平台都可以处理大规模负载,但Kafka更擅长扩展并且可以实现比RabbitMQ更高的吞吐量,因此Kafka赢得了这一轮。但是,值得注意的是,大多数系统都没有达到这些限制!因此,除非您正在构建下一个非常流行的百万用户软件系统,否则您不必过分担心可伸缩性,毕竟这两种消息传递平台都可以正常工作。7.消费者的复杂性RabbitMQ使用智能代理和愚弄消费者的模型。消费者注册到消费者队列,然后RabbitMQ将传入的消息推送给消费者。RabbitMQ还有一个pullAPI;然而,它很少被使用。RabbitMQ管理消息的分发和队列中消息的删除(并可能传输到DLX)。消费者无需考虑这一点。根据RabbitMQ结构的设计,当负载增加时,一个队列上的消费者组可以有效地从一个消费者扩展到多个消费者,并且不需要对系统做任何改动。与RabbitMQ的高效扩展相比,Kafka使用了一种万无一失的代理和智能消费者模型。消费者组中的消费者需要协调它们之间的主题分区租约(以便特定分区仅被消费者组中的一个消费者收听)。消费者还需要管理和存储他们的分区偏移索引。好在KafkaSDK已经帮我们打包好了,不需要我们自己去管理。另外,当我们负载低的时候,单个消费者需要并行处理和管理多个分区,这会消耗更多的消费者端资源。当然,随着负载的增加,我们只需要扩展消费者组,使消费者的数量等于主题中的分区数量。这就需要我们配置Kafka添加额外的分区。但是,随着负载再次下降,我们无法删除之前添加的分区,这将需要消费者做更多的工作。尽管如此,正如我们上面提到的,KafkaSDK已经为我们完成了这项额外的工作。Kafka分区无法删除,消费者在按比例缩小时做更多的工作赢家:按照设计,RabbitMQ是为白痴消费者构建的。所以这一轮RabbitMQ获胜。五、如何选择?现在我们面临着百万美元的问题:“何时使用RabbitMQ以及何时使用Kafka?”总结以上差异,我们不难得出以下结论。选择RabbitMQ的优惠条件:先进灵活的路由规则;消息定时控制(控制消息过期或消息延迟);先进的容错处理能力,在消费者更容易处理消息失败的场景(瞬时或持久化);更简单的消费者实施。选择Kafka的优惠条件:严格的消息顺序;延长消息保留时间,包括重播过去消息的可能性;传统解决方案无法满足的高扩展性。在大多数情况下,这两个消息平台都能满足我们的要求。但是,由我们的架构师选择最合适的工具。在做出决定时,我们需要考虑上面强调的功能差异和非功能限制。这些约束如下:两个消息传递平台的当前开发人员知识;托管云解决方案的可用性(如果适用);每个解决方案的运营成本;我们的目标堆栈的SDK的可用性。在开发复杂的软件系统时,我们可能会试图为所有必要的消息传递用例使用相同的消息传递平台。但是,根据我的经验,同时使用这两个消息传递平台通常会带来更多好处。例如,在事件驱动架构系统中,我们可以使用RabbitMQ在服务之间发送命令,使用Kafka实现业务事件通知。原因是事件通知通常用于事件溯源、批处理操作(ETL样式)或审计目的,因此Kafka的消息保留功能非常有价值。相比之下,命令通常需要在消费者端进行额外的处理,并且处理可能会失败,因此需要高级容错。在这里,RabbitMQ在功能上有很多闪光点。我可能会在未来写一篇关于此的详细文章,但你必须记住——你的里程可能会有所不同,因为适用性取决于你的具体需求。6.思想总结我写这篇文章是因为我观察到许多开发人员认为RabbitMQ和Kafka是等价的。我希望在本文的帮助下,您可以对这两种技术实现以及它们之间的技术差异有一个扎实的了解。这反过来会通过它们的差异影响两个平台,以更好地服务于用例。这两个消息传递平台都很棒,并且都很好地服务于多个用例。但是,作为解决方案架构师,这取决于我们对每个用例的需求的理解,以及优化,然后选择最合适的解决方案。
