虽然在以往的项目开发过程中已经使用过RabbitMQ与Kafka,但还是不能准确并全面的总结出它们俩之间的差异。
在这之前很长一段时间一直都是把这两种技术当做等价的来看待,突然想到如果是我在某种特定业务下来做选型的话,我要怎么选呢?万一选错了,对于软件开发和后期的维护都会造成严重的影响。
所谓学而时习之,不亦说乎。温故而知新,可以为师矣。所以通过官网和参考了一些博客,做了以下整理:
宏观的差异,RabbitMQ与Kafka只是功能类似,并不是同类
RabbitMQ是消息中间件,Kafka是分布式流式系统。
RabbitMQ
被概括为“开源分布式消息代理”,用Erlang编写,有助于在复杂的路由方案中有效地传递消息,可以通过服务器上启用的插件进行扩展,高可用(队列可以在集群中的机器上进行镜像)
有队列
作为消息中间件的一种实现,RabbitMQ支持典型的开箱即用的消息队列。开发者定义一个命名队列,然后发布者向这个队列中发送消息。最后消费者通过这个命名队列获取待处理的消息。
RabbitMQ的发布/订阅模式
RabbitMQ使用消息交换器(Exchange)来实现发布/订阅模式。发布者可以把消息发布到消息交换器上而不用知道这些消息都有哪些订阅者。每一个订阅了交换器的消费者都会创建一个队列;然后消息交换器会把生产的消息放入队列以供消费者消费。消息交换器也可以基于各种路由规则为一些订阅者过滤消息。
注意:RabbitMQ支持临时和持久两种订阅类型。消费者可以调用RabbitMQ的API来选择他们想要的订阅类型
Apache Kafka
被描述为“分布式事件流平台”,用Scala和Java编写,促进了原始吞吐量,基于“分布式仅追加日志”的思想,该消息将消息写入持久化到磁盘的日志末尾,客户端可以选择从该日志开始读取的位置,高可用(Kafka群集可以在多个服务器之间分布和群集)
无队列,按主题存储
Kafka不是消息中间件的一种实现。它只是一种分布式流式系统,Kafka的存储层是使用分区事务日志来实现的。
Kafka没有实现队列。Kafka按照类别存储记录集,并且把这种类别称为主题(topic)。
Kafka为每个主题(topic)维护一个消息分区日志。每个分区都是由有序的不可变的记录序列组成,并且消息都是连续的被追加在尾部。默认情况下,Kafka使用轮询分区器(partitioner)把消息一致的分配到多个分区上。
消费者通过维护分区的偏移量(或者说索引)来顺序的读出消息,然后消费消息。单个消费者可以消费多个不同的主题,并且消费者的数量可以伸缩到可获取的最大分区数量。
所以在创建主题的时候,需要考虑一下在创建的主题上预期的消息吞吐量。在消费同一个主题的多个消费者构成的组称为消费者组中,通过Kafka提供的API可以处理同一消费者组中多个消费者之间的分区平衡以及消费者当前分区偏移的存储。
Kafka的发布/订阅模式
生产者向一个具体的主题发送消息,然后多个消费者组可以消费相同的消息。每一个消费者组都可以独立的伸缩去处理相应的负载。由于消费者维护自己的分区偏移,所以他们可以选择持久订阅或者临时订阅,持久订阅在重启之后不会丢失偏移而临时订阅在重启之后会丢失偏移并且每次重启之后都会从分区中最新的记录开始读取。
但是这种实现方案不能完全等价的当做典型的消息队列模式看待。当然,我们可以创建一个主题,这个主题和拥有一个消费者的消费组进行关联,这样我们就模拟出了一个典型的消息队列。不过这会有许多缺点,例如:消费失败不支持重试等,下面微观的差异中会有说明 。
Kafka是按照预先配置好的时间保留分区中的消息,而不是根据消费者是否消费了这些消息。这种保留机制可以让消费者自由的重读之前的消息。另外,开发者也可以利用Kafka的存储层来实现诸如事件溯源和日志审计功能。
微观差异,类似功能的不同特点
Kafka支持消息有序性,RabbitMQ不保证消息的顺序
RabbitMQ
RabbitMQ文档中关于消息顺序保证的说明:
“发到一个通道(channel)上的消息,用一个交换器和一个队列以及一个出口通道来传递,那么最终会按照它们发送的顺序接收到。”
在RabbitMQ中只要我们是单个消费者(并且通过限制消费者的并发数等于1,不过,随着系统规模增长,单线程消费者模式会严重影响消息处理能力),那么接收到的消息就是有序的。然而,一旦有多个消费者从同一个队列中读取消息,那么消息的处理顺序就没法保证了。
由于消费者读取消息之后可能会把消息放回(或者重传)到队列中(例如,处理失败的情况),这样就会导致消息的顺序无法保证。一旦一个消息被重新放回队列,另一个消费者可以继续处理它,即使这个消费者已经处理到了放回消息之后的消息。因此,消费者组处理消息是无序的。
Kafka
Kafka在消息处理方面提供了可靠的顺序保证。
Kafka能够保证发送到相同主题分区的所有消息都能够按照顺序处理。
所有来自相同流的消息都会被放到相同的分区中,这样消费者组就可以按照顺序处理它们。在同一个消费者组中,每个分区都是由一个消费者的一个线程来处理。结果就是我们没法伸缩(scale)单个分区的处理能力。
不过,在Kafka中,我们可以伸缩一个主题中的分区数量,这样可以让每个分区分担更少的消息,然后增加更多的消费者来处理额外的分区。
在消息路由和过滤方面,RabbitMQ提供了更好的支持
RabbitMQ
RabbitMQ可以基于定义的订阅者路由规则路由消息给一个消息交换器上的订阅者。一个主题交换器可以通过routingKey的特定头来路由消息。
或者,headers交换器可以基于任意的消息头来路由消息。这两种交换器都能够有效地让消费者设置他们想要消息类型,因此可以给使用者提供了很好的灵活性。
Kafka
Kafka在处理消息之前是不允许消费者过滤一个主题中的消息。一个订阅的消费者在没有异常情况下会接受一个分区中的所有消息。
作为一个开发者,你可能使用Kafka流式作业(job),它会从主题中读取消息,然后过滤,最后再把过滤的消息推送到另一个消费者可以订阅的主题。但是,这需要更多的工作量和维护,并且还涉及到更多的移动操作。
消息时序
分布式系统中,很多业务场景都需要考虑消息投递的时序,例如:
(1)单聊消息投递,保证发送方发送顺序与接收方展现顺序一致
(2)群聊消息投递,保证所有接收方展现顺序一致
(3)充值支付消息,保证同一个用户发起的请求在服务端执行序列一致
RabbitMQ
在保证消息时序方面,RabbitMQ提供了多种能力:
1)消息存活时间(TTL)
发送到RabbitMQ的每条消息都可以关联一个TTL属性。发布者可以直接设置TTL或者根据队列的策略来设置。
系统可以根据设置的TTL来限制消息的有效期。如果消费者在预期时间内没有处理该消息,那么这条消息会自动的从队列上被移除(并且会被移到死信交换器上,同时在这之后的消息都会这样处理)。
TTL对于那些有时效性的命令特别有用,因为一段时间内没有处理的话,这些命令就没有什么意义了。
2)延迟/预定的消息
RabbitMQ可以通过插件的方式来支持延迟或者预定的消息。当这个插件在消息交换器上启用的时候,生产者可以发送消息到RabbitMQ上,然后这个生产者可以延迟RabbitMQ路由这个消息到消费者队列的时间。
这个功能允许开发者调度将来(future)的命令,也就是在那之前不应该被处理的命令。例如,当生产者遇到限流规则时,我们可能会把这些特定的命令延迟到之后的一个时间执行。
Kafka
Kafka没有提供这些功能。它在消息到达的时候就把它们写入分区中,这样消费者就可以立即获取到消息去处理。Kafka也没有为消息提供TTL的机制,不过我们可以在应用层实现。
注意:Kafka分区是一种追加模式的事务日志。所以,它是不能处理消息时间(或者分区中的位置)。
Kafka支持消息留存,RabbitMQ不支持
RabbitMQ
当消费者成功消费消息之后,RabbitMQ就会把对应的消息从存储中删除,且这种设定没法修改。
Kafka
相反,Kafka会给每个主题配置超时时间,只要没有达到超时时间的消息都会保留下来。在消息留存方面,Kafka仅仅把它当做消息日志来看待,并不关心消费者的消费状态。
消费者可以不限次数的消费每条消息,并且他们可以操作分区偏移来“及时”往返的处理这些消息。Kafka会周期的检查分区中消息的留存时间,一旦消息超过设定保留的时长,就会被删除。
Kafka的性能不依赖于存储大小。所以,理论上,它存储消息几乎不会影响性能(只要你的节点有足够多的空间保存这些分区)。
RabbitMQ的容错处理优于Kafka
消息处理存在两种可能的故障:
1) 瞬时故障
故障产生是由于临时问题导致,比如网络连接或者服务崩溃等。我们可以通过多次测试来尝试减轻这种故障。
2) 持久故障
故障产生是由于永久的问题导致的,并且这种问题不能通过额外的重试来解决。比如常见的原因有软件bug或者无效的消息格式。
RabbitMQ
RabbitMQ提供了诸如交付重试和死信交换器(DLX)来处理消息处理故障。
DLX的主要思路是根据合适的配置信息自动地把路由失败的消息发送到DLX,并且在交换器上根据规则来进一步的处理,比如异常重试,重试计数以及发送到“人为干预”的队列。
在RabbitMQ中当一个消费者正在处理或者重试某个消息时(即使是在把它返回队列之前),其他消费者都可以并发的处理这个消息之后的其他消息。
当某个消费者在重试处理某条消息时,作为一个整体的消息处理逻辑不会被阻塞。所以,一个消费者可以同步地去重试处理一条消息,不管花费多长时间都不会影响整个系统的运行。
消费者1持续的在重试处理消息1,同时其他消费者可以继续处理其他消息
Kafka
Kafka没有提供这种机制。需要我们自己在应用层提供和实现消息重试机制。
注意:当一个消费者正在同步地处理一个特定的消息时,那么同在这个分区上的其他消息是没法被处理的。
由于消费者不能改变消息的顺序,所以我们不能够拒绝和重试一个特定的消息以及提交一个在这个消息之后的消息。
一个应用层解决方案:可以把失败的消息提交到一个“重试主题”,并且从那个主题中处理重试;但是这样的话我们就会丢失消息的顺序。
如果消费者阻塞在重试一个消息上,那么底部分区的消息就不会被处理
Kafka在伸缩方面更优并且能够获得比RabbitMQ更高的吞吐量
RabbitMQ
典型的RabbitMQ部署包含3到7个节点的集群,并且这些集群也不需要把负载分散到不同的队列上。
这些典型的集群通常可以预期每秒处理几万条消息。
Kafka
Kafka使用顺序磁盘I / O来提高性能。
Kafka的大规模部署通常每秒可以处理数十万条消息,甚至每秒百万级别的消息。
Pivotal公司记录了一个Kafka集群每秒处理一百万条消息的例子;但是,它是在一个有着30个节点集群上做的,并且这些消息负载被优化分散到多个队列和交换器上。
注意: 大部分系统都还没有达到这些极限(反正我目前从未参与开发过此类系统)!所以,除非你正在构建下一个非常受欢迎的百万级用户软件系统,否则你不需要太关心伸缩性问题,毕竟这两个消息平台都可以工作的很好。
RabbitMQ的消费者复杂度低于Kafka
RabbitMQ
RabbitMQ使用的是智能代理和傻瓜式消费者模式。
消费者注册到消费者队列,然后RabbitMQ把传进来的消息推送给消费者。RabbitMQ也有拉取(pull)API;不过,一般很少被使用。
RabbitMQ管理消息的分发以及队列上消息的移除(也可能转移到DLX)。消费者不需要考虑这块。
根据RabbitMQ结构的设计,当负载增加的时候,一个队列上的消费者组可以有效的从仅仅一个消费者扩展到多个消费者,并且不需要对系统做任何的改变。
Kafka
Kafka使用的是傻瓜式代理和智能消费者模式。
消费者组中的消费者需要协调他们之间的主题分区租约(以便一个具体的分区只由消费者组中一个消费者监听)。
消费者也需要去管理和存储他们分区偏移索引。不过Kafka SDK已经为我们封装了,所以我们不需要自己管理。
另外,当我们有一个低负载时,单个消费者需要处理并且并行的管理多个分区,这在消费者端会消耗更多的资源。
随着负载增加,我们只需要伸缩消费者组使其消费者的数量等于主题中分区的数量。这就需要我们配置Kafka增加额外的分区。
但是,随着负载再次降低,我们不能移除我们之前增加的分区,这需要给消费者增加更多的工作量。不过Kafka SDK已经帮我们做了这个额外的工作。
Kafka分区没法移除,向下伸缩后消费者会做更多的工作
结论
首先是在不考虑一些非功能性限制(如运营成本,开发人员对两个平台的了解等)的情况下:
优先选择RabbitMQ的条件
- 高级灵活的路由规则。
- 消息时序控制(控制消息过期或者消息延迟)。
- 高级的容错处理能力,在消费者更有可能处理消息不成功的情景中(瞬时或者持久)。
- 更简单的消费者实现。
优先选择Kafka的条件
- 严格的消息顺序。
- 延长消息留存时间,包括过去消息重放的可能。
- 传统解决方案无法满足的高伸缩能力。