2024.06.28 - 消息队列-是什么?为什么?怎么选?

218 阅读34分钟

历史

消息队列对现在的人而言,其实是一个很新的概念,但是如果放在历史的框架中,就不再陌生了。 从古至今,催生了不同的传递信息的方式,如飞鸽传书、马拉松捷报的跑者、长城烽火台狼烟、鸿门宴的摔杯为号、工业革命时期的报纸、乃至于山本五十六的“虎虎虎”。

image

《虎!虎!虎!》

第二次世界大战太平洋战争中,时任日本帝国海军联合舰队司令的山本五十六偷袭珍珠港成功后,发报兵按着电键,向日本大本营连续拍发偷袭成功的暗码“**!!**”,某种程度上也是一种原始的消息队列。

消息队列的出生,就伴随着人们对消息的内容大小、准确性、保密性、时效性的需求不断在提高。

真正的消息队列出现的历史还是比较短暂的:

image.png

  • 第一阶段是 2000 年以前,80 年代出现的首款消息队列——The Information Bus,首次提出发布订阅模式,旨在解决软件间的通信难题;90 年代,IBM、Oracle、Microsoft 等国际商业软件巨头纷纷推出自家的 MQ,其中 IBM MQ 是这个时期的代表产品,价格昂贵,主要面向大型金融、电信等高端企业,这类商业 MQ 通常采用高端硬件,以软硬件一体机形式交付,并且它们的架构是单机架构。

  • 第二阶段是 2000 年至 2007 年,进入 00 年代后,初代开源消息队列崭露头角,JMS、AMQP 两大标准诞生,与之对应的实现分别为 ActiveMQ 和 RabbitMQ,开源推动了消息队列的普及,降低了使用门槛,逐渐成为企业级架构的必备要素。相较于今天,这类 MQ 主要面向传统企业级应用,针对小流量场景,其横向扩展能力相对较弱。

  • 第三阶段是 2007 年至 2018 年,此时 PC 互联网和移动互联网呈爆发式发展态势。由于传统消息队列难以承受亿级用户的访问流量和海量数据传输,因此催生出互联网消息中间件,这类产品核心能力在于全面采用分布式架构,具备强大的横向扩展能力。开源方面的典型代表有 Kafka、RocketMQ,以及淘宝的 Notify。Kafka 的出现更将消息中间件从 Messaging 领域拓展至 Streaming 领域,从分布式应用的异步解耦场景延伸至大数据领域的流存储和流计算场景。

  • 第四阶段开始于 2014 年并延续至今,IoT、云计算、云原生引领着新的技术潮流。在 IoT 场景下,消息队列开始从云内服务端应用通信延伸至边缘机房和物联网终端设备,支持 MQTT 等物联网标准协议也成为各大消息队列的必备特性。

现状

ActiveMQ

ActiveMQ由java基于JMS1.1规范的实现,但是支持多种编程语言。

一、架构:

image.png ActiveMQ 由三个重要部分组成,包括网络服务(Network Services)、连接(Connectors)、消息模式(Topic,Queue)和消息持久化方式(MessageStore)。

二、连接协议

ActiveMQ 提供了多种应用协议,如 OpenWire、StompREST、WSNotification 等。不同的协议具有不同的特点,其中 OpenWire 用得较多。使用 ActiveMQ 时,根据 JSM 规范,需要获得一个 JMS connection factory,然后再去创建 connection。这个过程中往往需要指定所使用的协议。

三、消息模式和机制

消息模式:

ActiveMQ 提供了两种消息模式:点对点模式(Queue)和发布订阅模式(Topic),这两种模式基本上可以覆盖大部分的需求。

  1. 点对点模式(Queue)

该模式使用队列作为中间媒介,队列是一个先进先出的结构。一个或多个生产者将消息发送到队列,然后多个消费者按消息的先后顺序从队列中消费。

image.png

  1. 发布订阅模式(Topic):该模式使用 topic 作为中间媒介,topic 可以被认为是一个广播站。当一个或多个生产者将消息发布到 topic 广播站之后,topic 广播站会向当前已注册订阅的每一个消费者广播消息(这里的消费者被称为订阅者)。需要注意的是,每一个订阅者都会收到消息。

