消息队列

Posted by 令德湖周杰伦 on 01-29,2024

1.应用场景

消息队列(Message Queue)简称MQ,是分布式系统中常用的系统组件,用来交换信息的一种技术,应用场景包括:

  • 应用解耦
  • 异步通信
  • 流量削峰
  • 广播
  • etc

2.MQ的构成

2.1 模型

  1. 订阅pub/发布sub 模型
  2. 点对点

2.2 构成

包括生产者、消费者、Broker构成,其中 Message消息体,根据不同的通信协议定义的固定格式进行编码的数据包,来封装业务消息,实现业务的传输,

2.2.1 Broker

消息代理者,主要负责存消息的存储和转发的核心服务。其中消息转发分为2种方式:

  1. push,broker主动将消息推送给对应consumer
    1. 优点:实时性
    2. 缺点:broker会不断向消费者发送消息,受限于消费者的消费能力,可能造成消息的堆积,
  2. pull,consumer主动从broker种拉取相应的消息
    1. 根据消费方消息速度进行消息拉取
    2. 存在延时,实时性不能保证

通常一组对应关系的生产者和消费者通过topic来维系

2.2.2 生产者Producer

负责生产消息,将业务消息封装成Message,发送到broker。

2.2.3 消费者Consumer

负责消费消息,broker将收到的消息保存,并转发给消费者,消费者根据消费到的消息做相应的业务逻辑。

3.消费语义

3.1 At most once

消息至多被消费一次
特点:

  • 消息可能会丢失,但绝不重传
  • 因为不需要考虑考虑消息的丢失问题,吞吐量大
  • 实现简单
  • 不会重复投敌,所以不会存在消息重复的问题

实现:分阶段考虑

  1. msg从producer发送到broker阶段:broker不需要对接受到msg做确认,producer也不用关心是否发送到broker
  2. broker阶段:
    1. broker 存储不需要持久化
    2. 转发消息,不需要考虑consumer是否真的收到
  3. consumer阶段:consumer接受到消息后,broker可以直接删除消息,且不需要考虑consumer消费msg的情况如何(有没有真的收到,是不是需要重发)

3.2 At least once

消息至少被消费一次
特点:

  • 消息可以重传,但绝不丢失
  • 不能容忍消息的丢失,但是允许消息可以重复消费

实现:

  1. msg从producer发送到broker阶段:
    1. broker必须对接受到msg做确认
    2. producer如果没有收到broker的ack,需要重发
  2. broker阶段:
    1. broker 对接受到的消息必须持久化
    2. 转发消息,broker必须接受到consumer的ack才能删除消息
  3. consumer阶段:
    1. 必须完成消费,给broker发送ack,通知broker删除消息
    2. 消费方,需要做好幂等操作

3.3 Exactly once

消息仅被消费一次,
特点:

  • 每一条消息只被传递一次。
  • 实现比较复杂,需要对消息的唯一性做好识别

这里的仅被消费一次,通常指的是:Producer 上产生的消息被 Consumer 仅消费一次。

实现:

  1. msg从producer发送到broker阶段:

    1. broker必须对接受到msg做确认
    2. producer必须为消息生产唯一标识,broker用来判断,如果收到重复的消息,不在进行处理
  2. broker阶段:

    1. broker 对接受到的消息必须持久化,
    2. 不能有重复消息(每条消息在其消费队列里有唯一标识)
    3. 转发消息,broker必须接受到consumer的ack才能删除消息
  3. consumer阶段:

    1. 必须完成消费,给broker发送ack,通知broker删除消息
    2. 消费方,对消息做记录,如果收到相同的消息不再进行消费

4. 高级特性

4.1 消息重复

产生消息重复原因:

  1. 网络问题,producer 第一次就发送成功,但由于网络问题没有收到ack,导致procucer重复发送消息
  2. 可能因为 broker 的消息进度丢失,导致消息重复投递给 consumer
  3. 消费成功,但是消费业务系统崩溃,导致消费进度没有同步到broker, broker重复转发消息
  4. 大多数消息队列实现,为了考虑性能,消费进度是异步同步给broker,也可能会产生重复消息

