Bootstrap

RocketMq底层原理(最详细)

RocketMQ原理

前面的部分我们都是为了快速的体验RocketMQ的搭建和使用。这一部分我们总结并学习下RocketMQ底层的一些概念以及原理。

1、RocketMQ概念:

 

ebc770d9127b3660ad61353bf0753c60.png

架构:生产者生产消息,先从nameServer获取Broker列表地址,再向Broker推送消息。消费者同样先从nameServer获取Broker列表地址,再从Broker中消费消息

1、 消息模型(Message Model)

RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息,每个 Broker 可以存储多个Topic的消息。

Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。

 

2、 消息生产者(Producer)

负责生产消息,把消息发送到broker服务器。

 

3、 主题(Topic)

表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。

同一个Topic下的数据,会分片保存到不同的Broker上,而每一个分片单位,就叫做MessageQueue。MessageQueue是生产者发送消息与消费者消费消息的最小单位。

 

4、 消息消费者(Consumer)

负责消费消息,提供了两种消费形式:拉取式消费、推动式消费。

  • 拉取式主动从Broker服务器拉消息、主动权由应用控制。
  • 推模式下Broker收到数据后会主动推送给消费端,该消费模式一般实时性较高。(一般使用这种,只需要一个监听器@RocketMQMessageListener就可以实现)

消费者同样会把同一类Consumer组成一个集合,叫做消费者组,消费者组使得实现负载均衡变得非常容易(把消费者启动两次即可得到一个消费者组)

 

RocketMQ 支持两种消息模式:集群消费(Clustering)和广播消费(Broadcasting)。

  • 集群消费模式下,相同Consumer Group的每个Consumer实例平均分摊消息。
  • 广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。

 

rocketMQ中,消费者组、消费者、Topic、队列的关系

 

ef48ff27688a25e85075eeb686f25a5e.png

 

1、消费者和消费者组属于个体与群体的关系

2、Topic是相当于一种消息类型,而队列queue则是属于某个Topic下的更细分的一种单元。

3、在同一个消费者组下的消费者,不能同时消费同一个queue。

4、一个消费者组下的消费者,可以同时消费同一个Topic下的不同队列的消息。

 

82b5508c2ce578a7037acee5246728c2.png

 

 

5、不同消费者组下的消费者,可以同时消费同一个Topic下的相同队列的消息(一般不会这么做)。

 

902dc54d89ed5913bd020d0a1ae9aa0f.png

6、同消费者组下的消费者,不可以同时消费不同Topic下的消息。

 

88328a60c37e1c287c2e812138cc2c68.png

 

 

5、 代理服务器(Broker Server)

消息中转角色,负责存储消息、转发消息

6、 名称服务(Name Server)

名称服务充当路由消息的提供者。Broker Server会在启动时向所有的Name Server注册自己的服务信息,并且后续通过心跳请求的方式保证这个Broker服务信息的实时性。

生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表
 

7、 消息(Message)

消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题Topic。

并且Message上有一个为消息设置的标志,Tag标签。用于同一主题下区分不同类型的消息。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。

 

2、消息存储

1、何时存储消息

分布式队列因为有高可靠性的要求,所以数据要进行持久化存储。

  1. MQ收到一条消息后,需要向生产者返回一个ACK响应,并将消息存储起来。(告诉生产者已经将消息进行存储)
  2. MQ Push一条消息给消费者后,等待消费者的ACK响应,需要将消息标记为已消费。如果没有标记为已消费,MQ会不断的尝试往消费者推送这条消息。
  3. MQ需要定期删除一些过期的消息,这样才能保证服务一直可用。

2、消息存储介质

RocketMQ采用直接用磁盘文件来保存消息,而不需要借助MySQL这一类索引工具。

3、 消息存储结构

rocketmq默认文件--commitLog 位于(当前用户下$HOME\store\)

 

5435ec5d5e631c2c79a046e2358eec7d.png

 

