RocketMQ 面试题总结

RocketMq 优势

吞吐量高:单机吞吐量可达十万级
可用性高:分布式架构
消息可靠性高:经过参数优化配置,消息可以做到0丢失
功能支持完善:MQ功能较为完善,还是分布式的,扩展性好
支持10亿级别的消息堆积:不会因为堆积导致性能下降
可靠性高:天生为金融互联网领域而生,对于要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况
稳定性高:RoketMQ在上可能更值得信赖,这些业务场景在阿里双11已经经历了多次考验


RocketMQ 基本概念

RocketMQ主要有四大核心组成部分:NameServer、Broker、Producer 以及 Consumer 四部分。
在这里插入图片描述

NameServer

NameServer 是一个服务与注册的发现中心。也是整个 RocketMQ 的“大脑”,所以 RocketMQ 需要先启动 NameServer 再启动 RocketMQ 中的 Broker

NameServer 是一个几乎无状态节点,可集群部署,节点之间无任何信息同步。NameServer底层由 Netty 实现,是内存式存储,所以 NameServer 中的 broker、topic不会持久化。

Broker:

消息服务器(Broker)是消息存储中心,主要作用是接收来自 Producer 的消息并存储,Consumer 从这里取得消息。存储与消息相关的元数据,包括用户组、消费进度偏移量、队列信息等。从部署结构图中可以看出 Broker 有 Master 和 Slave 两种类型, Master 既可以写又可以读,Slave 不可以写只可以读

在这里插入图片描述

Topic (主题)

用来区分消息的种类,表示一类消息的逻辑名字,消息的逻辑管理单位,无论生产还是消费消息,都需要执行 Topic。

一个发送者可以发送消息给一个或者多个 Topic;
一个消息接受者可以订阅一个或多个 Topic 消息;

Tag (标签)

RocketMQ 支持在发送时给 topic 的消息设置 tag,用于同一主题下区分不同类型的消息。

来自同一业务单元的消息,可以根据不同业务目的在同一主题下设置不同标签。比如有一个 Topic 消息为水果,那么水果可以有其他的标签 可以是 香蕉、西瓜、草莓等等,我们可以把对应的消息,打上对应的标签(Tag),这个就是方便我们在消费的时候做对应的筛选。

标签能够有效地保持代码的清晰度和连贯性,并优化 RocketMQ 提供的查询系统。消费者可以根据Tag实现对不同子主题的不同消费逻辑,实现更好的扩展性。

Offset (偏移量)

在 RocketMQ 中,有很多 offset 的概念。一般我们只关心暴露到客户端的 offset。不指定的话,一般指的是消费者消息的偏移量(ConsumerOffset)

Message queue 是无限长的数组。一条消息进来下标就会涨 1,而这个数组的下标就是 offset。

Message queue 中的 max offset 表示消息的最大 offset,Consumer offset 可以理解为标记 Consumer Group 在一条逻辑 Message Queue 上,消息消费到哪里即消费进度。(Offset)
在 RocketMQ 中,有很多 offset 的概念。一般我们只关心暴露到客户端的 offset。不指定的话,一般指的是消费者消息的偏移量(ConsumerOffset)

Message queue 是无限长的数组。一条消息进来下标就会涨 1,而这个数组的下标就是 offset。

Message queue 中的 max offset 表示消息的最大 offset,Consumer offset 可以理解为标记 Consumer Group 在一条逻辑 Message Queue 上,消息消费到哪里即消费进度。


RocketMq的工作流程

1)首先启动NameServer。NameServer启动后监听端口,等待Broker、Producer以及Consumer连上来
2)启动Broker。启动之后,会跟所有的NameServer建立并保持一个长连接,定时发送心跳包。心跳包中包含当前Broker信息(ip、port等)、Topic信息以及Borker与Topic的映射关系
3)创建Topic。创建时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic
4)Producer发送消息。启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic所在的Broker;然后从队列列表中轮询选择一个队列,与队列所在的Broker建立长连接,进行消息的发送
5)Consumer消费消息。跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,进行消息的消费


RocketMQ 消费模式有几种?

消费模型由Consumer决定,消费维度为Topic。

集群消费
一条消息只会被同Group中的一个Consumer消费
多个Group同时消费一个Topic时,每个Group都会有一个Consumer消费到数据

广播消费
消息将对一个Consumer Group下的各个Consumer实例都消费一遍。即使这些Consumer属于同一个Consumer Group,消息也会被Consumer Group中的每个Consumer都消费一次

  /**
     * 3. 设置消息模式,默认是CLUSTERING
     * MessageModel.BROADCASTING 广播消费模式
     * MessageModel.CLUSTERING   集群消费模式
     */
    consumer.setMessageModel(MessageModel.BROADCASTING);

