在分布式系统中,MQ的身影随处可见,它常用于异步处理、系统解耦、流量削峰等场景。常用的中间件也很多,比如Kafka、RabbitMQ、RocketMQ、NSQ等等。介绍这些中间件的书籍和资料已汗牛充栋,因此本文不打算深入MQ的各种细节,仅从宏观层面的几个关键点来看下Kafka和RabbitMQ的设计思想:
-
push or pull
-
消息投递模式
-
消息投递语义
-
消息存储
-
消息顺序性
-
消息可靠性
-
事务
push or pull
消费消息的推拉模式各有优缺点,具体到Kafka,消费者采用pull模式消费消息。Kafka官网对此也有专门介绍push vs. pull.
而RabbitMQ同时支持push和pull两种消息消费方式,两种方式通过不同的api来进行区分。
消息投递模式
常用的消息投递模式有两种:
-
点对点(P2P,Point-to-Point),一条消息最多只会被一个消费者消费
-
发布/订阅(Pub/Sub),一条消息可以被多个消费者消费
Kafka同时支持这两种消息投递模式,当所有消费者属于同一个消费者组时,每条消息只会被一个消费者消费,这就相当于是P2P模式;当所有消费者属于不同组时,每条消息会被广播给所有消费者,此时就相当于是Pub/Sub模式。
RabbitMQ通过一定的规则将消息经过交换器路由后存储在队列中,根据交换器类型的不同,同一条消息可以被路由给不同的队列,消费者最终消费的是队列中存储的消息。
从交换器层面上来说,RabbitMQ可以支持P2P模式和Pub/Sub模式,但从队列层面,RabbitMQ只支持P2P模式。
消息投递语义
消息中间件的消息投递语义常见的有三个层级:
-
最多一次(At Most Once)。消息可能会丢失,但不会重复。
-
最少一次(At Least Once)。消息可能会重复,但不会丢失。
-
恰好一次(Exactly Once)。每条消息会且只会被传输一次。
Kafka生产端支持最少一次投递语义。
由于多副本机制的存在,只要消息写入了broker就不会丢失,如果传输过程发生异常,生产者可以进行重试以确保消息正确提交。
Kafka消费端则同时支持最多一次和最少一次。
如果消费者先处理消息后提交位移,那么可能导致重复消费,也就是最少一次;如果小提交位移后处理消息,则可能导致消息丢失,也就是最多一次。
另外,通过引入幂等和事务这两个特性,Kafka还在生产端和消费端同时实现了恰好一次的投递语义。
Kafka官网对此也有专门介绍:Kafka message delivery semantics
RabbitMQ目前只支持最多一次和最少一次的投递语义。为了支持最少一次的语义,RabbitMQ的生产端需要开启事务机制或者生产者确认机制,确保消息可以被传输到RabbitMQ中,同时消息和队列都需要进行持久化处理。
消息存储
消息存储无非就是内存还是磁盘,而Kafka选择了直接将消息存储到磁盘中。这似乎与我们印象中的Kafka性能很高而磁盘速度极慢的事实相矛盾。对此,Kafka官网也进行了深入分析并提供了数据支撑:Kafka persistence
总结下来大概是这么个意思:
-
写入速度:顺序写内存 > 顺序写磁盘 > 随机写内存 > 随机写磁盘
-
大量使用了页缓存来实现高吞吐量
-
使用了零拷贝技术
RabbitMQ将消息保存在队列中,但其存储略为复杂,它分为持久化的消息和非持久化的消息,且两者都可以写入磁盘。随着系统负载的变化,保存在队列中的消息还可能会经过4种状态:
-
alpha:消息内容和索引都存储在内存中
-
beta:消息内容保存在磁盘中,而消息索引保存在内存中
-
gamma:消息内容保存在磁盘中,消息索引同时保存在内存和磁盘中
-
delta:消息内容和索引都保存在磁盘中
消息顺序性
Kafka通过消息在分区中的offset来保证消息的分区有序,但是因为offset并不跨越分区,因此无法保证主题有序。
另外在生产端异步发送场景下,Kafka也通过回调函数来保证分区有序。
但是,生产者重试场景,可能会破坏消息的分区有序性。因此,在需要严格保证消息有序性的场景下,可以将参数max.in.flight.requests.per.connection(该参数用来设置每个连接最多缓存的请求数)设置为1,此时即使生产者重试,也能保证消息的分区有序。
RabbitMQ则无法保证消息的顺序性。
消息可靠性
Kafka在保证消息可靠性上使用了以下几种手段:
-
多副本机制
-
acks机制(acks参数配置为-1或者all,那么生产者在消息发送之后,需要等待所有同步副本都成功写入成功后才能厚道来自服务端的响应,如此一来可以最大程度提高消息可靠性)
-
如果某一时刻所有follower副本都被踢出了同步副本集合,那么多副本机制和acks机制都将无法起到应有的作用,消息丢失风险大大增加。考虑到这种场景,Kafka提供了参数min.insync.replicas来指定同步副本集合中最小的副本数,如果不满足条件就会抛出异常。
-
消息回溯机制。即消费者可指定分区offset进行消费。
RabbitMQ引入了镜像队列机制来保证消息可靠性。顾名思义,镜像队列可以将队列镜像到集群中的其它broker中去。针对每一个配置了镜像的队列,会包含一个master节点和若干个slave节点。
事务
Kafka中的事务可以将生产消息、消费消息、提交消费位移等操作当做原子操作来处理,也正是由于这个特性,Kafka实现了大多数消息中间件所不支持的恰好一次语义。
RabbitMQ支持事务机制,但只能保证消息被写入broker,无法保证消息被正确消费。因此,即使同样支持事务机制,但是RabbitMQ依然无法像Kafka一样提供恰好一次的投递语义。
更多技术文章,咱们公众号见,我在公众号里等你~

网友评论