image.png

持久化订阅

在发布订阅模式中,topic 在接收到消息之后,只会给当前已经注册订阅的订阅者广播消息。如果由于某些原因,如网络问题,导致订阅者断开连接一段时间,而在这段时间内有接收新的消息,那么对于那些暂时断开的订阅者,消息可能就会丢失。

基于这种情况,ActiveMQ 将订阅者分为持久化订阅者和非持久化订阅者。非持久化订阅者不接受离线时生产的消息,而持久化订阅者则通过向 ActiveMQ 中注册一个表明自己身份的ClientId(每个订阅者都有一个ClientId,或自动生成,或自行指定),topic 收到消息时,会为处于离线状态的持久化订阅者根据其ClientId 保存消息。当下次相同ClientId 的订阅者连接时,就可以得到它离线状态下 topic 收到的消息了。

  1. 死信队列

    死信队列(Dead Letter Queue,简称 DLQ)用于保存处理失败或过期的消息。根据 Apache ActiveMQ 官网的说明,当发生以下操作时,消息将会被重新发送给消费者:

  2. Advisory

ActiveMQ的Advisory是一种特殊的消息发布机制,它可以让消息代理向客户端发送各种状态信息和事件通知。这些通知消息被称为"advisory"消息,可以帮助客户端监控和管理ActiveMQ系统的运行状态。

Advisory消息主要包括以下几种类型:

  • 队列事件:当队列被创建、删除或者队列中的消息数量发生变化时,ActiveMQ会发布相应的advisory消息。

  • 连接事件:当客户端与ActiveMQ代理建立或断开连接时,ActiveMQ会发布连接事件的advisory消息。

  • 主题事件:当主题被创建、删除或者主题中的订阅者发生变化时,ActiveMQ会发布相应的advisory消息。

  • 慢使用者:当某个消费者处理消息的速度过慢时,ActiveMQ会发布"慢使用者"的advisory消息。

  • 内存使用:当ActiveMQ代理的内存使用量超过设定的阈值时,会发布内存使用的advisory消息。

通过订阅这些advisory消息,客户端可以实时监控ActiveMQ系统的各种状态变化,并根据这些信息采取相应的措施,比如扩容、重启服务、调整消息处理策略等。

消息持久化方式

消息的持久化通常是为了避免消息丢失。即使服务器宕机重启后,消息也能自动恢复,而不是像内存数据一样被清除。当然,ActiveMQ 也允许你使用内存来存储消息。

当前 ActiveMQ 默认的持久化方式是采用 Kahadb,而目前主流讨论的持久化方案主要是:AMQ、Kahadb、JDBC、Leveldb、ReplicatedLeveldb。

持久化方式解释
AMQActiveMQ5.3之前版本默认持久化方式,采用日志文件的存储方式,写入和恢复速度都很快
KahadbKahadb是一个专门针对消息持久化的解决方案,在ActiveMQ5.4及之后的版本默认采用的存储化方式,性能等各方面比AMQ更优。
JDBCJDBC方式是将消息数据写入到数据库中,但是频繁的从数据库读取写入是一件很耗性能的事,于是在此方式的基础上又提出了Journal优化方案,使用高速缓存写入技术,当消费者消费速度跟不上生产者的生产速度时才写入数据库,这样大大提高了性能。
ReplicatedLeveldbReplicatedLeveldb是ActiveMQ和zookeeper整合时采用的持久化方式,一般在ActiveMQ做集群部署时用到

那为啥现在ActiveMQ用的人不多呢?

  • ActiveMQ 经过十多年的发展,涵盖了你能想到的所有消息领域的特性,并且支持所有协议。然而,这也导致了它过于庞大和复杂。

  • ActiveMQ 主要针对 Java 生态设计,对于非 Java 应用集成支持较弱,需要额外的开发工作。这可能会限制其在异构环境中的应用场景。

  • ActiveMQ 最初设计时并未考虑云原生环境,在容器化、弹性伸缩等方面支持较弱,可能无法充分发挥在云环境中的优势。同时ActiveMQ 的集群部署和管理相对比较复杂。

  • 淘宝早期的 notify 系统就是借鉴了 ActiveMQ 的设计。京东也曾经多年使用大规模的 ActiveMQ 集群,10 年前就已经有了几百台服务器。但他们发现,古老的 MQ 模型中,broker 过于笨重,一旦数据量增大就会出现卡顿的问题(即使 90%的场景都使用了 ActiveMQ,但在处理一定规模的数据量时,这个问题仍然难以解决)。因此,京东逐渐开发了自己的 JMQ。

