【主流技术】聊一聊消息队列 RocketMQ 的基本结构与概念
前言
RocketMQ 是阿里巴巴在 2012 年开源的分布式消息中间件,目前已经捐赠给 Apache 软件基金会,并于 2017 年 9 月 25 日成为 Apache 的顶级项目。
作为经历过多次阿里巴巴双十一这种“超级工程”的洗礼并有稳定出色表现的国产中间件,以其高性能、低延时和高可靠等特性近年来已经也被越来越多的国内企业使用。
一、初识 RocketMQ
2011 年初,Linkin 开源了 Kafka 这个优秀的消息中间件,淘宝中间件团队在对 Kafka 做过充分 Review 之后,被 Kafka 无限消息堆积、高效的持久化速度等优点吸引了。
美中不足的的是,Kafka 主要定位于日志传输,对于使用在淘宝交易、订单、充值等场景下还有诸多特性不满足。所以,阿里的中间件团队重新用 Java 语言编写了 RocketMQ ,定位于全场景的可靠消息传输。
目前 RocketMQ 在阿里集团的应用生态里被广泛应用于订单、交易、充值、物流、消息推送、日志处理, binglog 分发等场景。
RocketMQ 对比 Kafka,虽然设计的思想上有借鉴,但在架构上做了减法,在功能上做了加法:
- 去掉 Zookeeper,使用 NameServer 来管理 Broker 集群,通信更方便;
- 有延时队列和死信队列,开箱即用,减化代码逻辑实现。
1.1基本模型
RocketMQ 主要由 Producer、Broker、Consumer 三部分组成。其中,Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。
而 Broker 在实际部署过程中对应的是一台服务器,每个 Broker 可以存储多个 Topic 的消息,每个Topic 的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个 Topic 中的消息地址都会存储在多个 Message Queue 中。
ConsumerGroup 由多个Consumer 实例构成。
二、基本概念
以下的基本概念是理解和掌握 RocketMQ 最基础的概念,也是最重要的概念。现在不理解没有关系,先记住有个印象,后续使用的时候,兴许能帮助你豁然开朗。
2.1Producer
消息生产者(Producer)负责生产消息,一般由业务系统负责生产消息。
一个消息生产者会把业务应用系统里产生的消息(封装好的消息体)发送到 Broker 服务器。RocketMQ 提供多种发送方式,同步发送、异步发送、顺序发送、单向发送。其中,同步和异步方式均需要 Broker 返回确认信息(即Ack),单向发送不需要。
2.2Consumer
消息消费者(Consumer)负责消费消息,一般是由下游的系统负责异步消费。
一个消息消费者会从 Broker 服务器默认主动拉取(Pull 模式)消息,并将其提供给应用程序。从用户应用的角度而言提供了两种消费形式:拉取式消费(默认 Pull 模式)、推动式消费。
2.3Topic
主题(Topic)表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是 RocketMQ 进行消息订阅的基本单位。
2.4Tag
标签(Tag)为消息设置的标志,用于同一 Topic 下区分不同类型的消息。来自同一业务单元的消息,可以根据不同业务的功能模块在同一主题下设置不同的标签。标签能够有效地保持代码的清晰度和连贯性,并优化 RocketMQ 提供的查询系统。消费者可以根据 Tag 实现对不同子主题的不同消费逻辑,实现更好的扩展性。
2.5Message
消息(Message)是系统所传输信息的物理载体、生产和消费数据的最小单位,每条消息必须属于某一个主题。RocketMQ 中的每条消息都拥有唯一的 Message ID 作为标识,且可以携带具有业务标识的 Key。系统提供了通过 Message ID 和 Key 查询消息的功能。
2.6Broker
代理服务器(Broker)是消息中转角色,负责存储消息、转发消息。代理服务器在 RocketMQ 系统中负责接收从生产者发送来的消息并存储、同时为消费者的拉取请求作准备。代理服务器也存储消息相关的元数据,包括消费者组、消费进度偏移和主题和队列消息等。
2.7Pull Consumer
拉取式消费(Pull Consumer)是 Consumer 消费的一种类型,也是默认的类型。下游应用系统通常主动调用 Consumer 的拉消息方法从 Broke r服务器拉消息,即主动权由下游应用控制。一旦获取了批量消息,应用就会启动消费过程。
2.8Producer Group
生产者组(Producer Group)是同一类 Producer 的集合,这类 Producer 发送同一类消息且发送逻辑一致。如果发送的是事务消息且原始生产者在发送之后崩溃,则 Broker 服务器会联系同一生产者组的其他生产者实例重试提交或回溯消费。
2.9Consumer Group
消费者组(Consumer Group)是同一类 Consumer 的集合,这类 Consumer 通常消费同一类消息且消费逻辑一致。消费者组使得在消息过程中实现负载均衡和提高容错变得非常容易。要注意的是,消费者组的每个消费者实例必须订阅完全相同的 Topic。RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。
2.10Ordered Message
顺序消息分为普通顺序消费(Normal Ordered Message)和严格顺序消息(Strictly Ordered Message)。
普通顺序消费(Normal Ordered Message)下的消费者通过同一个消息队列(Message Queue) 收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。
而在严格顺序消息(Strictly Ordered Message)模式下,消费者收到的所有消息均是严格有序的。
三、高级特性
3.1消息顺序
消息有序指的是一类消息消费时,能按照发送的顺序来消费,RocketMQ 可以严格的保证消息有序。
例如:一个订单产生了三条消息分别是订单创建、订单付款、订单完成。消费时要按照这个顺序消费才能有意义,但是同时订单之间是可以并行消费的。
顺序消息分为全局顺序消息与分区顺序消息,全局顺序是指某个 Topic 下的所有消息都要保证顺序,部分顺序消息只要保证每一组消息被顺序消费即可。
-
全局顺序
-
对于指定的一个 Topic,所有消息按照严格的先进先出(FIFO)的顺序进行发布和消费。
-
适用场景:性能要求不高,所有的消息严格按照 FIFO 原则进行消息发布和消费的场景。
-
-
分区顺序
-
对于指定的一个 Topic,所有消息根据 sharding key 进行区块分区。 同一个分区内的消息按照严格的 FIFO 顺序进行发布和消费。 Sharding key 是顺序消息中用来区分不同分区的关键字段,和普通消息的 Key 是完全不同的概念。
-
适用场景:性能要求高,以 sharding key 作为分区字段,在同一个区块中严格的按照 FIFO 原则进行消息发布和消费的场景。
-
3.2消息可靠性
RocketMQ 支持消息的高可靠,以下是影响消息可靠性的 6 种情况:
- Broker 非正常关闭
- Broker 异常 Crash
- OS Crash
- 机器掉电,但是能立即恢复供电情况
- 机器无法开机(可能是 cpu、主板、内存等关键设备损坏)
- 磁盘设备损坏
其中上述的1、2、3、4 这四种情况都属于硬件资源可立即恢复的情况,RocketMQ 在这四种情况下能保证消息不丢失,或者丢失少量数据(取决于刷盘方式是同步还是异步)。
而5、6这两点属于单点故障,无法恢复,一旦发生,在此单点上的消息会全部丢失。
RocketMQ 在这两种情况下,通过异步复制可保证 99% 的消息不丢失,但是仍然会有极少量的消息可能丢失。通过同步双写技术可以完全避免单点,同步双写势必会影响性能,适合对消息可靠性要求极高的场合,例如与订单、支付等相关的应用。注:RocketMQ 从 3.0 版本开始支持同步双写。
3.3延时队列
延迟队列是指消息发送到 Broker 后,不会立即被消费,等待特定时间后才会投递给真正的 Topic。
Broker 有配置项 messageDelayLevel,默认值为:1s、5s、10s、30s、1min、2min、3min、4min、5min、6min、7min、8min、9min、10min、20min、30min、1h、2h 这 18 个 level。
也可以配置自定义 messageDelayLevel ,注意:messageDelayLevel 是 Broker 的属性,不属于某个 Topic。
发消息时,设置 delayLevel 等级即可:msg.setDelayLevel(level)。level 有以下 3 种情况:
- level == 0,消息为非延迟消息
- 1<=level<=maxLevel,消息延迟特定时间,例如 level==1,延迟1s
- level > maxLevel,则 level== maxLevel,例如 level==20,延迟 2h
定时消息会暂存在名为 SCHEDULE_TOPIC_XXXX 的 Topic 中,并根据 delayTimeLevel 存入特定的 queue,queueId = delayTimeLevel – 1,即一个 queue 只存相同延迟的消息,保证具有相同发送延迟的消息能够顺序消费。Broker 会调度地消费 SCHEDULE_TOPIC_XXXX,将消息写入真实的 Topic。
需要注意的是,定时消息会在第一次写入和调度写入真实 Topic 时都会计数,因此发送数量、TPS 都会变高,对性能可能会有一定影响。
3.4消息重试
Consumer 消费消息失败后,可以提供一种重试机制,令消息再消费一次。Consumer 消费消息失败通常可以认为有以下几种情况:
- 由于消息本身的原因
- 例如反序列化失败,消息数据本身无法处理(例如话费充值,当前消息的手机号被注销,无法充值)等。
- 这种错误通常需要跳过这条消息,再消费其它消息,而这条失败的消息即使立刻重试消费,99% 也不成功,所以最好提供一种定时重试机制,即过 10 秒后再重试。
- 由于依赖的下游应用服务不可用
- 例如数据库连接不可用,系统网络故障等。
- 遇到这种错误,即使跳过当前失败的消息,消费其他消息同样也会报错。这种情况建议应用 sleep 30s,再消费下一条消息,这样可以减轻 Broker 重试消息的压力。
RocketMQ 会为每个消费组都设置一个 Topic 名称为:%RETRY%+consumerGroup 的重试队列。这里需要注意的是:这个 Topic 的重试队列是针对消费组,而不是针对每个 Topic 设置的,用于暂时保存因为各种异常而导致 Consumer 端无法消费的消息。
考虑到异常恢复起来需要一些时间,会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大。
RocketMQ 对于重试消息的处理是先保存至 Topic 名称为:SCHEDULE_TOPIC_XXXX 的延迟队列中,后台定时任务按照对应的时间进行 Delay 后重新保存至 %RETRY%+consumerGroup 的重试队列中。
3.5死信队列
死信队列用于处理无法被正常消费的消息。
当一条消息初次消费失败,消息队列会自动进行消息重试。达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
RocketMQ 将这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。
在 RocketMQ 中,可以通过使用 consol e控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费。
四、文章小结
到这里关于消息队列 RocketMQ 的基本结构就分享完了,其实本文主要还是介绍一些基本的概念,后续笔者还会分享一些在正真项目中的实践,尽请期待。
最后,如果文章有不足和错误,还请大家指正。或者你有其它想说的,也欢迎大家在评论区里交流!
参考文档:
https://github.com/apache/rocketmq/blob/master/docs/cn/concept.md
https://github.com/apache/rocketmq/blob/master/docs/cn/features.md