rocketMQ高级特性

高级特性

ACL机制

权限控制,主要为Rocket提供Topic资源级别的用户访问权限控制,用户在使用Rocket权限控制时,可以在Cliend客户端通过RPCHook注入AccessKey和SecretKey签名;同时,将对应的权限控制属性(包括topic的访问权限,IP白名单和AccessKey和SecretKey签名等).设置在$ROCKETMQ_HOME/conf/plain_acl.yml的配置文件中。broker端对AccessKey所拥有的权限进行校验,校验不过,抛出异常。如果想使用ACL需要引入依赖包

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-acl</artifactId>
    <version>4.9.4</version>
</dependency>

在broker.conf将aclEnable=true

消息存储机制

磁盘文件保存的速度

RocketMq使用了顺序写,速度可以达到600MB/s。简单描述一下,如果在磁盘系统中要寻找一个block时,最起码要经历寻址和传输才可以读取到block的数据,而对于下一个block如果不知道位置的话,还是需要寻找位置(随机)。但是如果这个block的起始位置就在刚刚访问的block的后面,那就不需要寻找,直接可以传输数据(顺序)。

零拷贝

Linux系统分为内核态和用户态,文件操作、网络传输需要涉及到这两个状态的切换,避免不了的需要进行数据的复制。一台服务器如果要将本地磁盘的数据发送到客户端,一般分为两个步骤。1:read 读取本地文件内容、2:write将读取对的文件内容通过网络发送出去。这两个简单的步骤实际经历了四次的数据复制

  1. 从磁盘复制数据到内核态的内存
  2. 从内核态内存复制到用户态内存
  3. 从用户态内存复制到网络驱动的内核态内存
  4. 网络驱动内核态内存复制到网卡中进行传输。

rockerMQ使用MMAP来实现零拷贝。开辟了一块MappedByteBuffer(磁盘映射区域),将用户空间的虚拟地址和内核空间的虚拟地址指向同一个物理内存地址,这样用户空间和内核空间共享同一个内存数据,就可以直接对磁盘进行操作了,从而省去了cpu拷贝进缓冲区的操作。节省了很大的资源开销。需要注意的是,采用MappedByteBuffer这种内存映射的方式有几个限制,其中之一是一次只能映射1.5~2G的文件至用户态的虚拟内存,这也是为何RocketMq默认设置单个CommitLog日志数据文件为1G的原因了。

消息存储结构

RocketMQ消息的存储是由ConsumeQueue和CommitLog配合完成 的,消息真正的物理存储文件是CommitLog,ConsumeQueue是消息的逻辑队列,类似数据库的索引文件,存储的是指向物理存储的地址。每 个Topic下的每个Message Queue都有一个对应的ConsumeQueue文件。
在这里插入图片描述

  1. 消息都存储在CommitLog,每次消费者读取消息的时候,都是根据每个ConsumeMQ的ConsumeQueue的元数据,然后在根据当前的偏移量去到CommitLog获取真正的消息。

  2. CommitLog:消息主体以及元数据的存储主体,存储Producer端写入的消息主体内容,消息内容不是定长的。单个文件大小默认1G ,文件名长度为20位,左边补零,剩余为起始偏移量,比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073741824;当第一个文件写满了,第二个文件为00000000001073741824,起偏移量为1073741824,以此类推。消息主要是顺序写入日志文件,当文件满了,写入下一文件;在这里插入图片描述

  3. ConsumeQueue:消息消费队列,引入的目的主要是提高消息消费的性能RocketMQ是基于主题topic的订阅模式,消息消费是针对主题进行。如果要遍历commitlog文件根据topic检索消息是非常低效。Consumer即可根据ConsumeQueue来查找待消费的消息。其中,ConsumeQueue(逻辑消费队列)作为消费消息的索引。且consumequeue文件采取定长设计,每个条目共20个字节:

    1. 保存了指定Topic下的队列消息在CommitLog中的起始物理偏移量(8字节)offset(commitLogOffset
    2. 消息大小size(4字节),(msgSize)
    3. 消息Tag的HashCode值(8字节),(tagCode)。

    consumequeue文件可以看成是基于topic的commitlog索引文件,故consumequeue文件夹的组织方式如下:

    在这里插入图片描述
    在这里插入图片描述

    具体存储路径为:XX/store/consumequeue/{topic}/{queueId}/{fileName}

    单个文件由30W个条目组成,可以像数组一样随机访问每一个条目,每个ConsumeQueue*文件大小约***5.72M

  4. IndexFile:IndexFile(索引文件)提供了一种可以通过key或时间区间来查询消息的方法。

    1. Index文件的存储位置是: XXX/store/index/${fileName}

    2. 文件名fileName是以创建时的时间戳命名的在这里插入图片描述

    3. 固定的单个IndexFile文件大小约为400M

    4. 一个IndexFile可以保存 2000W个索引

    5. IndexFile的底层存储设计为在文件系统中实现HashMap结构,故rocketmq的索引文件其底层实现为hash索引

  5. 可以设置通过那个时间点后来进行消费

       consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_TIMESTAMP);
     consumer.setConsumeTimeout();
    