RocketMQ消息的存储主要分为2个部分:

1、CommitLog:存储消息的元数据。所有消息都会顺序存入到CommitLog文件当中。CommitLog由多个文件组成,每个文件固定大小1G。以第一条消息的偏移量为文件名。

 

0ff8733d514d656ecdf4b17b0f7df3c2.png

 

2、ConsumerQueue:存储消息在CommitLog的索引。一个MessageQueue一个文件,记录当前MessageQueue被哪些消费者组消费到了哪一条CommitLog。

 

6b1e1ee7a899b7fa72395d8fc5f6d6a5.png

 

整体的消息存储结构如下图:

 

4b098593ad099a38edc7d985ce16a4ce.png

 

另外还有几个文件可以了解下。

abort:这个文件是RocketMQ用来判断程序是否正常关闭的一个标识文件。正常情况下,会在启动时创建,而关闭服务时删除。但是如果遇到一些服务器宕机,或者kill -9这样一些非正常关闭服务的情况,这个abort文件就不会删除,因此RocketMQ就可以判断上一次服务是非正常关闭的,后续就会做一些数据恢复的操作。

config/*.json:这些文件是将RocketMQ的一些关键配置信息进行存盘保存。例如Topic配置、消费者组配置、消费者组消息偏移量Offset 等等一些信息。

 

8d285be3210158550ec2786d30d295ea.png

 

 

4、 刷盘机制

RocketMQ需要将消息存储到磁盘上,这样才能保证断电后消息不会丢失。同时这样才可以让存储的消息量可以超出内存的限制。

RocketMQ为了提高性能,会尽量保证磁盘的顺序写。消息在写入磁盘时,有两种写磁盘的方式,同步刷盘和异步刷盘

 

ee88d73f35c9a15f348434c44bc471fb.png

  • 同步刷盘:在返回写成功状态时,消息已经被写入磁盘。
  • 异步刷盘:在返回写成功状态时,消息可只是被写入了内存,当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。
  • 配置方式:刷盘方式是通过Broker配置文件里的flushDiskType 参数设置的,这个参数被配置成SYNC_FLUSH、ASYNC_FLUSH中的 一个。

5、 消息主从复制

如果Broker以一个集群的方式部署,会有一个master节点和多个slave节点,消息需要从Master复制到Slave上。而消息复制的方式分为同步复制和异步复制。

  • 同步复制:

同步复制是等Master和Slave都写入消息成功后才反馈给客户端写入成功的状态,步复制会增大数据写入的延迟,降低系统的吞吐量。

  • 异步复制:

异步复制是只要master写入消息成功,就反馈给客户端写入成功的状态。然后再异步的将消息复制给Slave节点在异步复制下,系统拥有较低的延迟和较高的吞吐量。但是如果master节点故障,而有些数据没有完成复制,就会造成数据丢失。

  • 配置方式:

消息复制方式是通过Broker配置文件里的brokerRole参数进行设置的,这个参数可以被设置成ASYNC_MASTER、 SYNC_MASTER、SLAVE三个值中的一个。

6、 负载均衡

6.1 Producer负载均衡

  1. Producer发送消息时,默认会轮询目标Topic下的所有MessageQueue往不同的MessageQueue上发送消息,以达到让消息平均落在不同的queue上的目的。(不仅往不同的队列发送消息,而且往不同的broker发送)
  2. 而由于MessageQueue是分布在不同的Broker上的,所以消息也会发送到不同的broker上。
  3. 同时生产者在发送消息时,可以指定一个MessageQueueSelector。通过这个对象来将消息发送到自己指定的MessageQueue上。这样可以保证消息局部有序。

 

 

48554ad5734b859b1540daca5f61f0b8.png

 

生产者发送消息结果,消息负载均衡发送到了2个borker和多个MessageQueue

SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847CED0000, offsetMsgId=C0A86A8300002A9F0000000000004C5A, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=4], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847CF60001, offsetMsgId=C0A86A8300002A9F0000000000004D1B, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=5], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847CF70002, offsetMsgId=C0A86A8300002A9F0000000000004DDC, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=6], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847CFB0003, offsetMsgId=C0A86A8300002A9F0000000000004E9D, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=7], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847CFE0004, offsetMsgId=C0A86A8100002A9F0000000000004C58, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-a, queueId=0], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D070005, offsetMsgId=C0A86A8100002A9F0000000000004D19, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-a, queueId=1], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D090006, offsetMsgId=C0A86A8100002A9F0000000000004DDA, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-a, queueId=2], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D0C0007, offsetMsgId=C0A86A8100002A9F0000000000004E9B, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-a, queueId=3], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D0F0008, offsetMsgId=C0A86A8100002A9F0000000000004F5C, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-a, queueId=4], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D110009, offsetMsgId=C0A86A8100002A9F000000000000501D, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-a, queueId=5], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D12000A, offsetMsgId=C0A86A8100002A9F00000000000050DE, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-a, queueId=6], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D16000B, offsetMsgId=C0A86A8100002A9F00000000000051A0, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-a, queueId=7], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D19000C, offsetMsgId=C0A86A8300002A9F0000000000004F5E, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=0], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D1B000D, offsetMsgId=C0A86A8300002A9F0000000000005020, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=1], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D1D000E, offsetMsgId=C0A86A8300002A9F00000000000050E2, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=2], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D1F000F, offsetMsgId=C0A86A8300002A9F00000000000051A4, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=3], queueOffset=0]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D210010, offsetMsgId=C0A86A8300002A9F0000000000005266, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=4], queueOffset=1]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D230011, offsetMsgId=C0A86A8300002A9F0000000000005328, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=5], queueOffset=1]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D250012, offsetMsgId=C0A86A8300002A9F00000000000053EA, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=6], queueOffset=1]
SendResult [sendStatus=SEND_OK, msgId=7F000001151418B4AAC214847D2A0013, offsetMsgId=C0A86A8300002A9F00000000000054AC, messageQueue=MessageQueue [topic=helloworld, brokerName=broker-b, queueId=7], queueOffset=1]
23:37:07.135 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.106.131:9876] result: true
23:37:07.145 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.106.131:10911] result: true
23:37:07.145 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.106.129:11011] result: true
23:37:07.145 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.106.129:10911] result: true
23:37:07.145 [NettyClientSelector_1] INFO RocketmqRemoting - closeChannel: close the connection to remote address[192.168.106.131:11011] result: true

Process finished with exit code 0

 

6.2 Consumer负载均衡

Consumer也是以MessageQueue为单位来进行负载均衡。分为集群模式和广播模式。

 

1、集群模式

在集群消费模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个消费实例即可。

 

 

7c9a354ea9c014a922abd85c5bdb0861.png

2、广播模式

广播模式下,每一条消息都会投递给订阅了Topic的所有消费者实例

 

7、消息重试

首先对于广播模式的消息, 是不存在消息重试的机制的,即消息消费失败后,不会再重新进行发送,而只是继续消费新的消息。

而对于集群模式的消息,当消费者消费消息失败后(例如消费业务发生异常),会自动重试

 

重试消息如何处理

重试的消息会进入一个 “%RETRY%" + ConsumeGroup 的队列中

注意:ConsumeGroup为消费组名称,譬如:

mqadmin topicList -n localhost:9876

 

3f4e77d41be213f5f59998ea150ef739.png

 

RocketMQ默认允许每条消息最多重试16次,每次重试的间隔时间如下:

重试次数

与上次重试的间隔时间

重试次数

与上次重试的间隔时间

1

10 秒

9

7 分钟

2

30 秒

10

8 分钟

3

1 分钟

11

9 分钟

4

2 分钟

12

10 分钟

5

3 分钟

13

20 分钟

6

4 分钟

14

30 分钟

7

5 分钟

15

1 小时

8

6 分钟

16

2 小时

这个重试时间跟延迟消息的延迟级别是对应的。不过取的是延迟级别的后16级别。

messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

 

重试次数:

  1. 如果消息重试16次后仍然失败,消息将不再投递。转为进入死信队列。
  2. 另外一条消息无论重试多少次,这些重试消息的MessageId始终都是一样的。
  3. 然后关于这个重试次数,RocketMQ可以进行定制。例如通过maxReconsumeTimes;将重试次数设定为20次。当定制的重试次数超过16次后,消息的重试时间间隔均为2小时。
//maxReconsumeTimes设置重试次数
@RocketMQMessageListener(maxReconsumeTimes = 2,messageModel = MessageModel.BROADCASTING,topic = "TopicTest",consumeMode= ConsumeMode.CONCURRENTLY,consumerGroup = "MyConsumerGroupXX")

 

8、死信队列

当一条消息消费失败,RocketMQ就会自动进行消息重试。而如果消息超过最大重试次数,RocketMQ就会认为这个消息有问题。但是此时,RocketMQ不会立刻将这个有问题的消息丢弃,而会将其发送到这个消费者组对应的一种特殊队列:死信队列。

 

13919984c614a463d57af6a67ab6168d.png

 

死信队列的特征:

  • 一个死信队列对应一个ConsumGroup,而不是对应某个消费者实例。
  • 如果一个ConsumeGroup没有产生死信消息,RocketMQ就不会为其创建相应的死信队列。
  • 死信队列中的消息不会再被消费者正常消费。
  • 死信队列的有效期跟正常消息相同。默认3天,对应broker.conf中的fileReservedTime属性。超过这个最长时间的消息都会被删除,而不管消息是否消费过。

通常,一条消息进入了死信队列,意味着消息在消费处理的过程中出现了比较严重的错误,并且无法自行恢复。此时,一般需要人工去查看死信队列中的消息,对错误原因进行排查。然后对死信消息进行处理,比如转发到正常的Topic重新进行消费,或者丢弃。

注:如果无法读取死信队列消息,可以把他们的权限perm权限配置成6,才能被消费(可以通过mqadmin指定或者web控制台)。

 

消费死信队列,订阅主题Topic设置成%DLQ%Consumer_Group即可

9、消息幂等

1、幂等的概念

在MQ系统中,对于消息幂等有三种实现语义:

at most once 最多一次:每条消息最多只会被消费一次
at least once 至少一次:每条消息至少会被消费一次
exactly once 刚刚好一次:每条消息都只会确定的消费一次
这三种语义都有他适用的业务场景。

 

其中,at most once是最好保证的。RocketMQ中可以直接用异步发送、sendOneWay等方式就可以保证。

 

而at least once这个语义,RocketMQ也有同步发送、事务消息等很多方式能够保证。

 

而这个exactly once是MQ中最理想也是最难保证的一种语义,需要有非常精细的设计才行。RocketMQ只能保证at least once,保证不了exactly once。所以,使用RocketMQ时,需要由业务系统自行保证消息的幂等性。

2、消息幂等的必要性

在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,这个重复简单可以概括为以下情况:

  • 发送时消息重复

当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。

  • 投递时消息重复

消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。

3、处理方式

从上面的分析中,我们知道,在RocketMQ中,是无法保证每个消息只被投递一次的,所以要在业务上自行来保证消息消费的幂等性。

而要处理这个问题,RocketMQ的每条消息都有一个唯一的MessageId,这个参数在多次投递的过程中是不会改变的,所以业务上可以用这个MessageId来作为判断幂等的关键依据。

但是,这个MessageId是无法保证全局唯一的,也会有冲突的情况。所以在一些对幂等性要求严格的场景,最好是使用业务上唯一的一个标识比较靠谱。例如订单ID。而这个业务标识可以使用Message的Key来进行传递。

 

;