解决方案:
最简单的方式:消费方做好消息幂等操作

  1. 业务自己实现:
    1. 使用数据库做记录,消费时进行判断,eg: 根据 id + status做唯一索引,如果有记录代表已消费过该消息
    2. 分布式锁,eg: 使用redis等
  2. 框架统一封装,原理和业务实现一样,只不过数据维度多了一个系统字段而已。

4.2 可靠性

消息的队列的可靠性,主要表现在消息数据会不会丢失的问题。还是从消息发送到消费过程,分阶段来看

4.2.1 生产者消息可靠性保证

  • 生产者可以通过消息重试的操作
  • 只到收到broker的ack

4.2.2 broker消息可靠性保证

当broker收到消息后,有2个选择:

  1. 消息存储起来后,然后给生产者回复ack
  2. 先回复ack,然后再异步存储

存储可以理解为就是刷盘方式,对应以上的方式就是

  1. 同步刷盘,带来了可靠性,也会一定程度降低性能。
  2. 异步刷盘,数据可能丢失,例如机器突然挂掉了,但会大大提升性能

一般broker都会使用高可用设计,例如主、从架构的broker,还会存在同步复制和异步复制的方式。

常见的broker方式:选择同步复制,加主从 Broker 都是和异步刷盘,这样在可靠性和性能之间做一个平衡。

4.2.3 消费者可靠性保证

消息到消息之后,需要主动ack到broker,broker收到确认后,更新topic对应的offset,如果没有收到ack,那么会重复推送,做好消息重复消费的幂等即可。
有的消息队列,例如kafka,消费到消息后会自动提交 offset ,需要关闭自动提交 offset ,改为处理完逻辑后自己手动提交 offset

4.3 消息顺序性

消息顺序性是指:消费消息的顺序要和生产的消息顺序一致
主要思路:

  1. 生产者发送消息时有顺序
  2. 消费者在消费消息时有顺序

4.3.1 局部顺序性 和 全局顺序性

局部是指根据业务的特性进行划分,例如在用户A发送消息的场景下,我们保证用户A的消息是顺序性即可,不需要对全部用户的消息进行顺序。

在rocketMQ中,顺序消息主要指的是都是 Queue 级别的局部顺序

  1. 生产者顺序性
    1. 生产者发送的时候可以用 MessageQueueSelector 为某一批消息发送到一个 Queue 上,eg:相同userId hash 到 一个queue上
    2. Producer 单线程顺序发送,且发送到同一个Queue,这样 Consumer 就可以按照 Producer 发送的顺序去消费消息(需要根据业务实际来决策)
    3. 这一批消息的消费将是顺序消息(并由同一个consumer完成消息)
    4. 或者设置 Message Queue 的数量只有 1 ,但这样消费的实例只能有一个,多出来的实例都会空跑
  2. 消费顺序性
    1. Consumer 消费消息的时候是针对Queue顺序拉取并开始消费
    2. 且一条 Queue 只会给一个消费者(集群模式下)
    3. 能够保证同一个消费者实例对于Queue上消息的消费是顺序地开始消费(不一定顺序消费完成,因为消费可能并行)

在Kafka中,顺序消息是指从单个分区顺序消费消息

  1. 生产者顺序性
    1. topic设置一个分区,消息顺序发送到这一个分区,会降低吞吐量
  2. 消费顺序性
    1. 对每个 Partition 内部单线程消费,单线程吞吐量太低,一般不会用这个
    2. 拉取到消息后,写到N个内存 queue,具有相同 key 的数据都到同一个内存 queue,N可以根据业务key hash 来搞,每个内存queue使用一个线程来消费

总结:根据实际业务来决策,一般是使用局部顺序性 + 消费顺序性 来实现

4.3.1 普通顺序消息 和 严格顺序消息