RabbitMQ

RabbitMQ是一个由erlang开发的AMQP(Advanved Message Queue Protocol)的开源实现。通过使用通用协议就可以做到在不同语言之间传递。tryrabbitmq.com/

AMQP一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。Erlang中的实现有 RabbitMQ等。

一、架构:

通常,在讨论消息队列服务时,会涉及三个主要概念:生产者、消息队列和消费者。RabbitMQ 在这个基本概念之上进行了进一步抽象,在生产者和队列之间引入了交换器(Exchange)。这样一来,生产者与消息队列之间就不再有直接的联系,而是生产者将消息发送给交换器,交换器再根据调度策略将消息转发给消息队列。

因此,消息生产者不会直接将消息发送给消息队列,而是通过与 Exchange 建立的 Channel 将消息发送给 Exchange。Exchange 根据路由规则将消息转发给特定的消息队列。消息队列会存储这些消息,等待消费者通过建立与消息队列连接的 Channel 来获取这些消息。

image.png

  1. Channel(信道):多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,复用TCP连接的通道。

  2. Message(消息):消息由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(消息优先权)、delivery-mode(是否持久性存储)等。

  3. Routing Key(路由键):消息头的一个属性,用于标记消息的路由规则,决定了交换机的转发路径。最大长度255 字节。

  4. Exchange(交换器|路由器):提供Producer到Queue之间的匹配,接收生产者发送的消息并将这些消息按照路由规则转发到消息队列。交换器用于转发消息,它不会存储消息 ,如果没有 Queue绑定到 Exchange 的话,它会直接丢弃掉 Producer 发送过来的消息。交换器有四种消息调度策略(下面会介绍),分别是fanout, direct, topic, headers。

  5. Queue(消息队列):存储消息的一种数据结构,用来保存消息,直到消息发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连接到这个队列将消息取走。需要注意,当多个消费者订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,每一条消息只能被一个订阅者接收。

  6. Binding(绑定):用于建立Exchange和Queue之间的关联。一个绑定就是基于Binding Key将Exchange和Queue连接起来的路由规则,所以可以将交换器理解成一个由Binding构成的路由表。

  7. Broker:RabbitMQ Server,服务器实体。

二、Exchange消息调度策略:

调度策略是指,Exchange 在接收到生产者发送的消息后,根据什么规则将消息转发到一个或多个队列中保存。它与三个因素有关:

  1. Exchange Type(Exchange 的类型)

  2. Binding Key(Exchange 和 Queue 的绑定关系)

  3. 消息的标记信息(Routing Key 和 headers)

Exchange 根据消息的 Routing Key 和Binding Key 分配消息。生产者在向 Exchange 发送消息时,通常会指定一个 Routing Key,以指定该消息的路由规则,而这个 Routing Key 需要与 Exchange Type 及 Binding Key 联合使用才能最终生效。

在 Exchange Type 和 Binding Key 固定的情况下(一般这些内容都是固定配置好的),生产者就可以在发送消息给 Exchange 时,通过指定 Routing Key 来决定消息的流向。

  1. Fanout (订阅模式|广播模式)

交换器会将所有发送到它的消息路由到所有与之绑定的消息队列中。订阅模式与Binding Key和Routing Key无关,交换器将接收到的消息分发给有绑定关系的所有消息队列(无论Binding Key和Routing Key是什么)。类似于子网广播,子网内的每台主机都会获得一份复制的消息。Fanout 交换机是转发消息最快的交换机。

image.png

  1. Direct(路由模式)