过期删除策略

CommitLog文件的删除过程

刷盘机制

RocketMQ 的所有消息都是持久化的,先写入系统 PageCache(内存中物理页),然后刷盘,可以保证内存与磁盘都有一份数据, 访问时,直接从内存读取。消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式,分布式同步刷盘和异步刷盘。

//ASYNC_FLUSH=异步刷盘,SYNC_FLUSH=同步刷盘
flushDiskType=ASYNC_FLUSH

同步刷盘

同步刷盘与异步刷盘的唯一区别是异步刷盘写完 PageCache直接返回,而同步刷盘需要等待刷盘完成才返回, 同步刷盘流程如下:

  1. 写入 PageCache后,线程等待,通知刷盘线程刷盘。通过PageCache是内存和磁盘做了映射。直接修改内存数据,由操作系统调度将内存数据和磁盘数据进行同步。
  2. 刷盘线程刷盘后,唤醒前端等待线程,可能是一批线程。
  3. 前端等待线程向用户返回成功

异步刷盘

在机械磁盘 15000 转磁盘测试顺序写文件,速度可以达到 300M 每秒左右,而线上的网卡一般都为千兆网卡,写磁盘速度明显快于数据网络入口速度,那么是否可以做到写完内存就向用户返回,由后台线程刷盘呢?

  1. 由于磁盘速度大于网卡速度,那么刷盘的进度肯定可以跟上消息的写入速度。
  2. 万一由于此时系统压力过大,可能堆积消息,除了写入 IO,还有读取 IO,万一出现磁盘读取落后情况, 会不会导致系统内存溢出,答案是否定的,原因如下:写入消息到 PageCache时,如果内存不足,则尝试丢弃干净的 PAGE,腾出内存供新消息使用,策略是LRU 方式。如果干净页不足,此时写入 PageCache会被阻塞,系统尝试刷盘部分数据,大约每次尝试 32个 PAGE , 来找出更多干净 PAGE。

综上,内存溢出的情况不会出现。

同步复制和异步复制

//复制规则 ASYNC_MASTER 异步复制MASTER
// SYNC_MASTER 同步双写Master
brokerRole=ASYNC_MASTER

如果一个Broker组有Master和Slave,消息需要从Master复制到Slave 上,有同步和异步两种复制方式。

  1. 同步复制

    同步复制方式是等Master和Slave均写 成功后才反馈给客户端写成功状态;

    在同步复制方式下,如果Master出故障,Slave上有全部的备份数据,容易恢复,但是同步复制会增大数据写入延迟,降低系统吞吐量。

  2. 异步复制

    异步复制方式是只要Master写成功 即可反馈给客户端写成功状态。

    在异步复制方式下,系统拥有较低的延迟和较高的吞吐量,但是如果Master出了故障,有些数据因为没有被写 入Slave,有可能会丢失;

  3. 配置

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

  4. 小结

    在这里插入图片描述

    实际应用中要结合业务场景,合理设置刷盘方式和主从复制方式, 尤其是SYNC_FLUSH方式,由于频繁地触发磁盘写动作,会明显降低性能。通常情况下,应该把Master和Save配置成ASYNC_FLUSH的刷盘 方式,主从之间配置成SYNC_MASTER的复制方式,这样即使有一台机器出故障,仍然能保证数据不丢,是个不错的选择。

