RocketMQ学习之高级功能
消息的存储
-
原因
分布式队列因为有高可靠性的要求,所以数据要进行持久化存储
-
流程
(1) 消息生成者发送消息;
(2) MQ收到消息,将消息进行持久化,在存储中新增一条记录 ;
(3) 返回ACK给生产者;
(4) MQ push 消息给对应的消费者,然后等待消费者返回ACK;
(5) 如果消息消费者在指定时间内成功返回ack,那么MQ认为消息消费成功,在存储中 删除消息,即执行第6步;如果MQ在指定时间内没有收到ACK,则认为消息消费失 败,会尝试重新push消息,重复执行4、5、6步骤
(6) MQ删除消息。
存储介质
-
关系型数据库
普通关系型数据库(如Mysql)在单表数据量达到千万级别的情况下,其IO读写性能往往 会出现瓶颈。在可靠性方面,该种方案非常依赖DB,如果一旦DB出现故障,则MQ的消息就无法落盘存储会导致线上故障。
-
文件系统(推荐)
目前业界较为常用的几款产品(RocketMQ/Kafka/RabbitMQ)均采用的是消息刷盘 至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)。消息刷盘为消息存储提供了一种高效率、高可靠性和高性能的数据持久化方式。除非部署MQ机器本身或是本地磁盘挂了,否则一般是不会出现无法持 久化的故障问题。
性能对比
- 文件系统 > 关系型数据库
消息的存储和发送
-
消息存储
**RocketMQ的消息用顺序写, 保证了消息存储的速度。**目前的高性能磁盘,顺序写速度可以达到600MB/s, 超过了一般网卡的传输速度。但是磁盘随机写的速度只有大概100KB/s,和顺序写的性能相差6000倍
-
消息发送
Linux操作系统分为【用户态】和【内核态】,文件操作、网络操作需要涉及这两种形态 的切换,免不了进行数据复制。 一台服务器 把本机磁盘文件的内容发送到客户端,一般分为两个【读】和【写】两个步骤,这两个看似简单的操作,实际进行了4 次数据复制,分别是:
- 从磁盘复制数据到内核态内存;
- 从内核态内存复 制到用户态内存;
- 然后从用户态 内存复制到网络驱动的内核态内存;
- 最后是从网络驱动的内核态内存复 制到网卡中进行传输。
通过使用mmap的方式(零拷贝),可以省去向用户态的内存复制,提高速度。这种机制在Java中是 通过MappedByteBuffer实现的 RocketMQ充分利用了上述特性,提高消息存盘和网络发送的速度。
这里需要注意的是,采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G 的文件至用户态的虚拟内存,这也是为何RocketMQ 默认设置单个CommitLog日志数据文件为1G的原因了。
-
零拷贝
-
mmap + write 方式
优点:即使频繁调用,使用小块文件传输,效率也很高
缺点:不能很好的利用 DMA 方式,会比 sendfile 多消耗 CPU,内存安全性控制复杂,需要避免 JVM Crash问题。 -
sendfile 方式
优点:可以利用 DMA 方式,消耗 CPU 较少,大块文件传输效率高,无内存安全新问题。
缺点:小块文件效率低于 mmap 方式,只能是 BIO 方式传输,不能使用 NIO。
RocketMQ 选择了第一种方式,mmap+write 方式,因为有小块数据传输的需求,效果会比 sendfile 更好。
消息存储结构
-
简介
RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。
每个Topic下的每个Message Queue都有一个对应 的ConsumeQueue文件。
-
存储结构例图
-
CommitLog:存储消息的元数据
-
ConsumerQueue:存储消息在CommitLog的索引
-
IndexFile:为了消息查询提供了一种通过key或时间区间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程
-
-
配置文件对应参数
#commitLog 存储路径 storePathCommitLog=/usr/local/java/rocketmq/store/commitlog #消费队列存储路径存储路径 storePathConsumeQueue=/usr/local/java/rocketmq/store/consumequeue #消息索引存储路径 storePathIndex=/usr/local/java/rocketmq/store/index
刷盘机制
-
简介
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式,分布式同步刷盘和异步刷盘。
-
同步刷盘
在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。
-
异步刷盘
在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘动作,快速写入。
-
配置
同步刷盘还是异步刷盘,都是通过Broker配置文件里的flushDiskType参数设置的,这个参数被配置成SYNC_FLUSH、ASYNC_FLUSH中的一个。
#刷盘方式 #- ASYNC_FLUSH 异步刷盘 #- SYNC_FLUSH 同步刷盘 flushDiskType=SYNC_FLUSH
高可用性机制
-
RocketMQ集群架构图
-
Master和Slave配合达到高可用
RocketMQ分布式集群是通过Master和Slave的配合达到高可用性的。
Master和Slave的区别:
-
在Broker的配置文件中,参数brokerId的值为0表明这个Broker是Master,大于0表明这个Broker是Slave,同时brokerRole参数也会说明这个Broker是Master还是Slave。
-
Master角色的Broker支持读和写,Slave角色的Broker仅支持读,也就是Producer只能和Master角色的Broker连接写入消息;Consumer可以连接Master角色的Broker,也可以连接Slave角色的Broker来读取消息。
-
配置说明
#- ASYNC_MASTER 异步复制Master #- SYNC_MASTER 同步双写Master #- SLAVE brokerRole=SLAVE
消息消费高可用
-
自动切换机制
在Consumer的配置文件中,并不需要设置是从Master读还是从Slave 读,当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave读。有了自动切换Consumer这种机制,当一个Master角色的机器出现故障后,Consumer仍然可以从Slave读取消息,不影响Consumer程序。
消息发送高可用
-
Broker组
在创建Topic的时候,把Topic的多个Message Queue创建在多个Broker组上(相同 Broker名称,不同 brokerId的机器组成一个Broker组),这样当一个Broker组的 Master不可 用后,其他组的Master仍然可用,Producer仍然可以发送消息。
RocketMQ目前还不支持把Slave自动转成Master,如果机器资源不足,需要把Slave转成Master,则要手动停止Slave角色Broker,更改配置文件,用新的配置文件启动Broker。
消息主从复制
-
复制方式
如果一个Broker组有Master和Slave,消息需要从Master复制到Slave 上,有同步和异步两种复制方式。
-
同步复制
同步复制方式是等Master和Slave均写成功后才反馈给客户端写成功状态。在同步复制方式下,如果Master出故障, Slave上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量。
-
异步复制
异步复制方式是只要Master写成功即可反馈给客户端写成功状态。在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果Master出了故障,有些数据因为没有被写入Slave,有可能会丢失;
-
配置
同步复制和异步复制是通过Broker配置文件里的brokerRole参数进行设置的,这个参数可以被设置成ASYNC_MASTER、 SYNC_MASTER、SLAVE三个值中的一个
-
总结
异步刷盘保证吞吐量,主从复制保证数据不丢失
实际应用中要结合业务场景,合理设置刷盘方式和主从复制方式, 尤其是SYNC_FLUSH 方式,由于频繁地触发磁盘写动作,会明显降低性能。通常情况下,应该把Master和Slave配置成ASYNC_FLUSH的刷盘方式,主从之间配置成SYNC_MASTER的复制方式,这样即使有一台机器出故障,仍然能保证数据不丢,是个不错的选择。
负载均衡
Producer负载均衡
-
概念
Producer端,每个实例在发消息的时候,默认会轮询所有的message queue发送,以达到让消息平均落在不同的queue上。而由于queue可以散落在不同的broker,所以消息 就发送到不同的broker下
-
样例图
Consumer负载均衡
-
集群模式
在集群消费模式下,每条消息只需要投递到订阅这个topic的Consumer Group下的一个实例即可。RocketMQ采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue。而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这时候会按照queue的数量和实例的数量平均分配queue给每个实例。
-
AllocateMessageQueueAveragely算法(默认)
-
AllocateMessageQueueAveragelyByCircle算法
-
广播模式
由于广播模式下要求一条消息需要投递到一个消费组下面所有的消费者实例,所以也就没有消息被分摊消费的说法。 在实现上,就是在consumer分配queue的时候,所有consumer都分到所有的queue。
-
注意事项
-
集群模式下,queue都是只允许分配只一个实例。这是由于如果多个实例同时消费一个queue的消息,由于拉取哪些消息是consumer主动控制的,那样会导致同一个消息在不同的实例下被消费多次,所以算法上都是一个queue只分给一个 consumer实例,一个consumer实例可以允许同时分到不同的queue。
-
集群模式下,需要控制让queue的总数量大于等于consumer的数量。通过增加consumer实例去分摊queue的消费,可以起到水平扩展的消费能力的作用。而有实例下线的时候,会重新触发负载均衡,这时候原来分配到的queue将分配到其他实例上继续消费。 但是如consumer实例的数量比message queue的总数量还多的话,多出来的 consumer实例将无法分到queue,也就无法消费到消息,也就无法起到分摊负载的作用了。
消息重试
顺序消息的重试
-
概念
对于顺序消息,当消费者消费消息失败后,消息队列 RocketMQ 会自动不断进行消息重试(每次间隔时间为 1 秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并处理消费失败的情况,避免阻塞现象的发生。
无序消息的重试
-
概念
对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。
无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。
-
重试次数
一条消息无论重试多少次,这些重试消息的MessageID不会改变
-
配置方式
- 配置重试方式
集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置(三种方式任选一种)
- 配置消息不重试方式
集群消费方式下,消息失败后期望消息不重试,需要捕获消费逻辑中可能抛出的异常,最终返回Action.CommitMessage,此后这条消息将不会在重试
- 自定义消息最大重试次数
- 获取消息重试次数
死信队列
-
概念
当一条消息初次消费失败,消息队列 RocketMQ 会自动进行消息重试;达到最大重试次数后,若消费依然失败,则表明消费者在正常情况下无法正确地消费该消息,此时,消息队列 RocketMQ 不会立刻将消息丢弃,而是将其发送到该消费者对应的特殊队列中。
在消息队列 RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。
死信特性
- 死信消息特性
-
不会再被消费者正常消费
-
有效期与正常消息相同,均为3天,3天后会被自动删除。
- 死信队列具有以下特性
-
一个死信队列对应一个Group ID,而不是对应单个消费者实例
-
如果一个Group ID未产生死信消息,消息队列RocketMQ不会为其创建相应的死信队列
-
一个死信队列包含了对应Group ID产生的所有死信消息,不论该消息属于哪个Topic
查看死信信息
-
控制台查询出现死信队列主题信息
-
消息界面根据主题查询死信消息
-
选择重新发送消息
一条消息进入死信队列,意味着某些因素导致消息者无法正常消费该消息,因此,通常需要对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列RocketMQ控制台重新发送该消息,让消费者重新消费一次
消费幂等
-
概念
消息队列RocketMQ消费者在接收到消息以后,有必要根据业务上的唯一key对消息做幂等处理的必要性
消息幂等必要性
- 消息重复发送原因
处理方式
- 通过消息key处理
参考链接
https://www.jianshu.com/p/7a5ab42e5eba