当消息的 Routing Key 与 Exchange 和 Queue之间的 Binding Key 完全匹配时,消息将被分发到该 Queue 中。只有当 Routing Key 和 Binding Key 完全匹配时,消息队列才能获取消息。

路由模式是 Exchange 的默认模式。RabbitMQ 默认提供了一个 Exchange,名字是空字符串,类型是 路由模式,绑定到所有的 Queue(每一个 Queue 和这个无名 Exchange 之间的 Binding Key 是 Queue 的名字)。因此,有时候我们感觉不需要交换器也可以发送和接收消息,但实际上是使用了 RabbitMQ 默认提供的 Exchange。

image.png

  1. Topic (通配符模式)

将消息的Routing Key与Exchange和Queue之间的Binding Key进行模糊匹配,如果匹配成功,将消息分发到该Queue。

Routing Key是一个以句点号“. ”分隔的字符串(我们将被句点号“. ”分隔开的每一段独立的字符串称为一个单词)。Binding Key与Routing Key一样,也是以句点号“. ”分隔的字符串。Binding Key中可以存在两种特殊字符“ * ”与“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。

image.png

  1. Headers(键值对模式)

Headers 不依赖于 Routing Key 与 Binding Key 的匹配规则来转发消息,它的路由规则是通过消息头的 Headers 属性来实现的。

这就好比 HTTP 请求的 Headers,在绑定 Queue 与 Exchange 时,需要指定一组键值对,其中键值对的 Hash 结构中必须包含一个键“x-match”,这个键的 Value 可以是“any”或“all”,分别代表消息携带的 Hash 需要全部匹配(all)或仅匹配一个键(any)。

当消息被发送到 Exchange 时,交换器会获取该消息的 headers,并对比其中的键值对是否与 Queue 与 Exchange 绑定时指定的键值对完全匹配。如果完全匹配,那么消息就会被路由到该 Queue,否则就不会被路由到该 Queue。Headers 交换机的优势在于它匹配的规则不仅限于字符串(String)类型,而是可以是 Object 类型。

image.png

三、消息机制:

  1. 消息确认:Message acknowledgment

在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在Timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。 这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的问题,Queue中堆积的消息会越来越多,消费者重启后会重复消费这些消息并重复执行业务逻辑。 如果我们采用no-ack的方式进行确认,也就是说,每次Consumer接到数据后,而不管是否处理完成,RabbitMQ会立即把这个Message标记为完成,然后从queue中删除了。

  1. 消息持久化:Message durability

如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了)。

  1. 队列长度限制:Queue Length Limit

RabbitMQ允许您为消息和队列设置TTL(生存时间)。 可以使用可选的队列参数或策略完成(推荐使用后一个选项)。 可以为单个队列,一组队列或单个消息应用消息TTL。

设置消息的过期时间 MessageProperties messageProperties = new MessageProperties(); messageProperties.setExpiration(“30000”);

  1. 死信交换器:Dead Letter Exchange

在队列上指定一个Exchange,则在该队列上发生如下情况, 1.消息被拒绝(basic.reject or basic.nack),且requeue=false 2.消息过期而被删除(TTL) 3.消息数量超过队列最大限制而被删除 4.消息总大小超过队列最大限制而被删除

就会把该消息转发到指定的这个exchange 需要定义了x-dead-letter-exchange属性,同时也可以指定一个可选的x-dead-letter-routing-key,表示默认的routing-key,如果没有指定,则使用消息原来的routeing-key进行转发

当定义队列时指定了x-dead-letter-exchange(x-dead-letter-routing-key视情况而定),并且消费端执行拒绝策略的时候将消息路由到指定的Exchange中去。

  1. 优先级队列:Priority queue

声明队列时需要指定x-max-priority属性,并设置一个优先级数值

创建优先级队列,需要增加x-max-priority参数,指定一个数字。表示最大的优先级,建议优先级设置为1~10之间。 发送消息的时候,需要设置priority属性,最好不要超过上面指定的最大的优先级。 如果生产端发送很慢,消费者消息很快,则有可能不会严格的按照优先级来进行消费。 第一,如果发送的消息的优先级属性小于设置的队列属性x-max-priority值,则按优先级的高低进行消费,数字越高则优先级越高。 第二,如果发送的消息的优先级属性都大于设置的队列属性x-max-priority值,则设置的优先级失效,按照入队列的顺序进行消费。 第三,如果消费端一直进行监听,而发送端一条条的发送消息,优先级属性也会失效。