高可用机制

RocketMQ分布式集群是通过Master和Slave的配合达到高可用性的。

Master和Slave的区别:

  1. 在Broker的配置文件中,参数brokerId的值为0表明这个Broker是Master,
  2. 大于0表明这个Broker是Slave,
  3. brokerRole参数也说明这个Broker是Master还是Slave。(SYNC_MASTER/ASYNC_MASTER/SALVE)
  4. Master角色的Broker支持读和写,Slave角色的Broker仅支持读。
  5. Consumer可以连接Master角色的Broker,也可以连接Slave角色的Broker来读取消息。

消息消费高可用

在Consumer的配置文件中,并不需要设置是从Master读还是从Slave 读,当Master不可用或者繁忙的时候,Consumer会被自动切换到从Slave 读。

有了自动切换Consumer这种机制,当一个Master角色的机器出现故障后,Consumer仍然可以从Slave读取消息,不影响Consumer程序。

这就达到了消费端的高可用性。

消息发送高可用

如何达到发送端的高可用性呢?

在创建Topic的时候,把Topic的多个Message Queue创建在多个Broker组上(相同Broker名称,不同brokerId的机器组成一个Broker组),这样既可以在性能方面具有扩展性,也可以降低主节点故障对整体上带来的影响,而且当一个Broker组的Master不可用后,其他组的Master仍然可用,Producer仍然可以发送消息的。

RocketMQ目前还不支持把Slave自动转成Master,如果机器资源不足,需要把Slave转成Master。

  1. 手动停止Slave角色的Broker。
  2. 更改配置文件。
  3. 用新的配置文件启动Broker。

在这里插入图片描述

如果需要保证消息严格顺序的场景下,由于在主题层面无法保证严格顺序,所以必须指定队列来发送消息,对于任何一个队列,它一定是落在一组特定的主从节点上,如果这个主节点宕机,其他的主节点是无法替代这个主节点的,否则就无法保证严格顺序。

RocketMQ 引入 Dledger,使用新的复制方式,可以很好地解决这个问题。Dledger 在写入消息的时候,要求至少消息复制到半数以上的节点之后,才给客户端返回写入成功,并且它是支持通过选举来动态切换主节点的。

举例:

假如有3个节点,当主节点宕机的时候,2 个从节点会通过投票选出一个新的主节点来继续提供服务,相比主从的复制模式,解决了可用性的问题。

由于消息要至少复制到 2 个节点上才会返回写入成功,即使主节点宕机了,也至少有一个节点上的消息是和主节点一样的。

Dledger在选举时,总会把数据和主节点一样的从节点选为新的主节点,这样就保证了数据的一致性,既不会丢消息,还可以保证严格顺序。

存在问题:

当然,Dledger的复制方式也不是完美的,依然存在一些不足:

  1. 比如,选举过程中不能提供服务。
  2. 最少需要 3 个节点才能保证数据一致性,3 节点时,只能保证 1 个节点宕机时可用,如果 2个节点同时宕机,即使还有 1 个节点存活也无法提供服务,资源的利用率比较低。
  3. 另外,由于至少要复制到半数以上的节点才返回写入成功,性能上也不如主从异步复制的方式快。

负载均衡

RocketMQ中的负载均衡都在Client端完成,具体来说的话,主要可以分为Producer端发送消息时候的负载均衡和Consumer端订阅消息的负载均衡。

Producer的负载均衡

在这里插入图片描述

如图所示,5 个队列可以部署在一台机器上,也可以分别部署在 5 台不同的机器上,发送消息通过轮询队列的方式 发送,每个队列接收平均的消息量。通过增加机器,可以水平扩展队列容量。 另外也可以自定义方式选择发往哪个队列。也可以重写负载均衡策略,比如顺序消息就是强制同一笔业务的消息发送到一个队列中

Consumer的负载均衡

广播模式不存在负载均衡,广播模式,是每条消息都会发送给消费组中的所有消费者。
在这里插入图片描述

