RocketMQ高级篇

RocketMQ学习之旅三

本文对RocketMQ消息的存储模型、高可用性机制、负载均衡、消息重试、死信队列和消费幂等等知识点做一些总结与分享。

一、存储介质

分布式队列因为有高可靠性的要求,所以数据要进行持久化存储,目前有如下两种存储介质:

  1. 关系型数据库DB
  • Apache下开源的另一款MQ【ActiveMQ(默认采用的KahaDB做消息存储)】可选用JDBC的方式来做消息持久化,通过建党的xml配置信息即可实现JDBC消息存储。
  • 由于普通关系型数据库(如MySQL)在单表数据量达到千万级别的情况下,其IO读写性能往往会出现瓶颈。在可靠性方面,该方案非常依赖DB,如果一旦DB出现故障,则MQ的消息就无法落盘存储会导致线上故障。
  1. 文件系统
  • 目前业界比较常用的几款产品(RocketMQ/Kafka/RabbitMQ)均采用的是消息刷盘至所部署虚拟机/物理机的文件系统来做持久化(刷盘一般可以分为异步刷盘和同步刷盘两种模式)。消息刷盘为消息存储提供了一种高效率、高性能的数据持久化方式。除非部署MQ机器或是本地磁盘挂了,否则一般是不会出现无法持久化的故障问题。

性能对比: 文件系统 > 关系型数据库

二、RocketMQ 消息的存储和发送

消息存储

  • 磁盘如果使用得当,磁盘的速度完全可以匹配上网络的数据传输速度。目前的高性能磁盘,顺序写速度可以达到600MB/s,超过了一般网卡的传输速度。但是磁盘随机写的速度只有大概100KB/s,和顺序写的性能相差6000倍。因为有如此巨大的速度差别,好的消息队列系统会比普通的消息队列系统速度快多个数量级。
  • RocketMQ的消息用顺序写,保证了消息存储的速度。

消息发送

linux操作系统分为【用户态】和【内核态】,文件操作、网络操作需要涉及这两种形态的切换,免不了进行数据复制。
一台服务器把把本机磁盘文件的内容发送到客户端,一般分为连个步骤:
1)read;读取本地文件内容;
2)write;将读取的内容通过网络发送出去。
这两个看似简单的操作,实际上进行了4次数据复制,分别是:
1)从磁盘复制数据到内核态内存;
2)从内核态内存复制到用户态内存;
3)然后从用户太内存复制到网络驱动的内核态内存;
4)从网络驱动的内核态内存复制到网卡中进行传输。
数据 -1⃣️-》内核态 -2⃣️-》 用户态 -3⃣️-》 网络启动内核 -4⃣️-》 网卡 --》 数据

通过使用mmap的方式,可以省去向用户态的内存复制(省略步骤2),提高速度。这种机制在java中是通过MappedByteBuffer实现的RocketMQ充分利用了上述特性,也就是所谓的“零拷贝”技术,提供消息存盘和网络发送的速度。

注意:采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G的文件至用户态内存,这也就是为何RocketMQ默认设置单个CommitLog日志数据文件为1G的原因。

三、RocketMQ 消息存储结构

rocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每个Topic下的每个Message Queue 都有一个对应的ConsumeQueue文件。如下图所示:
RocketMQ高级篇_第1张图片
CommitLog:存消息的元数据
ConsumerQueue:存储消息在CommitLog的索引(如果ConsumerQueue丢失,不要紧张,通过CommitLog可以将其还原)
IndexFile:为了消息查询提供了一种通过key或者时间来查询消息的方法,这种通过IndexFile来查找消息的方法不影响发送与消费消息的主流程。

四、刷盘机制

RocketMQ的消息时存储到磁盘上的,这样即保证断电后恢复,又可以让存储的消息量超出内存的限制。RocketMQ为了提高性能,会尽可能地保证磁盘的顺序写。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式,分布式同步刷盘和异步刷盘。
RocketMQ高级篇_第2张图片

  1. 同步刷盘(保证量消息的可靠性)
    - 在返回写成功状态时,消息已经被写入磁盘
    - 具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。

  2. 异步刷盘(可以提高消息发送的吞吐量)
    - 在返回写成功状态时,消息可能只能被写入来内存的PAGECACHE,写操作的返回快,吞吐量大;当内存里的消息量积累搭配一定程度时,统一触发写磁盘动作,快速写入。

配置:通过配置文件中的 flushDiskType 参数进行配置 同步【SYNC_FLUSH】/ 异步【ASYNC_FLUSH】

五、RocketMQ 的高可用性机制

RocketMQ高级篇_第3张图片

1.消息消费高可用

在Consumer的配置文件中,并不需要设置是从Master读还是从Slave中读,当MAster不可用或者繁忙的时候,Consumer会被自动切换到Slave读。有了自动切换Consumer这种机制,当一个Master角色的机器出现故障后,Consumer仍然可以从Slave读取消息,不影响Consumer程序。

2. 消息发送高可用

在创建Topic的时候,把Topic的多个Message Queue 创建在多个Broker组上,这样当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息。

RocketMQ目前还不支持把Slave自动转换成Master,如果机器资源不足,需要把Slave转成Master,则要手动停止Slave角色的Broker,更改配置文件,用新的配置文件启动Broker。