那RabbitMQ的缺点有什么呢?

  1. 单点故障问题:

  2. 集群部署复杂:

  3. 一致性问题:当RabbitMQ用于跨多个系统或服务进行消息传递时,可能会面临数据一致性的问题。例如,在一个分布式系统中,如果某个服务处理消息失败,可能会导致数据的不一致。因此,需要设计合适的消息处理机制和容错策略来确保数据的一致性。

  4. 资源消耗:RabbitMQ是一个重量级的消息队列系统,它在运行时会占用较多的系统资源,包括内存、CPU和磁盘空间等。在高并发或大规模数据处理的场景下,这可能会成为性能瓶颈。

  5. 跨语言支持不足:RabbitMQ虽然支持多种语言的客户端,因为本身是erlang编写的,在其他语言上的支持和修改存在成本问题。这可能会限制RabbitMQ在某些特定场景下的应用。

RocketMQ

一、架构:

image.png 还是能看出RockerMQ和之前介绍的两种MQ的架构还是有很多不同的。

RocketMQ 主要由生产者(Producer)、消费者(Consumer)、代理服务器(Broker Server)和名称服务器(Name Server)四部分组成。

生产者(Producer)

负责发布消息,支持集群部署。Producer 通过负载均衡选择 Broker 集群队列进行消息投递。

消费者(Consumer)

负责消费消息,支持集群部署。支持以 push(推)和 pull(拉)两种模式对消息进行消费。群组消费支持集群方式和广播方式(见 2.3)。

代理服务器(Broker Server)

消息中转角色,负责存储消息、转发消息。主要包含以下两个功能:

  • 接收从生产者发送来的消息并存储,同时为消费者的拉取请求做好准备。

  • 存储消息相关的元数据,如消费者组(consumer Group)、消费进度偏移(offset)、主题(Topic)、队列消息(Message Queue)等。

名称服务器(Name Server)

Name Server 是主题路由的注册中心,支持 Broker 的动态注册与发现。生产者或消费者可以通过 Name Server 查找各主题对应的 Broker IP 列表。多个 Name Server 实例组成集群,且每个实例都是无状态的(即每个实例的数据都是一样的)。

二、特点:

MQ 的三大特性:

  1. 异步解耦

  2. 流量削峰

  3. 数据收集

RocketMQ 的特点:

  1. 支持大量消息堆积:亿级消息的堆积能力,单个队列中可累积百万级别的消息。

  2. 高可用性:Broker 服务器支持多 Master 多 Slave 同步双写和异步复制模式,其中同步双写可确保消息不丢失。

  3. 高可靠性:生产者将消息发送到 Broker 端有三种方式,包括同步、异步和单向。其中同步和异步都可以保证消息成功发送。Broker 对消息刷盘有两种策略:同步刷盘和异步刷盘,其中同步刷盘可以确保消息成功存储到磁盘中。消费者的消费模式包括集群消费和广播消费两种,默认是集群消费,如果集群模式中有消费者挂掉,同一组内的其他消费者会接替其进行消费。综上所述,RocketMQ 具有很高的可靠性。

  4. 支持分布式事务消息:采用半消息确认和消息回查机制来保证分布式事务消息的可靠性。后续将详细介绍。

  5. 支持消息过滤:支持消费者业务端的标签过滤。

  6. 支持顺序消息:消息在 Broker 中采用队列的先进先出(FIFO)模式存储,即发送时是顺序的,只需保证消费的顺序性即可。

  7. 支持定时消息和延迟消息:Broker 具有定时消息机制,消息发送到 Broker 后不会立即被消费者消费,而是会等到特定的时间才被消费,延迟消息也是如此,延迟一定时间后才会被消费者消费。

三、消息模式和机制:

消息发送和消费的流程:

