从消费者角度比较Kafka 与 RabbitMQ - OpenCredo


对于大型分布式系统,Kafka 往往是更好的选择。它可以更有效地横向扩展,为更大的场景实现更好的吞吐量,包括消费者离线和不可用时。RabbitMQ 非常适合具有较低延迟要求的系统,消费者可以跟上消息的生产,但可能对并行吞吐量处理要求较低。 
RabbitMQ 和 Kafka 遵循非常不同的消息传递方法。RabbitMQ 是更传统的消息代理系统,但它利用了 AMQP 交换提供的额外灵活性,这在某些运维上下文中是有益的。Kafka 是一个完全不同的系统,专门为解决分布式系统大规模处理海量数据(例如吞吐量、排序、高可用性等)的问题而设计。
消息和事件驱动的系统为各种形式和规模的组织提供了一系列好处。它们的核心是帮助生产者和消费者解耦,这样每个人都可以按照自己的节奏工作,而不必等待另一个——最好的异步处理。
事实上,此类系统支持一系列消息传递模式,围绕客户端的处理和消费选项提供不同级别的保证。以发布/订阅模式为例,它使一条消息可以被多个消费者广播和消费;或者竞争消费者模式,它使一条消息能够被处理一次,但多个并发消费者争夺荣誉——本质上提供了一种分配负载的方法。然而,这些模式的实际实现方式在很大程度上取决于所使用的技术,因为每种模式都有自己的方法和独特的权衡。 
在本文中,我们将探讨这一切如何应用于 RabbitMQ 和 Apache Kafka,以及这两种技术有何不同,特别是从消息消费者的角度来看。
 
RabbitMQ 概述
RabbitMQ是一种流行的开源消息代理,它通过将消息推送给消费者来工作。消费者处理并确认每条消息,然后从队列中删除该消息。RabbitMQ 支持多种标准协议,包括AMQP(主要协议)、STOMP 和 MQTT。RabbitMQ 服务器是用Erlang编写的并依赖于Erlang,但是有多种语言的客户端库可以与之交互。
与其他传统的排队系统相比,RabbitMQ 有一个(好的)转折点:交换(AMQP 协议的一部分)。使用 RabbitMQ,发布者通常会发布到交换器,然后交换器负责将该消息路由到零个或多个队列,具体取决于实施的策略,例如直接扇出。不同的交换类型与其他元数据(如路由键和标头)相结合,为用户提供了极大的灵活性。
RabbitMQ 中的队列可能是持久的(即在代理失败的情况下,尚未处理的消息将在重新启动时继续存在),但消息本身在消费时会被销毁。代理负责跟踪并确保它知道所有合适的客户端何时处理了消息,以及何时推送更多消息。 
这种架构方法和实现对消费者有影响。例如,实现 pub/sub 模式需要明确的决策和预先构建,知道哪个合适的交换(在这种情况下使用扇出)和队列作为构建块。然而,实现竞争消费者模式并不一定需要您担心使用哪种类型的交换,而只需担心多个消费者可以连接到同一个队列(在扩展和消息传递排序方面有注意事项)。 
 