3.消息主从复制

1)同步复制
2)异步复制

通过 brokerRole 参数配置 SLAVE【从结点】/ SYNC_MASTER【同步复制Master】/ ASYNC_MASTRER【异步复制Master】

4. 推荐的模式

异步刷盘+同步复制

六、负载均衡

1.Producer负载均衡

Producer端,每个实例在发消息端时候,默认会轮询所有端messgae queue 发送,以达到让消息平均落在不同端queue上。

2.Consumer负载均衡(集群模式)

在集群消费模式下,每条消息只需要投递到订阅这个topic到Consumer Group下的一个实例即可。RocketMQ采用主动拉取的方式拉取并消费消息,在拉取的时候需要明确指定拉取哪一条message queue,而每当实例的数量有变更,都会触发一次所有实例的负载均衡,这个时候会按照queue的数量和实例的数量平均分配queue给每个实例。

默认的分配算法
RocketMQ高级篇_第4张图片
AllocateQueueAveragely 每个consumer实例平均分配每个consume queue
另一种算法
RocketMQ高级篇_第5张图片
AllocateMessageQueueAveragelyByCircle,平均分摊每一条queue,只是以环状轮流份queue的形式。

3.注意:

  • 集群模式下,queue都是只允许分配一个实例
  • 通过增加consumer实例去分摊queue的消费,可以起到水平扩张消费能力的作用。
  • 有实例下线的时候,会重新触发负载均衡,这时候原来分配到的queue将分配到其他的实例上继续消费。
  • 如果consumer实例的数量比message queue的总数量还多的话,多出来的consumer实例将无法分到queue,也就无法消费到消息,起到分摊负载的作用。
  • 所以需要空中queue的总数量大于等于consumer的数量

七、消息重试

1.顺序消息的重试

对于顺序消息,当消费这消费消息失败后,消息队列RocketMQ会自动不断进行消息重试(每次时间间隔为1秒),这时,应用会出现消息消费被阻塞的情况。因此,在使用顺序消息时,务必保证应用能够及时监控并并处理消息消费失败的情况,避免阻塞现象的发生。

2.无序消息重试

对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,可以通过设置返回状态达到消息重试的结果。无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。

3.重试次数

消息队列RocketMQ默认允许每条消息最多重试16次。如下图所示:
RocketMQ高级篇_第6张图片
如果消息重试16次后仍然失败,消息将不再投递。
如果严格按照重试时间来计算时间间隔,某条消息在一直消费失败的前提下,将会在接下来的4小时46分钟内进行16次重试,超过这个时间范围,消息将不再投递。
一条消息无论重试多少次,这些重试消息的Message ID 不会改变。

4.重试的实现

集群消费方式下,消息消费失败后期望消息重试,需要在消息监听器接口的实现中明确进行配置,有如下三种方式:

  1. 返回 Action.ReconsumeLater
  2. 返回 null
  3. 直接抛出异常,消息将重试

5.自定义重试的最大次数

Properties properties = new Properties();
//配置对应 Group ID 的最大消费重试次数
properties.put(PropertyKeyConst.MaxReconsumeTimes,"20");
Consumer consumer = ONSFactory.createConsumer(properties);

1.最大重试次数小于等于16次,则重试时间间隔同上图所示
2.最大重试次数大于16次,超过16次的重试时间间隔均为每次2小时

6.注意事项

1.消息最大重试次数的设置相对相同Group ID 下的所有实例Consumer有效
2.采用覆盖对方式生效,最后启动对Consumer实例会覆盖之前启动对配置

八、死信队列

当一条消息初次消费失败,消息队列RocketMQ会自动进行消息重试;达到最大重试次数后,若依旧消费失败,则表明消费者在正常情况下无法正确的消费该消息,此时,消息队列RocketMQ不会立即将消息丢弃,而是将其发送到该消费者对应对特殊队列中。可以通过控制台查看,并重新发送该消息。

死信消息的特征

1.不会再被消费者正常消费
2.有效期与正常消息相同,均为3天,3天以后会被自动删除。因此,请在死信消息产生后对3天内即使处理。

死信队列的特征

1.一个死信队列对应一个Group ID ,而不是对应单个消费者实例
2.如果一个Group ID 未产生死信消息,消息队列RocketMQ 不会为其创建相应的死信队列
3.一个死信队列包含了对应Group ID产生的所有死信消息,不论该消息属于哪个Topic

九、幂等消息

消息队列RocketMQ消费者在接收到消息以后,有必要根据业务上的唯一Key对消息做幂等处理。

消息队列RocketMQ可能会fas重复发送消息的情况:

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

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

3)负载均衡时消息重复(包括但不限于网络抖动、broker重启以及订阅方式应用重启)
当消息队列RocketMQ对Broker或者客户端重启、扩容或者缩容时,会触发Rebalance,此时消费者也可能会收到重复对消息。

处理方式

Producter在发送消息时定义唯一对消息key
订阅方收到消息时可以依据消息对Key进行幂等处理。

学习总结参考 黑马程序员《RocketMQ系统精讲,经受历年双十一狂欢节考验的分布式消息中间件》,图片来源于网络侵删~

你可能感兴趣的:(RocketMQ高级篇)