各种消息队列对比,使用场景和问题

210 阅读14分钟

什么是消息队列,使用场景有哪些

消息队列(Message Queue,即MQ)是一种应用程序之间的通信方法,用于在不同的进程或系统中传递消息或数据。消息队列的使用场景主要有:

  • 解耦:A服务可能要依赖于B服务,如果不用MQ那么AB强耦合;如果使用MQ,A服务只需要把消息放到MQ中,B服务可以直接从MQ中获取消息并执行操作,AB实现解耦。
  • 异步:若A服务依赖于BCD服务,那么A服务需要等待BCD完成才能最终返回响应,而使用MQ时A可以把消息放入MQ,直接返回,BCD从MQ中读取消息执行操作即可,即ABCD实现异步
  • 削峰:面对突然的高并发请求,MQ可以先接收请求,把请求放到队列中,再慢慢处理,避免高并发流量压垮系统的关键组件。
  • 此外还有一些具体场景,例如日志处理,消息广播等

常见消息队列的对比

  • Kafka
    • 高吞吐量,低延迟
    • 分布式架构,使用副本机制确保数据不丢失,支持持久化
    • 同一个Partition中可以保证消息有序
    • 主要用于实时数据管道(消息总线),大数据量业务如日志采集
  • RocketMQ
    • 参考Kafka设计,拥有Kafka的很多优点,主要是可靠性高,因为RocketMQ主要是为了金融互联网领域研发的,在电商中的订单扣款,秒杀交易等场景比较适用
  • RabbitMQ
    • 使用erlang语言开发(具有高并发性),但吞吐量比Kafka小
    • 使用镜像队列集群实现多副本并提供消息持久化能力
    • 用于复杂消息路由,提供延时队列机制
    • 不保证有序

MQ基本概念

  • Producer:消息生产者,负责生产和发送消息到Broker
  • Broker:消息处理中心,负责存储,确认,重试等,一般包含多个Queue
  • Comsumer:消息消费者,负责从Broker中获取消息并处理
  • 消息队列模式
    • 点对点模式:一个消息只能被一个消费者消费
    • 发布/订阅模式:一个消息可以被多个订阅者并发获取并处理

Kafka

Kafka是什么

一个分布式的,支持多分区,多副本,基于Zookeeper的分布式消息流平台。

相关概念

  • Broker
    • Kafka集群中的一个节点,接收生产者的消息,为消息设置偏移量并保存,处理消费者的拉取
  • Topic
    • 生产者将消息发送到指定的Topic,消费者从Topic中读取数据
  • Partition
    • 每个Topic被分割为多个Partition,每个Partition有多个副本,这些副本存储在不同broker上,用于保证高可用。
    • 分区内的数据通过offset保证有序性,且一个分区只能对应消费者组中的一个消费者。如果消费者变动还要重平衡。
    • 生产者发送消息到哪个分区或消费者消费哪个分区的策略包括顺序轮询,随机和指定消息key,默认使用轮询。
    • Kafka消息由key和value组成,key指定消息应该存储在哪个Paritition中,value为消息本身。
  • Offset
    • Offset是消息的唯一标识符,每个Partition中的每个消息都有一个顺序递增的Offset,保证Partition内的消息有序。
    • 消费者通过提交offset来告知Kafka已经读取消费了哪些消息,offset可以自动提交也可以设置手动提交。
    • 位移主题:Consumer提交的offset会被保存到位移主题中,如果自动提交offset,即使没有消息,消费者也会不停往位移主题中写入offset。Kafka会定期删除位移主题中的过期消息。
  • HW (High Watermark)俗称高水位
    • 它标识了一个特定的offset,消费者只能拉取到这个offset之前的消息。
  • LEO(Last End Offset)
    • 标识当前日志文件中下一条待写入的消息的offset,也就是日志文件记录的最新的数据。Parition的每个副本都会维持自身的LEO,这些副本中LEO的最小值即为高水位,消费者只能消费高水位之前的数据,这保证了kafka的数据可靠性。
  • 消费者组
    • 多个消费者实例的集合,它们共同订阅一个或多个Topic,并且每个Partition只能分配给组内的某一个消费者(一个消费者可以消费多个分区)。
  • Rebalance
    • 重平衡,消费者组内增减消费者时需要重新分配Partition,或Partition数量发生变更。
  • 消息传递方式
    • Kafka中,生产者使用push模式将消息发送到broker,消费者使用pull模式从broker订阅消息。
    • 对于消费者,使用push和pull模式都有缺点,push很难适应消费速率不同的消费者,若push太快消费者会拒接,若push太慢则浪费消费者性能。pull在broker没有消息时消费者也会不断地轮询,需要设置在没有消息时让消费者阻塞。
  • 消息压缩机制
    • 生产者端可以配置参数使用压缩算法,当消息到达消费者端后由消费者进行解压。
  • ZooKeeper
    • Kafka使用ZooKeeper来管理各种配置和元数据,协调各组件间的关系
    • Broker,Topic,Partition(Leader和Follower),消费者都要在Zookeeper上注册,Zookeeper会管理它们的消息,并维护它们之间的关系,例如在消费者组成员变化时做Rebalance
    • Zookeeper负责Kafka集群上的Leader选举,Leader与Follower间的数据同步