image.png

  1. nameserver 启动:启动监听端口,等待 Broker、Producer 和 Consumer 连接。

  2. broker 启动:长连接所有 nameserver,并发送心跳(topic--brokerip+port)。

  3. producer 启动:建立与 nameserver 的长连接,获取 topic-broker 关系,向 broker 发送消息。

  4. consumer 启动:建立与 nameserver 的长连接,获取 topic-broker 关系,从 broker 拉取消息并消费。

RocketMQ机制:

1. 订阅与发布

消息的发布是指某个生产者向某个topic发送消息;消息的订阅是指某个消费者关注了某个topic中带有某些tag的消息,进而从该topic消费数据。

2. 消息顺序

全局顺序:对于指定的一个 Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。

分区顺序:对于指定的一个 Topic,所有消息根据 sharding key 进行区块分区。 同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。

3. 消息过滤

RocketMQ 的消费者可以根据 Tag 进行消息过滤,也支持自定义属性过滤。消息过滤目前是在 Broker 端实现的,优点是减少了对于 Consumer 无用消息的网络传输,缺点是增加了 Broker 的负担、而且实现相对复杂。

4. 消息可靠性

RocketMQ 支持消息的高可靠,影响消息可靠性的几种情况:

  • Broker 非正常关闭:Broker 异常 Crash

  • OS Crash:机器掉电,但是能立即恢复供电情况

  • 机器无法开机(可能是 cpu、主板、内存等关键设备损坏):磁盘设备损坏

1)、2)、3)、4) 四种情况都属于硬件资源可立即恢复情况,RocketMQ 在这四种情况下能保证消息不丢,或者丢失少量数据(依赖刷盘方式是同步还是异步)。

5)、6)属于单点故障,且无法恢复,一旦发生,在此单点上的消息全部丢失。RocketMQ 在这两种情况下,通过异步复制,可保证 99%的消息不丢,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与 Money 相关的应用。注:RocketMQ 从 3.0 版本开始支持同步双写。

5. 至少一次

至少一次(At least Once)指每个消息必须投递一次。Consumer 先Pull 消息到本地,消费完成后,才向服务器返回 ack,如果没有消费一定不会 ack 消息,所以 RocketMQ 可以很好的支持此特性。

6. 回溯消费

回溯消费是指 Consumer 已经消费成功的消息,Broker 在向 Consumer 投递成功消息后,消息仍然需要保留。

7. 事务消息

RocketMQ 事务消息(Transactional Message)是指应用本地事务和发送消息操作可以被定义到全局事务中,要么同时成功,要么同时失败,通过事务消息能达到分布式事务的最终一致。

8. 定时消息

定时消息(延迟队列)是指消息发送到 broker 后,不会立即被消费,等待特定时间投递给真正的 topic。需要注意的是,定时消息会在第一次写入和调度写入真实 topic 时都会计数,因此发送数量、tps 都会变高。

9. 消息重试(消费者)

Consumer 消费消息失败后,要提供一种重试机制,令消息再消费一次。Consumer 消费消息失败通常可以认为有以下几种情况:

  • RocketMQ 会为每个消费组都设置一个 Topic 名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个 Topic 的重试队列是针对消费组,而不是针对每个 Topic 设置的),用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息。

  • 考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。

RocketMQ 对于重试消息的处理是先保存至 Topic 名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时任务按照对应的时间进行 Delay 后重新保存至“%RETRY%+consumerGroup”的重试队列中。

10. 消息重投(生产者)

生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway 没有任何保证。消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在 RocketMQ 中是无法避免的问题。消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、consumer 负载变化也会导致重复消息。

11. 流量控制

生产者流控,因为 broker 处理能力达到瓶颈;消费者流控,因为消费能力达到瓶颈。

12. 死信队列

死信队列用于处理无法被正常消费的消息。RocketMQ 将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。

四、消息的不丢失和一致性:

RocketMQ如何保证消息不丢失:
  1. Producer 发送消息阶段