Kafka概览
Apache Kafka不仅仅是一个消息传递系统,它还是一个成熟的分布式事件流平台(也处理消息)。Kafka 使用自定义协议并部署为基于 JVM 的解决方案,该解决方案也依赖于 Apache ZooKeeper,尽管后者正在构建为依赖项。这已经可用于2.8.0 版的早期访问预览。Kafka 同样拥有以大多数流行编程语言提供的客户端集成库。
Kafka 是基于拉取的,因为消费者有责任以他们认为合适的速度和速度拉取和消费消息。在幕后 Kafka 使用分布式仅追加日志,它在基本级别上与git相当。生产者将消息附加到日志中,就像将提交添加到 git 存储库一样,不同的消费者可以按照自己的节奏处理这些消息,类似于 git 分支指向不同提交的方式,并且可以在不影响历史记录的情况下自由移动。
事实上,在 Kafka 中,消息是持久的,并且会根据保留设置在系统中保留一段时间。相同的消息可以被不同的消费者多次消费,或者实际上被同一个消费者多次消费。 
Kafka 使用主题和分区的概念,其中分区是用于实现其令人印象深刻的可扩展性特性的主要机制。在基本层面上,可以将主题想象为一个队列,其中包含感兴趣的消费者订阅的特定类型的消息(在发布/订阅的意义上)。然而,主题被细化为分区(即分片),消息根据一个键路由到不同的分区。因此,例如,相同国家/地区代码的消息将始终在同一分区中结束。
消息的这种持久性与 Kafka 基于分区的架构相结合,提供了一种自然的方式来实现发布/订阅模式,其中可以添加多个新消费者,而不必从消费者的角度预先重新构建任何东西。竞争的消费者模式同样很容易适应,尽管它确实需要引入一个新的“消费者群体”概念。但除此之外,Kafka 还为消费者提供了重放消息的能力,这进一步开辟了更多事件/消息模式的可能性,例如Event Sourcing
 
吞吐量和排序
在 RabbitMQ 中,您可以为同一个队列拥有多个并行消费者——但无需做任何特别的事情,这是以排序为代价的。这是因为并行消费者进程不能保证能够从队列中挑选消息并按顺序处理。虽然单个队列中的消息是有序的,但不幸的是,将所有内容限制在单个队列中确实会削弱可扩展性,最终也会削弱整体消息排序。
为了解决排序问题,RabbitMQ 从 3.8 版本开始引入了Single Active Consumer的概念。这与Kafka消费者群体非常相似.
使用此设置,虽然您可能有多个注册的队列消费者,但在任何给定时间只有一个消费者处于活动状态,并且这是从队列中消费的消费者。如果活动消费者被取消或死亡,它可以故障转移到另一个注册消费者。
因此,虽然您通过这种方式获得了顺序保证,但您仍然存在一次只有一个消费者的吞吐量问题。
要解决此问题,您需要将其与附加的Consistent Hash ExchangeSharded Plugins结合起来. 这些插件为交换提供了一种将传入消息一致地拆分为多个队列的方法。
这假设您的数据能够以对有序处理有意义的方式进行分区或分片。例如,ID 为 1-100 的所有客户都可能进入一个队列,101-200 进入另一个队列,等等。能够对每个分区或共享队列使用单个活动消费者,引入了以前不可用的并行级别,从而提高吞吐量,同时保留该特定队列的有序处理。请注意,无法保证跨队列的有序处理。    

另一方面,Kafka 通过利用其本机分区和消费者组功能来完成上述所有工作。这是从一开始就内置的,通过其内置的分区架构,可扩展性和吞吐量得到提高,同时仍然保持分区内的有序处理。与 RabbitMQ 附加组件一样,不能保证跨分区排序。虽然 Kafka 允许一个分区有多个消费者,但这些消费者独立处理消息。为了确保消费者之间一致的有序处理,您需要将它们组织成一个逻辑消费者组. 这样的一个组在第一个实例中提供了一个高度可用的消费者池,但是其架构使得在任何给定时间只有一个从分区中消费,尽管如果它死了另一个可以接管。如果您可以将主题拆分为适当的分区,消费者就可以同时使用该主题,而不会影响消息排序。 
 
路由
路由是 RabbitMQ 的秘诀。使用不同类型的交换结合生产者发送的特定数据(路由密钥和/或标头),消息可以路由到不同的队列或复制到多个队列。该系统可以轻松配置为确保消费者只收到他们需要的消息。这是非常有效的,并提供了一种方法来最小化消费者需要处理的时间、空间和数据量。 
另一方面,Kafka 没有实现开箱即用的路由。这将过滤掉不需要的消息的负担交给了消费者自己,或者需要引入中间过程。在后一种情况下,这些中间过程通常需要过滤,然后将消息子集转发/复制到新主题,然后成为感兴趣的消费者的新订阅点。虽然在某些情况下,分区可用于模拟过滤形式(例如,按国家/地区代码对主题进行分区,以便每个国家/地区拥有不同的消费者),但分区的存在主要是为了实现扩展,因此它们不一定总是用于路由和过滤合身自然。如果没有周密的计划,这种方法在处理和存储方面可能是浪费的。
 
