RabbitMQ,ZeroMQ,Kafka 是一个层级的东西吗?相互之间有哪些优缺点?

属于分布式消息队列相关的问题。
关注者
1,966
被浏览
471,952

36 个回答

先说ZeroMQ。这东西叫【Zero】MQ,其实压根就不是我们传统意义上的MQ。它是一个用于网络编程的SDK,目标是给常见的网络通讯方式提供一个更好用的接口,在没有“broker”的情况下实现网络通讯。

ZeroMQ (also spelled ØMQ, 0MQ or ZMQ) is a high-performance asynchronous messaging library, aimed at use in distributed or concurrent applications. It provides a message queue, but unlike message-oriented middleware, a ZeroMQ system can run without a dedicated message broker.
……
The philosophy of ZeroMQ starts with the zero. The zero is for zero broker (ZeroMQ is brokerless), zero latency, zero cost (it’s free), and zero administration.

比如,想实现一个1个req/resp的机制。用常规的socket是个相当麻烦的事。从客户端要connect,按照报格式序列化数据,发数据,接收数据,关闭连接;而服务端要listen,accept,读取数据,发送数据……这还不算每个步骤的错误处理,缓冲区的管理,多路复用……。(所以普通人估计直接就上http了)。再比如fanout,fanin,master-worker……这些模式也都相当麻烦。而ZeroMQ提供了SDK可以帮助开发者快速实现这些功能。

不过ZeroMQ提供的某些模式已经带有了很明显的的broker的特征。不知道作者是如何考虑的。
高级Req/Resp模式
高级pub/sub模式

但个人认为ZeroMQ适合用来搭建一个框架,而并非被直接被业务使用。比如,ZeroMQ完全不解决消息高可用的的问题。消息落盘这种事并不在ZeroMQ的scope里。因为它和常规MQ的差异过于巨大(除了名字里带mq,和mq没有半毛钱关系),后边就不再讨论了。从应用角度,除非是做网络编程的,我也不太建议进一步深入了解它。

RabbitMQ代表了传统的broker为中心的MQ,其设计和信箱很像。被发送的消息经过1个或者多个broker的处理,最终进入一个消费者的信箱(queue)。消费者正确处理后,这个message就被删除了。如果存在多个不同的,彼此独立的consumer,可以设置各自独立的queue,各不影响。这里说”以broker为中心“就是着重强调”转发能力”。一个broker可以根据路由规则进行投递,可以fanout,可以根据tag做部分message的过滤。多个broker之间还可以“接力”。message还能有TTL生命周期。在复杂的企业级信息系统里实现message通讯和协作。RabbitMQ也满足多个标准,如AMQP,MQTT,STOMP等。遵守这些协议可以帮助上游在多个MQ上做迁移。

这类MQ从使用者角度,用起来非常方便。producer只管发,consumer只管收。业务逻辑都是broker上面配置。

但这种设计也带来一些根本性的问题,让他非常不适合在一些场景中使用。

  • 首先是保序性。RabbitMQ(或者这类MQ)都只能保证在单broker+单consumer+不自动进入死信队列的情况下实现保证严格的顺序。但是单broker+单consumer是不可扩展的,无法实现更高的吞吐。类似于数据库Syncer之类需要严格顺序的服务就无法实现了。
  • 因为存在多个queue,因此一个message可能被写入多次,存在写放大的问题。但既然支持了多个queue,就不可能不复制message。那么吞吐就会受到极大的影响。类似于日志流式处理的场景就无法支持了。
  • 一个message被消费了,就会被清理。那么如果一个流处理存在bug,产生了错误的结果,就意味着无论如何都无法重新修正了。除非数据源可以把messge重新发一遍。如果MQ拓扑很复杂,这几乎就不可能。类似的,也没法做到“半夜把白天的数据用批处理重新跑一遍”之类的事情。

所以Kafka从一开始就实现了一个和传统的MQ完全不同的”MQ“。(我感觉叫他logbase可能更合适)它的出现一开始就是面向大数据,高吞吐的场景。尽管Kafka里也有个“broker”。但这个broker干的事情和RabbitMQ的“根据业务逻辑转发和处理”完全不同。Kafka的broker仅仅是以理论上最快的速度来将消息写入,供consuemr读取。在磁盘上,最快的写入就是顺序写(当时HDD还大量存在,HDD的顺序写和随机写的性能差异比SSD大得多)。因此Kafka的broker就是将数据的以“Append only”的形式写入文件。(对比传统MQ一般会用某种数据库)。这个Append only file模型上实际就是个log(留意这里的log的意思并不是业务日志,是指总是追加写的数据模型)。