Kafka为什么快

  • 分区机制
    • Kafka的Topic被分割为多个分区,每个分区可以独立地被消费,从而实现消息的并行处理
  • 零拷贝技术
    • 零拷贝指减少了从磁盘读数据到发送数据这一过程的拷贝次数和系统调用次数。(传统方式需要4次,而零拷贝最多2次)。Kafka传输层的接口使用了零拷贝技术,直接将页缓存中的数据发送到网卡的Buffer中。
  • 异步提交+批量发送
    • Kafka发送消息的接口是异步的,即不直接把消息发给broker端,而是先把数据写入内存然后返回(这样上层看起来很快),等到内存中聚集了一定量的消息,再批量发送,减少了网络IO次数
  • 数据落盘优化
    • broker收到消息后,先同步写数据到操作系统的page cache中,然后再异步刷数据到磁盘中,并且刷磁盘时使用了顺序追加的方式,而不是随机写,提高了刷盘速度。

Kafka如何保证数据不丢失(生产者和消费者的数据一致性问题)

首先要想在哪里可能发送数据丢失(三个阶段),然后如何感知数据丢失,最后数据丢失会引发什么问题,怎么办。

哪里可能发送数据丢失,如何处理

  • 生产者到Broker
    • 设置ack参数,要求leader接收到消息后要等所有follower都同步到了消息才认为写消息成功并回复ack,生产者要通过回调收到broker的ack才认为消息发送成功。ack参数可设为0,1,-1:
      • 0表示生产者将数据发送出去就不管了,数据可靠性差
      • 1是默认值,表示数据发送到leader,leader成功接收就回复确认,但此时若leader宕机会造成数据丢失
      • -1表示生产者需要等待所有follower回复ack才算发送成功,可靠性最高
  • Broker本身存储
    • 每个Partition上的数据会被同步到其他节点形成副本,通过存储副本保障高可用性
    • 可能某个broker宕机导致leader下线,但leader中还有一部分数据未同步到follower,选举出的新leader没有这部分数据。Kafka设置了几个参数来避免这个问题:
      1. 要求每个partition至少要有两个副本,即一主一从,并且leader至少与一个follower保持联系,确保自己挂了还有节点可以继位
      2. 要求每条数据必须写入其所有副本后才认为是写入成功
  • Broker到消费者
    • 关闭自动提交offset,必须在消费者处理逻辑成功后手动提交offset

如何感知数据丢失?

  • 设置一种消息检测机制,在生产者端给每个发出的消息指定一个全局唯一id 或版本号,在消费端做对应的版本校验(可以使用拦截器实现)

Kafka如何保证高可用

  • Kafka提供了HA机制,每个partition会生成副本,这些副本中会选举出leader,其他为follower。只有leader对外提供读写操作,follower负责保存副本数据,并在leader宕机后被选举为新Leader。
  • 写数据时生产者将数据写入leader,leader将其持久化,其他follower主动来leader拉取数据进行同步,同步完成后发送ack给leader,leader收到所有follower的ack后才认为消息写成功,消费者才能读取到。
  • 通过设置参数指定follower与leader的同步标准,即follower能够落后于leader的最长时间,默认10s。
  • Kafka集群中有多个broker节点,它们之间也要选举出leader,主要是通过在Zookeeper中创建临时节点来实现。

Kafka如何保证消息不被重复消费

  • 在redis中使用消息id保证幂等性,生产者生成唯一消息id保存到redis,消费者要检查redis中有消息id并删除后,才能执行业务。存在redis中的消息id删除成功但是业务执行失败的问题。
  • 在数据库建一张消息日志表,包含唯一消息ID和消息执行状态,生产者生成消息后先持久化到数据库并发送到kafka(事务),消费者需要先去mysql检查消息状态并执行业务逻辑后更新消息状态(事务)。定期扫描状态异常的消息并重新发送。但是与数据库交互过多,数据库压力大。
  • Kafka的生产者本身也有幂等性的设置,它会用Id标识生产者发送的消息的唯一性,不过只能保证Partition内的消息幂等性。