消费消息是 push 还是 pull ?

RocketMQ没有真正意义的push,都是pull,虽然有push类,但实际底层实现采用的是长轮询机制,即拉取方式。

broker端属性 longPollingEnable 标记是否开启长轮询。默认开启

追问:为什么要主动拉取消息而不使用事件监听方式?
事件驱动方式是建立好长连接,由事件(发送数据)的方式来实时推送。

如果 broker 主动推送消息有可能造成消息在 consumer 端堆积过多,同时又不能被其他 consumer 消费的情况。
而 pull 的方式可以根据当前自身情况来pull,不会造成过多的压力而造成瓶颈。所以采取了pull的方式。

// 1. 创建消费者(Pull)对象
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("GROUP_TEST");
// 1. 创建消费者(Push)对象
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("GROUP_TEST");

如何保证消息的顺序消费

首先多个 queue 只能保证单个 queue 里的顺序,queue 是典型的 FIFO,天然顺序。多个 queue 同时消费是无法绝对保证消息的有序性的。所以总结如下:

同一 topic,同一个 QUEUE,发消息的时候一个线程去发送消息,消费的时候 一个线程去消费一个 queue 里的消息。

追问:怎么保证消息发到同一个 queue ?
Rocket MQ 给我们提供了 MessageQueueSelector 接口,可以自己重写里面的接口,实现自己的算法,举个最简单的例子:判断i % 2 == 0,那就都放到 queue1 里,否则放到 queue2 里。

for (int i = 0; i < 5; i++) {
    Message message = new Message("orderTopic", ("hello!" + i).getBytes());
    producer.send(
        // 要发的那条消息
        message,
        // queue 选择器 ,向 topic中的哪个queue去写消息
        new MessageQueueSelector() {
            // 手动 选择一个queue
            @Override
            public MessageQueue select(
                // 当前topic 里面包含的所有queue
                List<MessageQueue> mqs,
                // 具体要发的那条消息
                Message msg,
                // 对应到 send() 里的 args,也就是2000前面的那个0
                Object arg) {
                // 向固定的一个queue里写消息,比如这里就是向第一个queue里写消息
                if (Integer.parseInt(arg.toString()) % 2 == 0) {
                    return mqs.get(0);
                } else {
                    return mqs.get(1);
                }
            }
        },
        // 自定义参数:0
        // 2000代表2000毫秒超时时间
        i, 2000);
}

消息重复消费

影响消息正常发送和消费的重要原因是网络的不确定性。

引起重复消费的原因
ACK
正常情况下在 consumer 真正消费完消息后应该发送 ack,通知 broker 该消息已正常消费,从 queue 中剔除
当 ack 因为网络原因无法发送到 broker,broker 会认为此条消息没有被消费,此后会开启消息重投机制把消息再次投递到 consumer

消费模式
在 CLUSTERING 模式下,消息在 broker 中会保证相同 group 的 consumer 消费一次,但是针对不同group 的 consumer 会推送多次

解决方案
去重操作直接放在了消费端,消费端处理消息的业务逻辑保持幂等性。那么不管来多少条重复消息,可以实现处理的结果都一样。


Broker中的消息被消费后会立即删除吗?

不会,每条消息都会持久化到 CommitLog 中,每个 Consumer 连接到 Broker 后会维持消费进度信息,当有消息消费后只是当前 Consumer 的消费进度(CommitLog的offset)更新了。

追问:那么消息会堆积吗?什么时候清理过期消息?
默认72小时后会删除不再使用的 CommitLog 文件

  • 检查这个文件最后访问时间
  • 判断是否大于过期时间
  • 指定时间删除,默认凌晨4点

如何保证消息不丢失

首先在如下三个部分都可能会出现丢失消息的情况:

  • Producer端
  • Broker端
  • Consumer端

Producer端如何保证消息不丢失

采取 send() 同步发消息,发送结果是同步感知的。
发送失败后可以重试,设置重试次数。默认 3 次。
producer.setRetryTimesWhenSendFailed(10);

集群部署,比如发送失败了的原因可能是当前Broker宕机了,重试的时候会发送到其他Broker上。

Broker端如何保证消息不丢失

修改刷盘策略为同步刷盘。默认情况下是异步刷盘的。
flushDiskType = SYNC_FLUSH

集群部署,主从模式,高可用。

Consumer端如何保证消息不丢失

完全消费正常后在进行手动 ack 确认。


消息堆积如何处理

首先要分析一下消息堆积可能造成的原因