当然,Kafka为了提高吞吐,还采用了很多其他的办法,比如双buffer,IO多路复用,零拷贝等。但Append Only File是Kafka(以及kafka这一类MQ,如RocketMQ)在数据模型上最显著的不同。

而consumer不会各自维护独立的“queue”,而是都直接读取同一份log文件。只不过不同consumer各自维护不同的读取位置。在早期的版本中这些位置被存在zk里,后来改到了虚拟的topic。而message的删除和谁消费完全没有关系,是需要独立配置的,比如可以配置为7天过期。只要存储够大,可以保证message一直不丢失,可以反复消费(只要调整读取位置就行了)。

并且,对于同一个log,message一旦写入,位置就不会变了。因此同样一个文件的任意consumer都可以保证顺序。这个文件在概念上被称为一个topic的partition。简单说就是Kafka的partition+consumer可以保证顺序。而Kafka可以保证对于各一个partition最多只能有一个consumer线程消费。因此对于这个consumer可以完全不担心顺序(但可以启动多个consumer消费同一个partition,除了活跃的那个consumer,其余的可以当做热备)。

这样,kafka就实现了一个可以极大吞吐的,有顺序保证的,可以被反复消费log文件队列。而log这种数据模型,其实是很多数据系统的内部核心数据结构。比如在mysql中有binlog负责主从同步,redo log负责数据恢复;在raft中每个节点都会维护一个log做total order broadcast等等。

传统MQ只负责转发和被消费前临时存储,而Kafka本身就是数据,因此Kafka像数据库那样认真的考虑了数据高可用的问题,实现了多副本,自动切主等功能。

在2010~2015年,因为大数据流式处理的兴起,Kafka成为数据分析领域无可替代的中间件(后来才有了RocketMQ和pulsar)。而对于大多数线上处理,只需要消息1跳的场景,不需要灵活配置转发规则的场景,Kafka也可以用。且吞吐高,相对的省资源,还可以省一套运维,顶多就是费点磁盘(便宜得很)。因此很多公司会选择运维几套不同的Kafka集群同时支持线上和离线业务。

所以你会发现,

  • 收集线上log可以用kafka
  • ETL可以用kafka。
  • 多数据中心数据同步可以用kafka。
  • 线上发个报警做推送通道可以用kafka。
  • 做数据库降级处理的请求暂存可以用kafka。
  • 线上业务削峰填谷抗流量还可以用kafka。
  • 实现CQRS也可以用Kafka。
  • ……

kafka如万金油一般的存在。kafka用起来就像是一个”log库“,基于它可以搭建各种各样的设施。相比之下,传统MQ你不会太关心他的存在和配置,仅仅是写消息和读消息而已。

所以我不是很喜欢那种“横评“——RabbitMQ支持什么什么,Kafka支持什么什么。这种横评传达的意味是二者可以直接对比。但实际上他们用起来感觉完全不一样。

Kafka本身极大的降低了broker模型的复杂度,因此不支持各种骚功能:

  • 想要多集群间转发?kafka broker不支持,一开始需要自己写代码consume+process+produce。后来kafka基于这个模式提供了stream api,还支持“事务”(exactly once processing)
  • 想要延迟消息?kafka不支持,需要自己写个业务服务定时去pull。为了大流量转发需求,可以考虑自己实现一个时间轮的服务。
  • 想要死信队列?没有built-in,但你可以自己弄个topic当死信队列。(但使用死信队列也就意味着必定无序)

同时为了性能,不可避免的发送和消费需要耦合一点点业务。比如多个partition的分发,只有业务逻辑上才能知道哪些消息是有序的。因此业务上自己要定义partition key。为了性能业务一般也要自己对消息做序列化(json,avro,pb等)。运维为了一致性要多搞一套zk(新版kafka已经在逐渐去zk化,但做法是自己实现一套分布式consensus协议)。