Kafka如何保证消息的顺序性

  • 同一个partition内的消息是有序的(offset保证),且每个partition只分配给消费者组中的单个消费者,所以把需要有序的消息放到同一个Partition中即可(生产者通过指定key控制消息分发到哪个partition)。
  • Kafka不保证跨分区的消息顺序性,所以如果需要跨分区的消息顺序性,需要在应用层处理,例如给所有消息设置id,并加入优先队列排序,让消费者优先处理id靠前的消息。另外,如果消费者是分布式的,还需要借助redis等中间件实现优先队列保证有序性。
  • 若一个消费者开启多线程,我们可以设置N个内存队列,对消息再次hash取模分发到不同的内存队列中,每个线程消费一个内存队列,相当于再模拟了一次Kafka分区。

Kafka如何解决消息积压

  • 消息积压原因一般是消费者消费能力不足。出现了消息积压要先临时扩容,增加消费者数量,同时降级一些非核心业务,减少生产者数量。然后排查问题,优化消费端的业务处理逻辑
  • 注意扩容时除了消费者数量还要扩容分区数,确保消费者和分区数相等,因为一个分区只能被一个消费者消费
  • 消息积压太多时也可考虑把消息批量取出并持久化到数据库,离线慢慢处理

为什么Kafka不支持读写分离

Kafka中读写消息都是在Leader上的,主要是因为读写分离有主从延迟问题,数据一致性问题

Kafka的consumer和Partition数量如何控制

  • 首先consumer不要大于partition,因为partition上无法并发,最多只允许被一个consumer消费
  • consumer比partition少时,一个consumer会对应多个partition,最好使得partition数量是consumer数量的整数倍,不然可能导致partition中的数据被取的不均匀。并且consumer消费多个partition不能保证数据间的顺序性。
  • 另外,增减consumer,broker,partition会导致rebalance,consumer对应的partition可能会发生变化。

RabbitMQ

RabbitMQ是什么

实现了高级消息队列协议(AMQP)的消息中间件,比起性能和吞吐量,AMQP更关注数据一致性,稳定性。

相关概念

  • Broker:RabbitMQ服务器,集群由多个broker组成
  • 消息:消息中包含路由键,优先级,是否需要持久性存储等信息
  • 队列:用于存储消息,队列可被一个或多个消费者订阅
  • 交换机:接收生产者的消息并将其路由到一个或多个队列中。交换机类型有direct,fanout,topic等。
    • direct需要路由键完全匹配,是单播模式。
    • topic通过模式匹配路由键分配消息
    • fanout不处理路由键,而是把消息广播到交换机绑定的所有队列上。
  • 绑定/路由键
    • 交换机基于路由键将消息发送到对应的队列,交换机与路由键的这种关系即为绑定

RabbitMQ工作流程

生产者

  1. 生产者先连接到Broker,开启信道
  2. 生产者声明交换机,队列并设置好相关属性
  3. 生产者通过路由键绑定交换机和队列
  4. 生产者发送消息到Broker,其中包含路由键,交换器等信息
  5. 相应的交换器根据接收到的路由键查找匹配队列,如果找到将消息存入队列中,如果没有找到则丢弃或退回给生产者

消费者

  1. 消费者连接到Broker,开启信道
  2. 向Broker请求消费队列中的消息,可能会设置响应的回调函数
  3. 等待Broker回应并投递队列中的消息
  4. 消费者接收消息并回复ACK(默认),MQ从删除对应消息

保证消息可靠性

  • 生产者到RabbitMQ
    • 生产者发送消息给MQ,MQ将消息持久化后才回复ACK。
    • 生产者收到ACK后才确认消息发送成功,若ACK丢失,生产者会超时重传。若重试失败,可将消息持久化到数据库,然后定期扫描重发
  • RabbitMQ自身
    • 配置镜像队列,使用集群模式在多个节点上存储队列副本,写消息时会自动同步到各个节点,即使某个节点宕机,其他节点仍可继续提供服务。
  • RabbitMQ到消费者
    • 消费者拿到消息并完成业务后发送ACK给MQ,通知MQ删除消息(需要设置,默认一收到就回复ACK)。
    • 如果消费者处理失败,它可以不发送确认或回复nack,MQ会重试或重投给其他消费者

延迟队列

  • RabbitMQ延迟队列可以实现定时任务,由消息TTL机制和死信Exchange配合。(延时队列可以实现未付款订单超时自动取消操作)
  • 生产者将消息发给MQ,MQ将其放到延时队列中(无人监听),队列过期时间30分钟,消息变为死信交给死信交换机,交换机将消息路由到指定的队列中处理
  • 进入死信路由的其他情况:
    • 消息被消费者拒收且不设置重新放回队列给其他消费者使用
    • 队列满了,排在前面的消息会被放到死信路由上