如图所示,如果有 5 个队列,2 个 consumer,那么第一个 Consumer 消费 3 个队列,第二consumer 消费 2 个队列。 这样即可达到平均消费的目的,可以水平扩展 Consumer 来提高消费能力。但是 Consumer 数量要小于等于队列数 量,如果 Consumer 超过队列数量,那么多余的Consumer 将不能消费消息 。

在RocketMQ中,Consumer端的两种消费模式(Push/Pull)底层都是基于拉模式来获取消息的,而在Push模式只是对pull模式的一种封装,其本质实现为消息拉取线程在从服务器拉取到一批消息后,然后提交到消息消费线程池后,又“马不停蹄”的继续向服务器再次尝试拉取消息。

如果未拉取到消息,则延迟一下又继续拉取。

在两种基于拉模式的消费方式(Push/Pull)中,均需要Consumer端在知道从Broker端的哪一个消息队列中去获取消息。

因此,有必要在Consumer端来做负载均衡,即Broker端中多个MessageQueue分配给同一个ConsumerGroup中的哪些Consumer消费。

要做负载均衡,必须知道一些全局信息,也就是一个ConsumerGroup里到底有多少Consumer。

知道了全局信息,才可以根据某种算法来分配,比如简单地平均分到各个Consumer。

在RocketMQ中,负载均衡或者消息分配是在Consumer端代码中完成的,Consumer*Broker处获得全局信息,然后自己做负载均衡,只处理分给自己的那部分消息。

Pull Consumer可以看到所有的Message Queue,而且从哪个Message Queue读取消息,读消息时的Offset都由使用者控制,使用者可以实现任何特殊方式的负载均衡。

DefaultMQPullConsumer有两个辅助方法可以帮助实现负载均衡,一个是registerMessageQueueListener函数,一个是MQPullConsumerScheduleService(使用这个Class类似使用DefaultMQPushConsumer,但是它把Pull消息的主动性留给了使用者)

DefaultMQPushConsumer的负载均衡过程不需要使用者操心,客户端程序会自动处理,每个DefaultMQPushConsumer启动后,会马上会触发一个doRebalance动作;而且在同一个ConsumerGroup里加入新的DefaultMQPush-Consumer时,各个Consumer都会被触发doRebalance动作。

负载均衡的分配粒度只到Message Queue,把Topic下的所有Message Queue分配到不同的Consumer中

负载均衡算法默认使用的是AllocateMessageQueueAveragely。位于org.apache.rocketmq.client.consumer.rebalance包下面,当然我们也是可以设置具体的负载均衡算法的

consumer.setAllocateMessageQueueStrategy(new AllocateMessageQueueAveragely());

以AllocateMessageQueueAveragely策略为例,如果创建Topic的时候,把Message Queue数设为3,当Consumer数量为2的时候,有一个Consumer需要处理Topic三分之二的消息,另一个处理三分之一的消息;当Consumer数量为4的时候,有一个Consumer无法收到消息,其他3个Consumer各处理Topic三分之一的消息。

可见Message Queue数量设置过小不利于做负载均衡,通常情况下,应把一个Topic的MessageQueue数设置为16。

Consumer端的心跳包发送

在Consumer启动后,它就会通过定时任务不断地向RocketMQ集群中的所有Broker实例发送心跳包(其中包含了消息消费分组名称、订阅关系集合、消息通信模式和客户端id的值等信息)。Broke端在收到Consumer的心跳消息后,会将它维护在ConsumerManager的本地缓存变量—consumerTable,同时并将封装后的客户端网络通道信息保存在本地缓存变量—channelInfoTabl中,为之后做Consumer端的负载均衡提供可以依据的元数据信息。

Consumer端实现负载均衡的核心类—RebalanceImpl

在Consumer实例的启动流程中启动MQClientInstance实例的部分,会完成负载均衡服务线程—RebalanceService的启动(每隔20s执行一次)。

消息消费队列在同一消费组不同消费者之间的负载均衡,其核心设计理念是在一个消息消费队列在同一时间只允许被同一消费组内的一个消费者消费,一个消息消费者能同时消费多个消息队列。

消息重试

顺序消息的重试

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

无序消息的重试

对于无序消息(普通、定时、延时、事务消息),当消费者消费消息失败时,您可以通过设置返回状态达到消息重试的结果。