普通顺序消息:

  1. 正常情况下可以保证完全的顺序消息
  2. 如果broker发生宕机或重启,由于队列总数发生发化,消费者会触发负载均衡,而默认地负载均衡算法采取哈希取模平均,这样负载均衡分配到定位的队列会发化,使得队列可能分配到别的实例上,则会短暂地出现消息顺序不一致
  3. 如果业务能容忍短暂的乱序,使用普通顺序方式比较合适

严格顺序消息:

  1. 无论什么情况下都需要保证消息的顺序
  2. 牺牲了分布式 Failover 特性,因为Broker集群中只要有一台机器不可用,则整个集群都不可用,服务可用性大大降低。

结论:根据实际业务来决策,一般业务顺序性保证都是:局部顺序性 + 普通顺序消息

5. MQ高可用

不同的消息队列,由于其架构不同,所以也实现不同。

高可用的详细说明
https://dunwu.github.io/design/pages/9a462f/#%E9%AB%98%E5%8F%AF%E7%94%A8%E6%9E%B6%E6%9E%84%E7%AE%80%E4%BB%8B

架构设计大致分为3种:

  1. 主备
  2. 主从
  3. 集群 + 分区

5.1 RocketMQ高可用

RocketMQ的构成中包括:Producer、Consumer、Namesrv、Broker

按照角色按个分析:

  1. Producer高可用

    1. Producer在业务系统中,目前业务系统都是微服务集群部署,自带高可用
    2. Producer 上配置多个 Namesrv 列表,从而保证 Producer 和 Namesrv 的连接高可用。并从Namesrv上定时拉取最新的 Topic 信息。
    3. Producer 会和所有 Broker 直连,在发送消息时,会选择一个 Broker 进行发送。如果发送失败,则会使用另外一个 Broker
    4. Producer 会定时向 Broker 心跳,证明其存活。而 Broker 会定时检测,判断是否有 Producer 异常下线
  2. Consumer高可用

    1. Consumer也需要部署多个,保证高可用,当相同消费组下的consumer上线和下线时,会重新分配对应Topic的Queue到目前的consumer上
    2. Consumer 配置多个 Namesrv 列表,从而保证 Consumer 和 Namesrv 的连接高可用。并且,会从 Consumer 定时拉取最新的 Topic 信息。
    3. Consumer 会和所有 Broker 直连,消费相应分配到的 Queue 的消息。如果消费失败,则会发回消息到 Broker 中。
    4. Consumer 会定时向 Broker 心跳,证明其存活。而 Broker 会定时检测,判断是否有 Consumer 异常下线。
  3. Namesrv高可用

    1. Namesrv 需要部署多个节点,以保证 Namesrv 的高可用
  4. Broker高可用

    1. 多个 Broker 可以形成一个 Broker 分组,每个 Broker 分组存在一个 Master 和多个 Slave 节点

5.2 Kafka高可用

Kafka的构成:Producer、Consumer、zk、Broker

按照角色按个分析:

  1. Producer高可用
    1. Producer在业务系统中,目前业务系统都是微服务集群部署,自带高可用
    2. Producer 从zk 拉取到 Topic 的元数据后,选择对应的 Topic 的 leader 分区,进行消息发送写入。
    3. Broker 根据 Producer 的 request.required.acks 决定写入所有broker的数量
  2. Consumer高可用
    1. Consumer也需要部署多个,保证高可用
    2. 每个 Consumer 分配其对应的 Topic Partition,并从该分区的leader中拉取数据
    3. 当相同消费组下的consumer上线和下线时,会将Topic Partition 再均衡,重新分配给 Consumer
  3. zk高可用
    1. 部署2n+1台
  4. broker高可用
    Kafka 为分区引入了多副本机制,同一分区的不同副本中保存的信息是相同的,通过多副本机制实现了故障的自动转移,当集群中某个 broker 失效时仍然能保证服务可用,可以提升容灾能力,具体表现为:
    1. broker集群
    2. 一个topic有多个分区
    3. 每个分区中有一主 + 多从 的副本
    4. 同一分区的不同副本中保存的信息是相同的
    5. Leader(主分区)对外提供服务,而 Follower 只是被动地同步 Leader 而已,不能与外界进行交互