1.应用场景
消息队列(Message Queue)简称MQ,是分布式系统中常用的系统组件,用来交换信息的一种技术,应用场景包括:
- 应用解耦
- 异步通信
- 流量削峰
- 广播
- etc
2.MQ的构成
2.1 模型
- 订阅pub/发布sub 模型
- 点对点
2.2 构成
包括生产者、消费者、Broker构成,其中 Message消息体,根据不同的通信协议定义的固定格式进行编码的数据包,来封装业务消息,实现业务的传输,
2.2.1 Broker
消息代理者,主要负责存消息的存储和转发的核心服务。其中消息转发分为2种方式:
- push,broker主动将消息推送给对应consumer
- 优点:实时性
- 缺点:broker会不断向消费者发送消息,受限于消费者的消费能力,可能造成消息的堆积,
- pull,consumer主动从broker种拉取相应的消息
- 根据消费方消息速度进行消息拉取
- 存在延时,实时性不能保证
通常一组对应关系的生产者和消费者通过topic来维系
2.2.2 生产者Producer
负责生产消息,将业务消息封装成Message,发送到broker。
2.2.3 消费者Consumer
负责消费消息,broker将收到的消息保存,并转发给消费者,消费者根据消费到的消息做相应的业务逻辑。
3.消费语义
3.1 At most once
消息至多被消费一次
特点:
- 消息可能会丢失,但绝不重传
- 因为不需要考虑考虑消息的丢失问题,吞吐量大
- 实现简单
- 不会重复投敌,所以不会存在消息重复的问题
实现:分阶段考虑
- msg从producer发送到broker阶段:broker不需要对接受到msg做确认,producer也不用关心是否发送到broker
- broker阶段:
- broker 存储不需要持久化
- 转发消息,不需要考虑consumer是否真的收到
- consumer阶段:consumer接受到消息后,broker可以直接删除消息,且不需要考虑consumer消费msg的情况如何(有没有真的收到,是不是需要重发)
3.2 At least once
消息至少被消费一次
特点:
- 消息可以重传,但绝不丢失
- 不能容忍消息的丢失,但是允许消息可以重复消费
实现:
- msg从producer发送到broker阶段:
- broker必须对接受到msg做确认
- producer如果没有收到broker的ack,需要重发
- broker阶段:
- broker 对接受到的消息必须持久化
- 转发消息,broker必须接受到consumer的ack才能删除消息
- consumer阶段:
- 必须完成消费,给broker发送ack,通知broker删除消息
- 消费方,需要做好幂等操作
3.3 Exactly once
消息仅被消费一次,
特点:
- 每一条消息只被传递一次。
- 实现比较复杂,需要对消息的唯一性做好识别
这里的仅被消费一次,通常指的是:Producer 上产生的消息被 Consumer 仅消费一次。
实现:
-
msg从producer发送到broker阶段:
- broker必须对接受到msg做确认
- producer必须为消息生产唯一标识,broker用来判断,如果收到重复的消息,不在进行处理
-
broker阶段:
- broker 对接受到的消息必须持久化,
- 不能有重复消息(每条消息在其消费队列里有唯一标识)
- 转发消息,broker必须接受到consumer的ack才能删除消息
-
consumer阶段:
- 必须完成消费,给broker发送ack,通知broker删除消息
- 消费方,对消息做记录,如果收到相同的消息不再进行消费
4. 高级特性
4.1 消息重复
产生消息重复原因:
- 网络问题,producer 第一次就发送成功,但由于网络问题没有收到ack,导致procucer重复发送消息
- 可能因为 broker 的消息进度丢失,导致消息重复投递给 consumer
- 消费成功,但是消费业务系统崩溃,导致消费进度没有同步到broker, broker重复转发消息
- 大多数消息队列实现,为了考虑性能,消费进度是异步同步给broker,也可能会产生重复消息
解决方案:
最简单的方式:消费方做好消息幂等操作
- 业务自己实现:
- 使用数据库做记录,消费时进行判断,eg: 根据 id + status做唯一索引,如果有记录代表已消费过该消息
- 分布式锁,eg: 使用redis等
- 框架统一封装,原理和业务实现一样,只不过数据维度多了一个系统字段而已。
4.2 可靠性
消息的队列的可靠性,主要表现在消息数据会不会丢失的问题。还是从消息发送到消费过程,分阶段来看
4.2.1 生产者消息可靠性保证
- 生产者可以通过消息重试的操作
- 只到收到broker的ack
4.2.2 broker消息可靠性保证
当broker收到消息后,有2个选择:
- 消息存储起来后,然后给生产者回复ack
- 先回复ack,然后再异步存储
存储可以理解为就是刷盘方式,对应以上的方式就是
- 同步刷盘,带来了可靠性,也会一定程度降低性能。
- 异步刷盘,数据可能丢失,例如机器突然挂掉了,但会大大提升性能
一般broker都会使用高可用设计,例如主、从架构的broker,还会存在同步复制和异步复制的方式。
常见的broker方式:选择同步复制,加主从 Broker 都是和异步刷盘,这样在可靠性和性能之间做一个平衡。
4.2.3 消费者可靠性保证
消息到消息之后,需要主动ack到broker,broker收到确认后,更新topic对应的offset,如果没有收到ack,那么会重复推送,做好消息重复消费的幂等即可。
有的消息队列,例如kafka,消费到消息后会自动提交 offset ,需要关闭自动提交 offset ,改为处理完逻辑后自己手动提交 offset
4.3 消息顺序性
消息顺序性是指:消费消息的顺序要和生产的消息顺序一致
主要思路:
- 生产者发送消息时有顺序
- 消费者在消费消息时有顺序
4.3.1 局部顺序性 和 全局顺序性
局部是指根据业务的特性进行划分,例如在用户A发送消息的场景下,我们保证用户A的消息是顺序性即可,不需要对全部用户的消息进行顺序。
在rocketMQ中,顺序消息主要指的是都是 Queue 级别的局部顺序
- 生产者顺序性
- 生产者发送的时候可以用 MessageQueueSelector 为某一批消息发送到一个 Queue 上,eg:相同userId hash 到 一个queue上
- Producer 单线程顺序发送,且发送到同一个Queue,这样 Consumer 就可以按照 Producer 发送的顺序去消费消息(需要根据业务实际来决策)
- 这一批消息的消费将是顺序消息(并由同一个consumer完成消息)
- 或者设置 Message Queue 的数量只有 1 ,但这样消费的实例只能有一个,多出来的实例都会空跑
- 消费顺序性
- Consumer 消费消息的时候是针对Queue顺序拉取并开始消费
- 且一条 Queue 只会给一个消费者(集群模式下)
- 能够保证同一个消费者实例对于Queue上消息的消费是顺序地开始消费(不一定顺序消费完成,因为消费可能并行)
在Kafka中,顺序消息是指从单个分区顺序消费消息
- 生产者顺序性
- topic设置一个分区,消息顺序发送到这一个分区,会降低吞吐量
- 消费顺序性
- 对每个 Partition 内部单线程消费,单线程吞吐量太低,一般不会用这个
- 拉取到消息后,写到N个内存 queue,具有相同 key 的数据都到同一个内存 queue,N可以根据业务key hash 来搞,每个内存queue使用一个线程来消费
总结:根据实际业务来决策,一般是使用局部顺序性 + 消费顺序性 来实现
4.3.1 普通顺序消息 和 严格顺序消息
普通顺序消息:
- 正常情况下可以保证完全的顺序消息
- 如果broker发生宕机或重启,由于队列总数发生发化,消费者会触发负载均衡,而默认地负载均衡算法采取哈希取模平均,这样负载均衡分配到定位的队列会发化,使得队列可能分配到别的实例上,则会短暂地出现消息顺序不一致
- 如果业务能容忍短暂的乱序,使用普通顺序方式比较合适
严格顺序消息:
- 无论什么情况下都需要保证消息的顺序
- 牺牲了分布式 Failover 特性,因为Broker集群中只要有一台机器不可用,则整个集群都不可用,服务可用性大大降低。
结论:根据实际业务来决策,一般业务顺序性保证都是:局部顺序性 + 普通顺序消息
5. MQ高可用
不同的消息队列,由于其架构不同,所以也实现不同。
架构设计大致分为3种:
- 主备
- 主从
- 集群 + 分区
5.1 RocketMQ高可用
RocketMQ的构成中包括:Producer、Consumer、Namesrv、Broker
按照角色按个分析:
-
Producer高可用
- Producer在业务系统中,目前业务系统都是微服务集群部署,自带高可用
- Producer 上配置多个 Namesrv 列表,从而保证 Producer 和 Namesrv 的连接高可用。并从Namesrv上定时拉取最新的 Topic 信息。
- Producer 会和所有 Broker 直连,在发送消息时,会选择一个 Broker 进行发送。如果发送失败,则会使用另外一个 Broker
- Producer 会定时向 Broker 心跳,证明其存活。而 Broker 会定时检测,判断是否有 Producer 异常下线
-
Consumer高可用
- Consumer也需要部署多个,保证高可用,当相同消费组下的consumer上线和下线时,会重新分配对应Topic的Queue到目前的consumer上
- Consumer 配置多个 Namesrv 列表,从而保证 Consumer 和 Namesrv 的连接高可用。并且,会从 Consumer 定时拉取最新的 Topic 信息。
- Consumer 会和所有 Broker 直连,消费相应分配到的 Queue 的消息。如果消费失败,则会发回消息到 Broker 中。
- Consumer 会定时向 Broker 心跳,证明其存活。而 Broker 会定时检测,判断是否有 Consumer 异常下线。
-
Namesrv高可用
- Namesrv 需要部署多个节点,以保证 Namesrv 的高可用
-
Broker高可用
- 多个 Broker 可以形成一个 Broker 分组,每个 Broker 分组存在一个 Master 和多个 Slave 节点
5.2 Kafka高可用
Kafka的构成:Producer、Consumer、zk、Broker
按照角色按个分析:
- Producer高可用
- Producer在业务系统中,目前业务系统都是微服务集群部署,自带高可用
- Producer 从zk 拉取到 Topic 的元数据后,选择对应的 Topic 的 leader 分区,进行消息发送写入。
- Broker 根据 Producer 的 request.required.acks 决定写入所有broker的数量
- Consumer高可用
- Consumer也需要部署多个,保证高可用
- 每个 Consumer 分配其对应的 Topic Partition,并从该分区的leader中拉取数据
- 当相同消费组下的consumer上线和下线时,会将Topic Partition 再均衡,重新分配给 Consumer
- zk高可用
- 部署2n+1台
- broker高可用
Kafka 为分区引入了多副本机制,同一分区的不同副本中保存的信息是相同的,通过多副本机制实现了故障的自动转移,当集群中某个 broker 失效时仍然能保证服务可用,可以提升容灾能力,具体表现为:- broker集群
- 一个topic有多个分区
- 每个分区中有一主 + 多从 的副本
- 同一分区的不同副本中保存的信息是相同的
- Leader(主分区)对外提供服务,而 Follower 只是被动地同步 Leader 而已,不能与外界进行交互