1、如果是机器本身的原因,比如消费者组有几个消费者服务挂掉了,剩下少量消费者消费能力不足导致的消费积压,那就正常重新启动,然后慢慢再去消费积压的消息。

2、如果是生产者端由业务暴增引起的生产过快,而消费者端消费能力不足,这个时候就可以采取生产者端限流或者进行消费者扩容;这个时候要注意,如果生产者只是短期暴增或者消息的业务不是很重要可以采用限流,如果是长期暴增真正的业务量上涨就必须要进行消费者扩容。

3、如果是消费者挂了,然后 broker 堆积了很多消息,然后可以先把堆积的消息读到别的地方比如 mysql 或者 es 然后去后续进行处理,然后把 RocketMQ 堆积的消息删掉,启动消费者保障消费者正常消费,这里要注意的是删除堆积消息之前,需要停止 mq。


高吞吐量下如何优化生产者和消费者的性能

开发
同一 group 下,多机部署,并行消费
单个 Consumer 提高消费线程个数
批量消费
消息批量拉取
业务逻辑批量处理
运维
网卡调优
jvm 调优
多线程与 cpu 调优
Page Cache


RocketMQ是如何保证数据的高容错性的?

在不开启容错的情况下,轮询队列进行发送,如果失败了,重试的时候过滤失败的Broker
如果开启了容错策略,会通过RocketMQ的预测机制来预测一个Broker是否可用。
如果上次失败的Broker可用那么还是会选择该Broker的队列
如果上述情况失败,则随机选择一个进行发送
在发送消息的时候会记录一下调用的时间与是否报错,根据该时间去预测broker的可用时间。其实就是send消息的时候queue的选择。


broker 如何处理拉取请求

Consumer 首次请求 Broker:
判断 Broker 中是否有符合条件的消息

    • 响应Consumer
    • 等待下次 Consumer 的请求
  • 没有
    • PullRequestHoldService 来Hold连接,每个5s执行一次检查pullRequestTable有没有消息,有的话立即推送
    • 每隔1ms检查commitLog中是否有新消息,有的话写入到pullRequestTable
    • 当有新消息的时候返回请求
    • 挂起consumer的请求,即不断开连接,也不返回数据使用consumer的offset

RocketMQ 的存储机制

RocketMq采用文件系统进行消息的存储,相对于ActiveMq采用关系型数据库进行存储的方式就更直接,性能更高了

RocketMq与Kafka在写消息与发送消息上,继续沿用了Kafka的这两个方面:顺序写和零拷贝

1)顺序写
我们知道,操作系统每次从磁盘读写数据的时候,都需要找到数据在磁盘上的地址,再进行读写。而如果是机械硬盘,寻址需要的时间往往会比较长而一般来说,如果把数据存储在内存上面,少了寻址的过程,性能会好很多;
但Kafka 的数据存储在磁盘上面,依然性能很好,这是为什么呢?
这是因为,Kafka采用的是顺序写,直接追加数据到末尾。实际上,磁盘顺序写的性能极高,在磁盘个数一定,转数一定的情况下,基本和内存速度一致
因此,磁盘的顺序写这一机制,极大地保证了Kafka本身的性能

2)零拷贝
比如:读取文件,再用socket发送出去这一过程

buffer = File.read
Socket.send(buffer)

传统方式实现:
先读取、再发送,实际会经过以下四次复制
1、将磁盘文件,读取到操作系统内核缓冲区Read Buffer
2、将内核缓冲区的数据,复制到应用程序缓冲区Application Buffer
3、将应用程序缓冲区Application Buffer中的数据,复制到socket网络发送缓冲区
4、将Socket buffer的数据,复制到网卡,由网卡进行网络传输
在这里插入图片描述

传统方式,读取磁盘文件并进行网络发送,经过的四次数据copy是非常繁琐的
重新思考传统IO方式,会注意到在读取磁盘文件后,不需要做其他处理,直接用网络发送出去的这种场景下,第二次和第三次数据的复制过程,不仅没有任何帮助,反而带来了巨大的开销。那么这里使用了零拷贝,也就是说,直接由内核缓冲区Read Buffer将数据复制到网卡,省去第二步和第三步的复制。

那么采用零拷贝的方式发送消息,必定会大大减少读取的开销,使得 RocketMq 读取消息的性能有一个质的提升

此外,还需要再提一点,零拷贝技术采用了 MappedByteBuffer 内存映射技术,采用这种技术有一些限制,其中有一条就是传输的文件不能超过 2G,这也就是为什么 RocketMq 的存储消息的文件CommitLog 的大小规定为 1G 的原因

小结:RocketMq 采用文件系统存储消息,并采用顺序写写入消息,使用零拷贝发送消息,极大得保证了 RocketMq 的性能

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值