次时代的MQ会考虑在保留Kafka的优点的基础之上,再引入一些传统MQ的能力(比如延迟队列),以及一些全新的能力(比如租户隔离,事务消息,审计,资源池……)。找机会再细讲。

ZeroMQ我没用过。RabbitMQ和Kafka都用过很久。我可以说一下Kafka和RabbitMQ的区别,各自适合什么场景。

经常有人问我

有个 xx 需求,我应该用 Kafka 还是 RabbitMQ ?

这个问题很常见,而且很多人对二者的选择也把握不好。

同时,这个问题在面试中也经常问到。

下面我会通过 6 个场景,来对比分析一下 Kafka 和 RabbitMQ 的优劣。

一、消息的顺序

有这样一个需求:当订单状态变化的时候,把订单状态变化的消息发送给所有关心订单变化的系统。

订单会有创建成功、待付款、已支付、已发货的状态,状态之间是单向流动的。

好,现在我们把订单状态变化消息要发送给所有关心订单状态的系统上去,实现方式就是用消息队列。

在这种业务下,我们最想要的是什么?

  1. 消息的顺序:对于同一笔订单来说,状态的变化都是有严格的先后顺序的。
  2. 吞吐量:像订单的业务,我们自然希望订单越多越好。订单越多,吞吐量就越大。

在这种情况下,我们先看看 RabbitMQ 是怎么做的。

首先,对于发消息,并广播给多个消费者这种情况,RabbitMQ 会为每个消费者建立一个对应的队列。也就是说,如果有 10 个消费者,RabbitMQ 会建立 10 个对应的队列。然后,当一条消息被发出后,RabbitMQ 会把这条消息复制 10 份放到这 10 个队列里。

当 RabbitMQ 把消息放入到对应的队列后,我们紧接着面临的问题就是,我们应该在系统内部启动多少线程去从消息队列中获取消息。

如果只是单线程去获取消息,那自然没有什么好说的。但是多线程情况,可能就会有问题了……

RabbitMQ 有这么个特性,它在官方文档就声明了自己是不保证多线程消费同一个队列的消息,一定保证顺序的。而不保证的原因,是因为多线程时,当一个线程消费消息报错的时候,RabbitMQ 会把消费失败的消息再入队,此时就可能出现乱序的情况。

T0 时刻,队列中有四条消息 A1、B1、B2、A2。其中 A1、A2 表示订单 A 的两个状态:待付款、已付款。B1、B2 也同理,是订单 B 的待付款、已付款。

到了 T1 时刻,消息 A1 被线程 1 收到,消息 B1 被线程 2 收到。此时,一切都还正常。

到了 T3 时刻,B1 消费出错了,同时呢,由于线程 1 处理速度快,又从消息队列中获取到了 B2。此时,问题开始出现。

到了 T4 时刻,由于 RabbitMQ 线程消费出错,可以把消息重新入队的特性,此时 B1 会被重新放到队列头部。所以,如果不凑巧,线程 1 获取到了 B1,就出现了乱序情况,B2 状态明明是 B1 的后续状态,却被提前处理了。

所以,可以看到了,这个场景用 RabbitMQ,出现了三个问题:

  1. 为了实现发布订阅功能,从而使用的消息复制,会降低性能并耗费更多资源
  2. 多个消费者无法严格保证消息顺序
  3. 大量的订单集中在一个队列,吞吐量受到了限制

那么 Kafka 怎么样呢?Kafka 正好在这三个问题上,表现的要比 RabbitMQ 要好得多。

首先,Kafka 的发布订阅并不会复制消息,因为 Kafka 的发布订阅就是消费者直接去获取被 Kafka 保存在日志文件中的消息就好。无论是多少消费者,他们只需要主动去找到消息在文件中的位置即可。

其次,Kafka 不会出现消费者出错后,把消息重新入队的现象。

最后,Kafka 可以对订单进行分区,把不同订单分到多个分区中保存,这样,吞吐量能更好。

所以,对于这个需求 Kafka 更合适。

二、消息的匹配

我曾经做过一套营销系统。这套系统中有个非常显著的特点,就是非常复杂非常灵活地匹配规则。

比如,要根据推广内容去匹配不同的方式做宣传。又比如,要根据不同的活动去匹配不同的渠道去做分发。

总之,数不清的匹配规则是这套系统中非常重要的一个特点。

