RocketMQ原理
前面的部分我们都是为了快速的体验RocketMQ的搭建和使用。这一部分我们总结并学习下RocketMQ底层的一些概念以及原理。
1、RocketMQ概念:
架构:生产者生产消息,先从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、队列的关系
1、消费者和消费者组属于个体与群体的关系
2、Topic是相当于一种消息类型,而队列queue则是属于某个Topic下的更细分的一种单元。
3、在同一个消费者组下的消费者,不能同时消费同一个queue。
4、一个消费者组下的消费者,可以同时消费同一个Topic下的不同队列的消息。
5、不同消费者组下的消费者,可以同时消费同一个Topic下的相同队列的消息(一般不会这么做)。
6、同消费者组下的消费者,不可以同时消费不同Topic下的消息。
5、 代理服务器(Broker Server)
消息中转角色,负责存储消息、转发消息
6、 名称服务(Name Server)
名称服务充当路由消息的提供者。Broker Server会在启动时向所有的Name Server注册自己的服务信息,并且后续通过心跳请求的方式保证这个Broker服务信息的实时性。
生产者或消费者能够通过名字服务查找各主题相应的Broker IP列表
7、 消息(Message)
消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题Topic。
并且Message上有一个为消息设置的标志,Tag标签。用于同一主题下区分不同类型的消息。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。
2、消息存储
1、何时存储消息
分布式队列因为有高可靠性的要求,所以数据要进行持久化存储。
- MQ收到一条消息后,需要向生产者返回一个ACK响应,并将消息存储起来。(告诉生产者已经将消息进行存储)
- MQ Push一条消息给消费者后,等待消费者的ACK响应,需要将消息标记为已消费。如果没有标记为已消费,MQ会不断的尝试往消费者推送这条消息。
- MQ需要定期删除一些过期的消息,这样才能保证服务一直可用。
2、消息存储介质
RocketMQ采用直接用磁盘文件来保存消息,而不需要借助MySQL这一类索引工具。
3、 消息存储结构
rocketmq默认文件--commitLog 位于(当前用户下$HOME\store\)
RocketMQ消息的存储主要分为2个部分:
1、CommitLog:存储消息的元数据。所有消息都会顺序存入到CommitLog文件当中。CommitLog由多个文件组成,每个文件固定大小1G。以第一条消息的偏移量为文件名。
2、ConsumerQueue:存储消息在CommitLog的索引。一个MessageQueue一个文件,记录当前MessageQueue被哪些消费者组消费到了哪一条CommitLog。
整体的消息存储结构如下图:
另外还有几个文件可以了解下。
abort:这个文件是RocketMQ用来判断程序是否正常关闭的一个标识文件。正常情况下,会在启动时创建,而关闭服务时删除。但是如果遇到一些服务器宕机,或者kill -9这样一些非正常关闭服务的情况,这个abort文件就不会删除,因此RocketMQ就可以判断上一次服务是非正常关闭的,后续就会做一些数据恢复的操作。
config/*.json:这些文件是将RocketMQ的一些关键配置信息进行存盘保存。例如Topic配置、消费者组配置、消费者组消息偏移量Offset 等等一些信息。
4、 刷盘机制
RocketMQ需要将消息存储到磁盘上,这样才能保证断电后消息不会丢失。同时这样才可以让存储的消息量可以超出内存的限制。
RocketMQ为了提高性能,会尽量保证磁盘的顺序写。消息在写入磁盘时,有两种写磁盘的方式,同步刷盘和异步刷盘
- 同步刷盘:在返回写成功状态时,消息已经被写入磁盘。
- 异步刷盘:在返回写成功状态时,消息可只是被写入了内存,当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。
- 配置方式:刷盘方式是通过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负载均衡
- Producer发送消息时,默认会轮询目标Topic下的所有MessageQueue往不同的MessageQueue上发送消息,以达到让消息平均落在不同的queue上的目的。(不仅往不同的队列发送消息,而且往不同的broker发送)
- 而由于MessageQueue是分布在不同的Broker上的,所以消息也会发送到不同的broker上。
- 同时生产者在发送消息时,可以指定一个MessageQueueSelector。通过这个对象来将消息发送到自己指定的MessageQueue上。这样可以保证消息局部有序。
生产者发送消息结果,消息负载均衡发送到了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下的一个消费实例即可。
2、广播模式
广播模式下,每一条消息都会投递给订阅了Topic的所有消费者实例
7、消息重试
首先对于广播模式的消息, 是不存在消息重试的机制的,即消息消费失败后,不会再重新进行发送,而只是继续消费新的消息。
而对于集群模式的消息,当消费者消费消息失败后(例如消费业务发生异常),会自动重试
重试消息如何处理
重试的消息会进入一个 “%RETRY%" + ConsumeGroup 的队列中
注意:ConsumeGroup为消费组名称,譬如:
mqadmin topicList -n localhost:9876
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
重试次数:
- 如果消息重试16次后仍然失败,消息将不再投递。转为进入死信队列。
- 另外一条消息无论重试多少次,这些重试消息的MessageId始终都是一样的。
- 然后关于这个重试次数,RocketMQ可以进行定制。例如通过maxReconsumeTimes;将重试次数设定为20次。当定制的重试次数超过16次后,消息的重试时间间隔均为2小时。
//maxReconsumeTimes设置重试次数
@RocketMQMessageListener(maxReconsumeTimes = 2,messageModel = MessageModel.BROADCASTING,topic = "TopicTest",consumeMode= ConsumeMode.CONCURRENTLY,consumerGroup = "MyConsumerGroupXX")
8、死信队列
当一条消息消费失败,RocketMQ就会自动进行消息重试。而如果消息超过最大重试次数,RocketMQ就会认为这个消息有问题。但是此时,RocketMQ不会立刻将这个有问题的消息丢弃,而会将其发送到这个消费者组对应的一种特殊队列:死信队列。
死信队列的特征:
- 一个死信队列对应一个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来进行传递。