无序消息的重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息。

  1. 重试次数

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

    第几次重试与上次重试的间隔时间第几次重试与上次重试的间隔时间
    110秒97分钟
    230秒108分钟
    31分钟119分钟
    42分钟1210分钟
    53分钟1320分钟
    64分钟1430分钟
    75分钟151小时
    86分钟162小时

    如果消息重试 16 次后仍然失败,消息将不再投递。如果严格按照上述重试时间间隔计算,某条消息在一直消费失败的前提下,将会在接下来的 4 小时 46 分钟之内进行 16 次重试,超过这个时间范围消息将不再重试投递。

    注意: 一条消息无论重试多少次,这些重试消息的 Message ID 不会改变。

  2. 配置方式

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

    1. 返回 ConsumeConcurrentlyStatus.RECONSUME_LATER; (推荐)
    2. 返回 Null
    3. 抛出异常

    在集群模式下,如果消息失败后期望消息不充实,需要俘获消费异常,然后最终返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS,此后这条消息将不会再重试。

自定义消息最大重试次数

消息队列 RocketMQ 允许 Consumer 启动的时候设置最大重试次数,重试时间间隔将按照如下策略:

  • 最大重试次数小于等于 16 次,则重试时间间隔同上表描述。

  • 最大重试次数大于 16 次,超过 16 次的重试时间间隔均为每次 2 小时。

    // 设置重新消费的次数 
    // 共16个级别,大于16的一律按照2小时重试 
    consumer.setMaxReconsumeTimes(20); 
    

需要注意

  • 消息最大重试次数的设置对相同 Group ID 下的所有 Consumer 实例有效
  • 如果只对相同 Group ID 下两个 Consumer 实例中的其中一个设置了MaxReconsumeTimes,那么该配置对两个 Consumer 实例均生效。
  • 配置采用覆盖的方式生效,即最后启动的 Consumer 实例会覆盖之前的启动实例的配置
  • 重试16次之后,消息将不在重试,直接放到了死信队列里面

获取消息重试次数

消费者收到消息后,可按照如下方式获取消息的重试次数:MessageExt.getReconsumeTimes()

死信队列

RocketMQ中消息重试超过一定次数后(默认16次)就会被放到死信队列中,在消息队列RocketMQ 中,这种正常情况下无法被消费的消息称为死信消息(Dead-Letter Message),存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。可以在控制台Topic列表中看到“DLQ”相关的Topic,默认命名是:

  1. %RETRY%消费组名称(重试Topic)
  2. %DLQ%消费组名称(死信Topic)
  3. 死信队列也可以被订阅和消费,并且也会过期

死信特性

死信消息具有以下特性:

  1. 不会再被消费者正常消费。
  2. 有效期与正常消息相同,均为 3 天,3 天后会被自动删除。因此,请在死信消息产生后的 3天内及时处理。对应broker.conf中的fileReservedTime

死信队列具有以下特性

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

一条消息进入死信队列,意味着某些因素导致消费者无法正常消费该消息,因此,通常需要对其进行特殊处理。排查可疑因素并解决问题后,可以在消息队列 RocketMQ 控制台重新发送该消息,让消费者重新消费一次。

消息幂等

Rocketmq也是无法避免的会出现消息重复消费的情况的,这时候需要再业务端进行业务的处理

  • 发送时消息重复:当一条消息发送到服务端并完成持久化,此时出现网络闪断或者客户端宕机,导致服务端对客户端应答失败。此时生产者意识消息发送失败了并且尝试了重新发送消息,那么消费者后续就会受到两条内容一样消息ID也相同的消息。
  • 投递时消息重复:消息消费的场景下,消息已投递到消费者并完成了业务处理,但是当客户端给服务端返回成功应答的时候,出现网络闪断,那么服务端为了保证至少被消费一次,会再次将消息发送给消费者,那么消费者会收到两条消息内容一样,消息ID也一样的消息
  • 负载均衡消息重复(包括但不限于网络抖动,Broker重启以及订阅方应用重启)。当消息队列RocketMq的Broker或者客户端重启、扩容时,会触发再均衡。此时消费者可能会收到重复消息

要处理这种问题,我只有在业务上利用消息ID来做幂等的判断。

  • 19
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值