首先,先看看 RabbitMQ 的,你会发现 RabbitMQ 是允许在消息中添加 routing_key 或者自定义消息头,然后通过一些特殊的 Exchange,很简单的就实现了消息匹配分发。开发几乎不用成本。

而 Kafka 呢?如果你要实现消息匹配,开发成本高多了。

首先,通过简单的配置去自动匹配和分发到合适的消费者端这件事是不可能的。

其次,消费者端必须先把所有消息不管需要不需要,都取出来。然后,再根据业务需求,自己去实现各种精准和模糊匹配。可能因为过度的复杂性,还要引入规则引擎。

这个场景下 RabbitMQ 扳回一分。

三、消息的超时

在电商业务里,有个需求:下单之后,如果用户在 15 分钟内未支付,则自动取消订单。

你可能奇怪,这种怎么也会用到消息队列的?

我来先简单解释一下,在单一服务的系统,可以起个定时任务就搞定了。

但是,在 SOA 或者微服务架构下,这样做就不行了。因为很多个服务都关心是否支付这件事,如果每种服务,都自己实现一套定时任务的逻辑,既重复,又难以维护。

在这种情况下,我们往往会做一层抽象:把要执行的任务封装成消息。当时间到了,直接扔到消息队列里,消息的订阅者们获取到消息后,直接执行即可。

希望把消息延迟一定时间再处理的,被称为延迟队列。

对于订单取消的这种业务,我们就会在创建订单的时候,同时扔一个包含了执行任务信息的消息到延迟队列,指定15分钟后,让订阅这个队列的各个消费者,可以收到这个消息。随后,各个消费者所在的系统就可以去执行相关的扫描订单的任务了。

RabbitMQ 和 Kafka 消息队列如何选?

先看下 RabbitMQ 的。

RabbitMQ 的消息自带手表,消息中有个 TTL 字段,可以设置消息在 RabbitMQ 中的存放的时间,超时了会被移送到一个叫死信队列的地方。

所以,延迟队列 RabbitMQ 最简单的实现方式就是设置 TTL,然后一个消费者去监听死信队列。当消息超时了,监听死信队列的消费者就收到消息了。

不过,这样做有个大问题:假设,我们先往队列放入一条过期时间是 10 秒的 A 消息,再放入一条过期时间是 5 秒的 B 消息。 那么问题来了,B 消息会先于 A 消息进入死信队列吗?

答案是否定的。B 消息会优先遵守队列的先进先出规则,在 A 消息过期后,和其一起进入死信队列被消费者消费。

在 RabbitMQ 的 3.5.8 版本以后,官方推荐的 rabbitmq delayed message exchange 插件可以解决这个问题。

  • 用了这个插件,我们在发送消息的时候,把消息发往一个特殊的 Exchange。
  • 同时,在消息头里指定要延迟的时间。
  • 收到消息的 Exchange 并不会立即把消息放到队列里,而是在消息延迟时间到达后,才会把消息放入。

再看下 Kafka 的:

Kafka 要实现延迟队列就很麻烦了。

  • 你先需要把消息先放入一个临时的 topic。
  • 然后得自己开发一个做中转的消费者。让这个中间的消费者先去把消息从这个临时的 topic 取出来。
  • 取出来,这消息还不能马上处理啊,因为没到时间呢。也没法保存在自己的内存里,怕崩溃了,消息没了。所以,就得把没有到时间的消息存入到数据库里。
  • 存入数据库中的消息需要在时间到了之后再放入到 Kafka 里,以便真正的消费者去执行真正的业务逻辑。
  • ……

想想就已经头大了,这都快搞成调度平台了。再高级点,还要用时间轮算法才能更好更准确。

这次,RabbitMQ 上那一条条戴手表的消息,才是最好的选择。

四、消息的保持

在微服务里,事件溯源模式是经常用到的。如果想用消息队列实现,一般是把事件当成消息,依次发送到消息队列中。

事件溯源有个最经典的场景,就是事件的重放。简单来讲就是把系统中某段时间发生的事件依次取出来再处理。而且,根据业务场景不同,这些事件重放很可能不是一次,更可能是重复 N 次。

假设,我们现在需要一批在线事件重放,去排查一些问题。

RabbitMQ 此时就真的不行了,因为消息被人取出来就被删除了。想再次被重复消费?对不起。