消费者的高可用性
使用 RabbitMQ,同一队列上的多个消费者提供更高的吞吐量和高可用性,但如前所述,竞争的消费者不尊重消息排序。因此,平衡吞吐量、高可用性和排序需要评估和平衡权衡:

  • 如果排序很重要,您需要 在任何给定时间将自己限制为每个队列的单个活动消费者。这牺牲了高可用性和吞吐量。
  • 您可以通过为该队列保留一个消费者池来实现高可用性,同时通过某种形式的分布式锁控制访问(单个活动消费者可以做到这一点,包括在发生故障时故障转移到不同的注册消费者)。然而,这仍然牺牲了吞吐量。
  • 您可以通过Consistent Hash ExchangeSharded Plugins等附加组件获得更高的吞吐量(同时保持高可用性和按队列排序)。  

使用 Kafka,您同样需要考虑如何使用分区架构和消费者组的组合来实现这一点,尽管这更自然地开箱即用。 
  • 如果排序很重要,您需要为单个分区(全局主题级别排序很重要)构建架构,或者确保您拥有可以保留部分排序(在每个分区内)的分区。这将与消费者组的使用相结合,以将一个分区的处理一次限制为一个消费者。  
  • 使用消费者组时高可用性是开箱即用的,但必须注意确保组中的消费者数量与主题中的分区数量一致。 
  • 更高的吞吐量来自于并行处理分区的能力

 
延迟
RabbitMQ 旨在垂直扩展,最繁重的工作发生在内存中。您需要小心确保队列积压保持相当小(理想情况下为空),以便消费者可以跟上,并且代理不会不堪重负。众所周知,具有不可预测消费模式的复杂消费者(例如,有些消费者正在等待其他下游流程)如果无法跟上,则在这种情况下会出现问题。
另一方面,Kafka 旨在横向扩展。Kafka 以更低的延迟换取更好的耐用性和可用性;保留和存储方法(见下一小节)意味着它可以处理更高的吞吐量需求,并且系统的稳定性不会受到临时消费者中断的威胁。
因此,对于较小的吞吐量场景,RabbitMQ 通常可以实现更好的端到端延迟,在这种情况下,消费者可以保证能够跟上消息的生产,但这是以较低的吞吐量、持久性和可用性组合为代价的。这是这个基准测试中的一个结论(但请注意,这是由 Confluent 自己发布的)。
 
储存
在 RabbitMQ 中,队列是持久的,因为它们可以在代理中断时幸存下来。另一方面,消息保留是基于确认的,因为消息一旦被确认就会从队列中删除。如果优化延迟,您自然会尝试保持队列小且移动,因此您的存储将被最小化。也就是说,现在存储非常便宜。  
与此同时,Kafka 使用基于策略的消息保留。它可以配置为保留数天的消息历史记录,允许在以后处理消息——也许一些消费者离线,或者需要一些重新处理。但是,在这种情况下观察消费者组滞后至关重要,因为如果消费者无法跟上超过保留期的积压,消息仍然可能丢失。然而,这对于 Kafka 来说往往不是问题,因为保留政策通常非常慷慨。并且如果您有良好的监控和警报(请参阅 James Bowkett 的Kafka、Devops 和 Resilience for all talk ),这应该能够得到控制。
总体而言,消费者在 RabbitMQ 中可以一次性处理消息,而 Kafka 消费者具有更大的灵活性,支持消息重放和事件溯源等模式。