发送阶段涉及到 Producer 到 broker 的网络通信,因此丢失消息的几率一定会有,那么 RocketMQ 在此阶段用了哪些手段来保证消息不丢失呢?

  • 提供SYNC的发送消息方式,等待 broker 处理结果。RocketMQ 提供了 3 种发送消息发送方式,分别是:同步发送、异步发送、单向发送。我们在使用producer.send()方法时,不指定回调方法,则默认采用同步发送的方式,这也是丢失几率最小的一种发送方式。

  • 发送消息如果失败或者超时,则重新发送。发送重试的源码其实就是一个for循环,默认重试 3 次。

  • broker 提供多master模式,即使某台 broker 宕机了,保证消息可以投递到另外一台正常的 broker 上。

  1. Broker 处理消息阶段
  • 提供同步刷盘的策略。

  • 提供主从模式,同时主从支持同步双写。

  1. Consumer 消费消息阶段

Consumer 默认提供的是 At least once 机制,从 producer 投递消息到 broker,即使前面的过程保证了消息正常持久化,但如果 consumer 消费消息没有消费到,也不能理解为消息绝对的可靠。因此 RocketMQ 默认提供了 At least once 机制来保证消息的可靠消费。

RocketMQ如何保证消息的最终一致性

(如何解决分布式事务)

使用 RocketMQ 的事务消息来确保数据的最终一致性,具体流程如下:

image.png

  1. Producer 服务 A 向 Broker发送一个半消息(也称为half消息),之所以先发送half消息,是为了确认 Producer 服务 A 与 Broker 之间的通信是否正常。若通信异常,服务 A 可以抛出异常,从而避免执行后续逻辑。

  2. 发送成功后,Broker 返回响应,此时消息为半消息,标记为**“不可投递”**状态,Consumer 无法消费。

  3. Producer 执行本地业务逻辑,并提交本地事务。

  4. 如果 Producer 本地事务提交成功,它会向 Broker发送commit,Broker 将半消息标记为正常消息,Consumer 可以正常消费;如果是Rollback,Broker 将丢弃该消息。

  5. 如果commit,将半消息写入磁盘。

  6. 如果 Broker 长时间未收到commit或rollback消息,它会尝试调用 Producer 提供的一个接口,通过该接口判断half消息的状态。如果从接口得知half消息执行成功,Broker 将其标记为正常消息;如果得知未成功,那么就会删除该half消息。

  7. Consumer 从 Broker 中消费到对应的消息。

  8. Consumer 处理本地业务逻辑,然后提交本地事务。

本质是还是**保证数据的最终一致性,**全流程的强一致性还是没办法实现。

疑问:

half 消息是什么?

  • 它和我们正常发送的普通消息是一样的,都是存储在 MQ 中,唯一不同的是 half 在 MQ 中不会立马被消费者消费到,除非这个 half 消息被 commit 了。

  • 在 commit 之前,half 消息会先放在一个内部队列中,只有 commit 了,才会真正将消息放在消费者能读取到的 topic 队列中。

如果服务 A 本地事务执行失败了,或者因为网络波动消失了,怎么办?

  • 服务 A 本地事务执行失败/没接收到消息的commit,先对自己本地事务进行回滚,然后再向 MQ 发送 rollback 操作。

  • 如果 Broker 长时间未收到commit或rollback消息,它会尝试调用 Producer 提供的一个接口,通过该接口判断half消息的状态。

rocketMQ缺点有什么呢?

  • RocketMQ 的集群管理相对复杂,需要手动配置 Name Server 和 Broker 之间的关系。

  • 集群扩容和负载均衡需要手动进行。

  • RocketMQ 单个 Broker 的吞吐量有限,较高的并发下性能可能会受到影响。

  • 跨集群复制和消息重分布等功能可能会带来性能瓶颈。

  • RocketMQ 需要较多的运维操作,如集群监控、故障诊断等。对运维人员的技术要求较高。

Kafka

我这里肯定没有艺严讲的仔细,还是推荐艺严的文章。

bytedance.larkoffice.com/wiki/AbaiwB…

ByteMQ

  1. 运维成本

Kafka 数据存储在本地磁盘,状态很重,因此存在以下问题

  • 扩容,balance,上下线机器需要实际拷贝数据,过程慢,对集群冲击大

  • 无机器级别自动容错功能,少数机器宕机也需要人工介入处理

  • balance 策略复杂,需要同时考虑网络带宽,磁盘IO等问题

  • 升级困难。升级过程中,重启实例会导致流量分布不均,集群抖动等问题。

  1. 稳定性