而 Kafka 呢,消息会被持久化一个专门的日志文件里。不会因为被消费了就被删除。

所以,对消息不离不弃的 Kafka 相对用过就抛的 RabbitMQ,请选择 Kafka。

五、消息的错误处理

很多时候,在做记录数据相关业务的时候,Kafka 一般是不二选择。不过,有时候在记录数据吞吐量不大时,我自己倒是更喜欢用 RabbitMQ。

原因就是 Kafka 有一个我很不喜欢的设计原则:

当单个分区中的消息一旦出现消费失败,就只能停止而不是跳过这条失败的消息继续消费后面的消息。即不允许消息空洞。

只要消息出现失败,不管是 Kafka 自身消息格式的损坏,还是消费者处理出现异常,是不允许跳过消费失败的消息继续往后消费的。

所以,在数据统计不要求十分精确的场景下选了 Kafka,一旦出现了消息消费问题,就会发生项目不可用的情况。这真是徒增烦恼。

而 RabbitMQ 呢,它由于会在消息出问题或者消费错误的时候,可以重新入队或者移动消息到死信队列,继续消费后面的,会省心很多。

坏消息就像群众中的坏蛋那样,Kafka 处理这种坏蛋太过残暴,非得把坏蛋揪出来不行。相对来说,RabbitMQ 就温柔多了,群众是群众,坏蛋是坏蛋,分开处理嘛。

六、消息的吞吐量

Kafka 是每秒几十万条消息吞吐,而 RabbitMQ 的吞吐量是每秒几万条消息,Kafka的吞吐量更大。

其实,在一家公司内部,有必须用到 Kafka 那么大吞吐量的项目真的很少。大部分项目,像 RabbitMQ 那样每秒几万的消息吞吐,已经非常够了。

在一些没那么大吞吐量的项目中引入 Kafka,我觉得就不如引入 RabbitMQ。

为什么呢?

因为 Kafka 为了更好的吞吐量,很大程度上增加了自己的复杂度。而这些复杂度对项目来说,就是麻烦,主要体现在两个方面:

1、配置复杂、维护复杂

Kafka 的参数配置相对 RabbitMQ 是很复杂的。比如:磁盘管理相关参数,集群管理相关参数,ZooKeeper 交互相关参数,Topic 级别相关参数等,都需要一些思考和调优。

另外,Kafka 本身集群和参与管理集群的 ZooKeeper,这就带来了更多的维护成本。Kafka 要用好,你要考虑 JVM,消息持久化,集群本身交互,以及 ZooKeeper 本身和它与 Kafka 之间的可靠和效率。

2、用好,用对存在门槛

Kafka 的 Producer 和 Consumer 本身要用好用对也存在很高的门槛。

比如,Producer 消息可靠性保障、幂等性、事务消息等,都需要对 KafkaProducer 有深入的了解。

而 Consumer 更不用说了,光是一个日志偏移管理就让一大堆人掉了不少头发。

相对来说,RabbitMQ 就简单得多。你可能都不用配置什么,直接启动起来就能很稳定可靠地使用了。就算配置,也是寥寥几个参数设置即可。

所以,大家在项目中引入消息队列的时候,真的要好好考虑下,不要因为大家都鼓吹 Kafka 好,就无脑引入。

总结

可以看到,如果我们要做消息队列选型,有两件事是必须要做好的:

  1. 列出业务最重要的几个特点
  2. 深入到消息队列的细节中去比较

等我们对这些中间件的特点非常熟悉之后,甚至可以把业务分解成不同的子业务,再根据不同的子业务的特征,引入不同的消息队列,即消息队列混用。这样,我们就可能会最大化我们的获益,最小化我们的成本。

说了这么多,其实还有很多 Kafka 和 RabbitMQ 的比较没有说,比如二者集群的区别,占用资源多少的比较等。以后有机会可以再提提。

总之,期待大家看完这篇文章后,能对 Kafka 和 RabbitMQ 的区别有了更细节性的了解。

最后,分享一个网上的比较全的对比图:

原创不易,求个赞。

看完是不是觉得很干货?我还写了很多程序员相关的学习技巧、架构经验、职场生存法则类的优质文章,总结成了一份电子书,我相信你看完之后,对你一定会对你有帮助。想获取电子书可以戳下面链接。