RabbitMQ vs Kafka
原文地址RabbitMQ vs Kafka Part 1 - Two Different Takes on Messaging
在本文中,我们将介绍RabbitMQ和Kafka是什么,如何实现消息队列。两者在技术决策方面大相径庭,各有千秋。本文不会给出任何结论,二是作为一个引子,以便在接下去的连载中深入探讨。
RabbitMQ
一个分布式的消息队列系统。分布式指的是RabbitMQ运行在节点集群上,消息队列的数据也分布在不同的节点上,为了容错和高可用性,节点数据也部分的被复制。RabbitMQ原生支持AMQP0.9.1, 其他协议以插件的形式支持,例如STOMP, MQTT,HTTP.
RabbitMQ的消息队列设计融合了经典与创新。经典在于它围绕着消息队列来设计,创新在于路由的高度弹性。其实,RabbitMQ的亮点就在于它的路由。构建一个分布式,可扩展,可靠的消息系统是其应有之意,而消息路由功能使得RabbitMQ在众多同类产品中十分抢眼.
交换机(exchange)与队列概述
发布者向交换机传递消息
交换机将消息路由给其他交换机或者路由给队列
消息接收后RabbitMQ发送ACK给发布者
消费者维护与RabbitMQ的TCP长连接,声明消费哪些队列
RabbitMQ将消息推送(push)给消费者
消费者发送成功还是失败的ACK
消费成功后消息即被删除
在上述概述列表背后,开发者和管理员需要做出大量决定来满足特定的消息传递保证和性能要求,我们会在后续文章中介绍。
看一个单一生产者,单一交换机,单一队列,单一消费者的例子:
如果有多个生产者向队列发布消息,且有多个消费者读取每一条消息?见下图:
如图所示,多个生产者向同一个交换机发送消息,交换机将消息路由到每一个队列,每一个队列都有一个消费者。使用RabbitMQ,支持不同的消费者消费同一个消息。与之相反,见下图:
图中有三个消费者,从同一个队列中消费。这就是竞争的消费者的例子。也就是说,消费者们互相竞争地去消费同一个队列中的消息。理论上讲,每个消费者可以消费1/3消息。竞争性消费者通常用来扩展消息处理能力,而对于RabbitMQ而言,这非常简单,只需要按需添加或减少消费者。无论存在多少竞争性的消费者,RabbitMQ保证一条消息只会被消费一次。
将上面两种模式结合在一起,我们可以组织多组竞争性消费者,而每组消费者都消费每一条消息。
在交换机和消费者之间的箭头称为绑定,我们会在另一篇文章中进一步介绍。
保证
RabbitMQ保证多一次传递(at most once delevery)和少一次传递(at least once delivery),但是不保证一次传递(exactly once delivery)。我们会在另一篇文章中介绍。
消息传递的顺序与消息进入队列的顺序相同。当然,这不能保证在竞争性消费者时,消息处理完成的顺序域消息发送的顺序相同。这与RabbitMQ的机制无关,而是并行处理有序集合的现实情况。这个问题可以通过使用一致哈希交换(Consistent Hashing Exchange)解决,如下文所示。
推动和消费者预取
RabbitMQ推送消息给消费者(pull的API已经废弃)。这可能导致消费者过载,尤其在消息生产的速度大于消费者处理它的速度。为了避免这种情况,可以配置预取限制(prefetch limit),也称为QoS限制。这个数值限定了任意时刻消费者拥有的未ACK的消息的数量,作为消费者开始落后的一个阈值。
为什么使用推送而不是拉取呢?首先,推动意味着低延迟。其次,理想情况下,在竞争性消费者时,我们希望均衡负载。但是如果使用拉取模式,负载可能不会均衡。越是不均衡,消息的延时会越大,消息处理时的乱序会越严重。这些因素使得RabbitMQ倾向于一条一条推送的模型。也是基于此,RabbitMQ的扩展性也相对受限。组合ACK可以缓解这个问题。
路由
交换机是路由功能的基本单元,将其他交换机或者队列联系起来。为了使得消息从交换机传递到队列或其他交换机,需要使用绑定。不同的交换机使用不同的绑定。有四种类型的交换机和对应的绑定。
扇出(Fanout):路由到所有与之有绑定的交换机和队列。这是标准的发布-订阅模式。
直通(Direct):基于消息中的路由键(routing key)(由生产者设定)路由消息。路由键是一个短字符串。直通交换机将消息路由到拥有对应的绑定键(binding key)的交换机和队列。
主题(topic):使用路由键路由,但是支持通配符。
头数据(Header):RabbitMQ支持在消息中加入定制的头数据。头数据交换机使用有数据进行路由。每一个绑定都有头数据对应的数据。绑定可以设置多个对应的有数据,也可设定ANY或者ALL.
一致哈希(consistent Hashing):交换机基于头数据或者路由键哈希消息到一个队列中。在扩展消费者而又希望保证消费顺序的情况下,一致性哈希可以排上用场。
上图是基于主题的交换机实例。
在另一篇文章中,我们会更深入的探讨路由。我们现在简单介绍一下上图。
生产者将错误日志发布出去,使用路由键LEVEL.AppName.
队列1将收到所有消息,因为使用了#通配符。
队列2将收到所有级别的错误日志,但是仅限于EC.WebUI这个应用。它使用*来匹配日志级别。
队列3将收到所有的ERROR级别的日志。
基于4种路由消息和交换机机制,RabbitMQ提供强大而可扩展的消息队列模式。接下去,我们继续介绍死信(Dead letter)交换机,瞬时(ephemeral)交换机和队列,读者可以见识RabbitMQ的强大之处。
死信交换机
我们可以配置队列,在以下情况,将消息传递给一个专门的交换机:
队列消息数目超出配置限制;
队列数据超出配置的字节数;
消息的生存时间(TTL)到期。发布者可以设定消息的生存时间,队列也可以拥有消息的生存时间,二者取短。
这个队列叫做死信队列。该功能可以解决不常见的问题。我们可以使用死信队列和TTL来实现延时队列和重试队列(例如指数后退策略)。
瞬时交换机和队列
交换机和队列可以被动态的创建,自动销毁(在一定的时间间隔后)。使用该机制可以支持基于消息队列的RPC,使用瞬时回复队列。
插件
你想要安装的个插件肯定是HTTP server形式的管理工具,使用WebUI和REST API。它非常容易安装,助你快速上手。基于脚本的部署同样简单。
其他插件包括:
一致性哈希交换,分片(sharding)交换等等
STOMP、MOTT协议等
web hook
SMTP 集成
RabbitMQ有很多功能,上文只是一个引子,接下去,我们介绍Kafka,它使用了完全不同策略进行消息传递,同样拥有很棒的功能。
Apache Kafka
Kafka是一个支持分布式和复制的操作日志(commit log)系统。它本身没有队列的概念,虽然它被大量使用为消息队列。我们来分解一下它的关键词。
分布式:Kafka不是在集群上,为了容错和扩展性
复制的:消息被复制并储存在集群中的若干节点
操作日志:消息被存储在分区的(partitioned),只能追加的(append-only)日志中。这种日志成为话题(topic)。这种日志的概念是Kafka的亮点。
理解Kafka的核心是理解日志和分区。那么,分区的日志跟一系列队列有什么不同呢?如图所示:
与RabbitMQ将消息存储在FIFO队列,追踪消息的状态不同,Kafka只是将消息追加到日志后。消息可以被消费一次,或者1000次。消息存储基于一种保留策略(retention policy)。那么,一个话题是如何消费的呢?每一个消费者追踪自己在日志中的位置,它拥有一个指针,只想新消费的消息,这个指针称作偏移(offset).消费者通过客户端的库维护偏移,基于Kafka的版本,这个偏移存储在zookeeper中或者在kafka中。zookeeper是一个分布式协调技术,用于很多分布式系统,例如选取(leader election)。Kafka依赖zookeeper进行集群管理。
kafka这种日志模型的奇妙之处在于,可以绕过很多消息传递状态的复杂性,并且对于消费者而言,可以允许回退功能。举例而言,你部署一个服务,计算发票。该服务有一个bug,24小时内的发票计算出错了。使用RabbitMQ,你必须得重新发布发票信息,但是使用Kafka,你只需要回退到24小时前。
我们来看一个例子。一个话题,一个分区,两个消费者,都想消费所有消息。从现在开始,我会标注消费者,因为图示不如RabbitMQ那般直接(要么是独立消费者,要么是竞争消费者)。
如图示,两个独立的消费者消费同一个分区,但是他们有不同的偏移。获取发票服务处理消息比较慢,而推送通知服务比较快;又或者发票服务宕机了一段时间后再次启动;又或者发票服务有一个bug,必须回退回去一段时间。
现在,如果发票服务需要扩展到三个实例(因为处理速度跟不上),在RabbitMQ中,我们只需要简单的增加两个发票服务的应用即可。但是,kafka不支持单个分区上的竞争性消费,kafka的并行粒度是分区级别。所以,如果我们需要三个发票处理消费者,我们必须拥有三个分区。如图:
这个例子想表达的是,你需要使得至少分区数目与大扩展数目相当。我们再谈谈分区。
分区和消费者组
每个分区是一个独立的数据文件,保证顺序。这一点很重要,顺序只在分区里被保证。这可能会引入性能和并行度之间的一些冲突。一个分区不支持竞争性消费者,所以我们的发票服务只能用一个实例对应一个分区。
消息可以被路由到分区,使用round robin方式或者通过哈希函数:hash(key)%分区数。使用哈希有好处,例如我们可以设计哈希函数,使得同样的内容路由到同样的分区。这可以支持很多消息分发模式和顺序要求。
消费者组类似于RabbitMQ中的竞争消费者。每个消费者消费话题中的一部分消息。与RabbitMQ中每个消费者从同一个队列取消息不同,在Kafka中,每个消费者从各自的分区读取消息。所以,上例中的三个发票服务实例,属于同一个消费者组。
从这点看,RabbitMQ稍微灵活一点,它保证全队列的顺序,并且可以动态的改变竞争性消费者的数目。而对于Kafka,如何组织数据的分区更加重要。
但是,对于顺序和并行度,Kafka也有自己的优势,而RabbitMQ在后续版本才更新的。那就是,RabbitMQ维护队列的全局有序,但是在分布式处理时,顺序就无法保证了。但是,kafka虽然不能维护全局有序,但是在分区级别,他可以维护顺序。所以,如果你需要部分顺序,那么Kafka不仅提供有序的消息分发,也提供有序的消息处理。想想一下,你有消息表示客户的新预定信息,你想按顺序处理。如果你基于预定号码进行分区,那么所有相关的消息会到达同一个分区,并且是有序的。如此,你可以创建大量的分区,使得你的处理高度并行并且保证顺序。
这个功能在RabbitMQ中同样支持,那就是通过一致性哈希交换机。当然了,kafka通过消费者组和分区的概念原生支持,而RabbitMQ需要你自己动手。
需要注意的是,当你改变分区数目时,原本的分区映射会发生改变,是的相关消息会分布到两个分区。有时这确实是个问题,因为顺序被打乱了。我们会在后续文章中继续深入探讨消息传递的语义和保证。
推送与拉取
RabbitMQ使用推送模式,同事使用预取限制防止消费者过载。这非常有利于低延时的消息通信,并且域RabbitMQ的队列模型协作的非常好。而Kafka使用拉取模型,消费者从特定的偏移拉取数据消费。为了避免轮训,kafka支持long-polling。
kafka使用拉取模型是因为它同时使用了分区模型。因为kafka保证了分区内的顺序,我们可以使用批量处理来提高吞吐率。这对于RabbitMQ而言意义不大,因为RabbitMQ更倾向去均衡负载,一条一条处理。
发布-订阅
kafka支持基本的发布-订阅。同时,因为kafka存在分区的概念和日志的概念,kafka还支持其他一些模式。在kafka中,生产者追加记录于日志中,而消费者根据偏移,可以在任意位置进行消费。
常见的模式如图:
当然,我们并不需要使得消费者组中的消费者数目与分区数目一样。如下图也是可以的:
在一个消费者组中的消费者会互相协调消费的分区,使得一个分区的数据不会被两个消费者消费。
类似的,如果消费者数目多有分区,多余的消费者会处于等待状态。
在消费者增减过后,消费者组可能是不平衡的。kafka会出发重平衡,使得消费者尽量均匀的分布与分区中。
重平衡在以下情况下出发:
一个消费者加入了消费者组
一个消费者离开了消费者组(停机或异常)
加入了新的分区
重平衡将导致短暂的服务延时。内存中的任何数据和状态可能不再有效。kafka消费的一种模式,是可以路由一些特定的消息(比如预定消息),到相同的分区,继而到相同的消费者。这叫做本地性(locality).当发生重平衡时,内存数据与状态将变得,除非重平衡后的对应分区与重平衡前的对应分区相同。所以,如果消费者维护一些信息,那么这些必须维护在外部。
日志压缩
标准的数据保留策略是基于时间和空间的。比如保存直到上周的多50GB数据。当时,另外还有一种数据保留策略——日志压缩。当日志压缩后,结果会是,每个消息键(message key)的新数据会被保留,而其余会被删除。
让我们设想,我们收到用户预定信息的消息。每次预定更改时,一个新的事件会产生。因此一个话题中,可能有一些消息,针对同一个消息,但是包含不同的新状态。当话题被压缩后,只有新状态的消息被保存下来。
取决于预定数据的体量,你可以选择将所有数据都保留下来。如果周期性的压缩话题,我们可以确保对一个预定,仅保留一条消息。+
消息顺序——补遗
我们已经讲到,扩展并维护消息顺序在RabbitMQ和Kafka中都是可行的,当然,kafka更加简单。使用RabbitMQ,必须使用一致性哈希交换机,手动的实现消费者组的逻辑(使用Zookeeper或者Consul)。
但是,RabbitMQ有一个kafka没有的特性,不止针对RabbitMQ,而是对于其他所有发布2-订阅的基于队列的消息系统。那就是,基于队列的消息系统允许订阅者排序。
具体讲,不同的应用不能共享队列,因为他们是竞争的。他们需要自己的队列。这给了应用足够的自由,去配置他们自己的队列。他们可以路由来自不同话题的多种事件到自己的队列。这使得应用可以自己维护相关事件的顺序。不同的应用可以配置不同的事件。
这个特性在基于日志的消息系统中是不行的。因为日志是共享的。多个应用读取同一个日志。所以,相关事件组织成单个话题是更高层的家伙决定的。
没有哪个消息队列是全能型选手。RabbitMQ允许你维护更广泛的事件的顺序,kafka则提供简单的方式可扩展的维护顺序。
结论
RabbitMQ提供了瑞士军刀级别的消息模式,基于它提供的多种功能。使用强大的路由,它可以减轻消费者获取,反序列化以及筛选的工作。它易于使用,可动态扩展消费者数目。他的插件架构可以提供其他协议的支持。
kafka的分布式日志,配合消费者的偏移,使得回退变成可能。它可以把相同键值的消息路由到相同的消费者,并且保证顺序,使得高度并行化并且有序的处理变成可能。Kafka的日志压缩和数据保留是RabbitMQ不支持的。后,必须承认,Kafka的扩展性比RabbitMQ更佳,当然,大多数情况下,我们不会碰到巨量的消息,所以两者都能胜任。
相关文章