正常情况下 Kafka 运行稳定,但在集群压力较大情况下集群会出现以下问题:

  • controller设计不合理,没有单独的controller节点,controller压力较大,容易成为性能瓶颈,造成优雅重启单broker时间较长,部分partition不可用时间较长。

  • 一旦产生消费 lag (集群运维操作,集群异常,网络原因,消费者停止消费一段时间再继续消费等),会 miss page cache,直接从磁盘读取数据,速度低,对磁盘压力大,导致消费延时,极端情况下造成 lag 越来越大的问题

  • 单机故障情况下,集群会进行调度调整,剩余机器压力增大,集群压力很大的情况下容易成为压死骆驼的最后一根稻草

结合运维成本一节的问题,在 Kafka 集群遇到问题时,因为状态重,操作基本都需要拷贝数据,很难做到快速无损恢复。由于重启较慢,集群雪崩或者代码出问题时也很难恢复或回滚。

  1. 扩展性

扩展性问题主要体现在以下方面:

  • 集群扩容周期长。扩容后数据需要 balance,实际数据拷贝操作重,且容易对集群造成影响

  • 集群下线同理,需要先将数据从待下线机器上挪走

  • topic 删除时会实际删除物理文件,对磁盘造成冲击,需要小心操作

  • 机器利用率低。Kafka 高峰一般在晚上在线高峰时段,机器资源需求高。而凌晨到中午的低峰期机器资源利用率很低

小结

其实可以看出MQ的进化路程伴随了三个主要过程:

  1. 解耦合:

    2003年到2010年之前,计算机软件行业业刚刚兴起,解决系统间强耦合变成了程序设计的一大难题,这一阶段以activemq和rabbitmq为主的消息队列致力于解决系统间解耦合和一些异步化的操作的问题。这也是所有消息队列被使用的最多的功能之一。

  2. 吞吐量与一致性

    在 2010 年到 2012 年期间,大数据时代正式到来,实时计算的需求日益增长,数据规模也呈指数级增长。由于传统消息队列已无法应对大数据的挑战,消息队列设计的关键因素逐渐转向吞吐量和并发程度。在这一背景下,Kafka 应运而生,并在日志收集和数据通道领域占据了重要地位。

    然而,随着阿里电商业务的崛起,Kafka 在可靠性、一致性、顺序消息、事务消息支持等方面已无法满足阿里电商场景的需求。因此,RocketMQ 应运而生。阿里在自研消息队列的过程中借鉴了 Kafka 的许多设计理念,如顺序写盘、零拷贝、end-to-end 压缩方式等,同时也解决了 Kafka 的一些痛点问题,例如对 Zookeeper 的强依赖。阿里将 RocketMQ 捐赠给了 Apache,并最终成为了 Apache RocketMQ。

  3. 平台化

    2012 年后,云计算、k8s、容器化等新兴技术逐渐兴起,如何将底层技术能力平台化成为了众多公司的攻坚方向。阿里云、腾讯云、华为云等云厂商的入场都证明了这一点。

技术选型?

  1. 吞吐量和性能需求:

  2. 分布式下的数据一致性、稳定性和高并发:

  3. 消息传递需要实时推送消息:

  4. 日志采集:

  5. 需要二次开发:

  6. 部署和运维成本:

在分布式架构中

微服务之间通信选择合适的消息队列是很重要的。目前主流的消息队列有以下几种:

  1. RabbitMQ:

  2. Kafka:

  3. RocketMQ:

  4. ActiveMQ:

在选择消息队列时,需要结合自己的业务特点和需求,考虑以下几点:

  1. 吞吐量和延迟: 如果需要处理海量数据流,可以考虑 Kafka;对于一般的业务需求,RabbitMQ 或 RocketMQ 也是不错的选择。

  2. 可靠性和持久性: 如果对消息可靠性和持久性有较高要求,RabbitMQ 和 RocketMQ 比较合适。

  3. 协议支持: 如果需要支持多种消息协议,ActiveMQ 可能是更好的选择。