【RocketMQ】第四篇-RocketMQ的整体技术架构&常见问题

RocketMQ

  • 一、RocketMQ整体技术架构
  • 二、RocketMQ网络部署架构
    • NameServer采用HTTP服务器寻址
    • NameServer 的功能
    • 为何不用 ZooKeeper
    • RocketMQ底层通信机制
    • RocketMQ消息存储和发送
    • RocketMQ消息轨迹
      • 1、消息轨迹主要记录信息
      • 2、记录消息轨迹
      • 3、如何存储消息轨迹数据
  • 三、常见问题
    • 1、生产者消息重投(重新发送)
    • 2、消费者消息重试(接收消息重试)
    • 3、RocketMQ死信队列
  • 四、RocketMQ面试题
    • 1、RocketMQ 中的消息被消费后会立即删除吗?
    • 2、RocketMQ如何做负载均衡?
    • 3、消费负载均衡的consumer个数和queue个数不对等的时候会发生什么?
    • 4、是否会发生消息重复消费,如何解决?(幂等性问题)
    • 5、怎么保证消息发到同一个queue?
    • 6、RocketMQ如何保证消息不丢失
    • 7、高并发条件下如何优化生产者和消费者的性能?
    • 8、RocketMQ 是如何保证数据的高容错性的?
    • 9、Broker突然宕机了怎么办?
    • 10、Broker把自己的信息注册到哪个NameServer上?(问法是错的)
    • 11、RocketMQ对分布式事务支持的底层原理?
    • 12、线上是否遇到过消息积压的问题,
    • 13、有没有看过RocketMQ 的源码,如果看过,说说你对RocketMQ 源码的理解?

一、RocketMQ整体技术架构

【RocketMQ】第四篇-RocketMQ的整体技术架构&常见问题_第1张图片
1、NameServer是一个无状态节点,可以集群部署,每个节点之间无任何信息同步,每个节点都是独立的个体;

2、Broker部署相对复杂一些,Broker分为Master与Slave,一个Master可以对应多个Slave,但是一个Slave只能对应一个Master,Master与Slave 的对应关系通过指定相同的BrokerName,不同的BrokerId 来定义,BrokerId为0表示Master,非0表示Slave,Master也可以部署多个。每个Broker与NameServer集群中的所有节点建立长连接,定时注册Topic信息到所有NameServer,注意:一个Master多个Slave,但只有BrokerId=1的slave服务器才会参与消息的读负载;

3、Producer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic 服务的Master建立长连接,且定时向Master发送心跳,Producer完全无状态,可集群部署;

4、Consumer与NameServer集群中的其中一个节点(随机选择)建立长连接,定期从NameServer获取Topic路由信息,并向提供Topic服务的Master、Slave建立长连接,且定时向Master、Slave发送心跳,Consumer既可以从Master订阅消息,也可以从Slave订阅消息,消费者在向Master拉取消息时,Master服务器会根据拉取偏移量与最大偏移量的距离(判断是否读老消息,产生读I/O),以及从服务器是否可读等因素建议下一次是从Master还是Slave拉取消息;

二、RocketMQ网络部署架构

【RocketMQ】第四篇-RocketMQ的整体技术架构&常见问题_第2张图片

上面的网络部署架构图,描述其工作流程:
1、启动NameServer,NameServer起来后监听端口,等待Broker、Producer、Consumer连上来,相当于一个路由控制中心;
2、Broker启动,跟所有的NameServer保持长连接,定时发送心跳包,心跳包中包含当前Broker信息(IP+端口等)以及存储所有Topic信息,注册成功后,NameServer集群中就有Topic跟Broker的映射关系;
3、收发消息前,先创建Topic,创建Topic时需要指定该Topic要存储在哪些Broker上,也可以在发送消息时自动创建Topic;
4、Producer发送消息,启动时先跟NameServer集群中的其中一台建立长连接,并从NameServer中获取当前发送的Topic存在哪些Broker上,轮询从队列列表中选择一个队列,然后与队列所在的Broker建立长连接从而向Broker发消息;
5、Consumer跟Producer类似,跟其中一台NameServer建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息;

NameServer采用HTTP服务器寻址

客户端启动后,会定时访问一个静态HTTP服务器,地址如下:
http://jmenv.tbsite.net:8080/rocketmq/nsaddr,这个URL的返回内容如下:
192.168.0.11:9876;192.168.0.12:9876;192.168.0.13:9876
客户端默认每隔2分钟访问一次这个HTTP服务器,并更新本地的Name Server地址,URL已经在代码中硬编码,可通过修改/etc/hosts文件来改变要访问的服务器,例如在/etc/hosts增加如下配置:
127.0.0.1 jmenv.tbsite.net
推荐使用HTTP静态服务器寻址方式,好处是客户端部署简单,且Name Server集群可以热升级;

NameServer 的功能

NameServer 是整个消息队列中的路由服务器,集群的各个组件通过它来了
解全局的信息(相当于是注册中心、路由中心),同时,各个角色的机器都要定期向NameServer上报自己的状态,超时不上报NameServer认为某个机器出故障不可用了,会把该机器从可用列表里移除;
NameServer可以部署多个,相互之间独立,其他角色同时向多个NameServer
机器上报状态信息,从而达到热备份的目的, NameServer本身是无状态的,也就是说 NameServer 中的 Broker Topic 等状态信息不会持久存储,都是由各个角色定时上报并存储到内存中的;
NameServer每10 秒检查一次,时间戳超过2分钟则认为Broker失效,就会将该Broker从NameServer中删除;

为何不用 ZooKeeper

ZooKeeper是Apache 下的一个开源软件,为分布式应用程序提供协调服务,
RocketMQ为什么不采用Zookeeper,而要自己造轮子?
原因是ZooKeeper 的功能很强大,包括自动 Master 选举等, RocketMQ 的架构设计决定了它不需要进行 Master 选举,用不到这些复杂的功能,只需要 一个轻量级的元数据服务器就足够了,中间件对稳定性要求较高,RocketMQ NameServer 只有很少的代码,容易维护,所以不需要再依赖另一个中间件,从而减少整体维护成本;

RocketMQ底层通信机制

分布式系统各个角色间的通信效率很关键,通信效率的高低直接影响系统性能,基于Socket实现一个高效的TCP通信协议是很有挑战的,所以RocketMQ底层全部采用Netty进行通信,包括消息传递,信息注册;

RocketMQ消息存储和发送

分布式队列因为有高可靠性的要求,所以数据要通过磁盘进行持久化存储,用磁盘存储消息速度会不会很慢?能满足实时性和高吞吐量的要求吗?

实际上,磁盘速度快慢关键在于如何使用,使用得当,磁盘的速度完全可以匹配上网络的数据传输速度,目前的高性能磁盘,顺序写速度可以达到 600MB/s,超过了一般网卡的传输速度,但是磁盘随机写的速度只有大概 lOOKB/s,一个性能优良的消息队列系统必定会采用一些技术手段提高的磁盘操作效率;

Linux操作系统分为“用户态”和“内核态”,文件操作、网络操作需要涉及这两种形态的切换,避免不了进行数据复制,一台服务器把本机磁盘文件的内容发
送到客户端一般分为两个步骤:

  • 1 ) read (file, tmp_buf, len);读取本地文件内容;
    1. write (socket, tmp_buf, len);将读取的内容通过网络发送出去;

tmp_buf是预先申请的内存,这两个看似简单的操作,实际进行了4次数
据复制,分别是:

  • 从磁盘复制数据到内核态内存,
  • 从内核态内存复制到用户态内存(完成了read(file, tmp_b len));
  • 然后从用户态内存复制到网络驱动的内核态内存,
  • 最后是从网络驱动的内核态内存复制到网卡中进行传输(完成了
    write(socket, tmp_buf, len))

通过使用mmap方式,可以省去向用户态的内存复制,提高速度,这种机制在 Java中通过MappedByteBuffer实现

RocketMQ充分利用了上述特性,也就是所谓的“零拷贝”技术,提高消息存
盘和网络发送的速度;(包括kafka、netty等技术都是零拷贝技术)

RocketMQ消息轨迹

RocketMQ消息轨迹是指一条消息从生产者发送到消息队列RocketMQ的服务端,再到消费者消费处理,整个过程中的各个相关节点的时间、状态等数据汇聚而成的完整链路信息,该轨迹可作为生产环境中排查问题强有力的数据支持;
开启该功能,只需要创建生产者或者消费者的时候,指定对应参数为true即可,

DefaultMQProducer producer = new DefaultMQProducer("producer-group", true, "my_trace");

DefaultMQPushConsumer consumer = new 
DefaultMQPushConsumer("consumer-group", true, "my_consumer_trace");
不指定topic名称的话,默认是系统的topic:RMQ_SYS_TRACE_TOPIC

跟踪消息的内容举例:

Pub1595339451951DefaultRegionproducer-groupMyTopicC0A8006820AC18B4AAC26BAF522E0000TagAnull192.168.172.132:10911147840C0A8AC8400002A9F00000000000046D0true 

1、消息轨迹主要记录信息

  • traceType
    跟踪类型,可选值:Pub(消息发送)、SubBefore(消息拉取到客户端,执行业务定义的消费逻辑之前)、SubAfter(消费后);
  • timeStamp
    当前时间戳;
  • regionId
    broker所在的区域ID,取自BrokerConfig#regionId;
  • groupName
    组名称,traceType为Pub时为生产者组的名称;如果traceType为subBefore或subAfter时为消费组名称;
  • requestId
    traceType为subBefore、subAfter时使用,消费端的请求Id;
  • topic
    消息主题;
  • msgId
    消息唯一ID;
  • tags
    消息tag;
  • keys
    消息索引key,根据该key可快速检索消息;
  • storeHost
    跟踪类型为PUB时为存储该消息的Broker服务器IP;跟踪类型为subBefore、subAfter时为消费者IP;
  • bodyLength
    消息体的长度;
  • costTime
    耗时;
  • msgType
    消息的类型,可选值:Normal_Msg(普通消息),Trans_Msg_Half(预提交消息),Trans_msg_Commit(提交消息),Delay_Msg(延迟消息);
  • offsetMsgId
    消息偏移量ID,该ID中包含了broker的ip以及偏移量;
  • success
    是发送成功;
  • contextCode
    消费状态码,可选值:
    SUCCESS,TIME_OUT,EXCEPTION,RETURNNULL,FAILED;

2、记录消息轨迹

消息中间件的两大核心主题:消息发送、消息消费,其核心载体是消息,消息轨迹(消息的流转)主要记录消息是何时发送到哪台Broker,发送耗时多少时间,在什么是被哪个消费者消费,记录消息的轨迹主要是集中在消息发送前后、消息消费前后,通过RokcetMQ的Hook机制实现,主要是如下两个接口所定义的钩子函数;

通过实现上述两个钩子函数接口,可以实现在消息发送、消息消费前后记录消息轨迹;

3、如何存储消息轨迹数据

RocketMQ提供了两种方法来定义消息轨迹的Topic;

  • 系统默认Topic
    首先要将Broker的traceTopicEnable配置设置为true,表示在该Broker上创建topic名为:RMQ_SYS_TRACE_TOPIC,队列个数为1的消息轨迹跟踪功能,如果traceTopicEnable配置设置为false,表示该Broker不开启存储消息轨迹功能;

  • 自定义Topic
    在创建消息生产者或消息消费者时,可以通过参数自定义用于记录消息轨迹的Topic名称,不过建议使用系统默认的Topic即可;

三、常见问题

1、生产者消息重投(重新发送)

生产者在发送消息时,同步消息失败会重投,异步消息有重试,oneway没有任何保证,消息重投保证消息尽可能发送成功、不丢失,但可能会造成消息重复,消息重复在RocketMQ中是无法避免的问题,消息重复在一般情况下不会发生,当出现消息量大、网络抖动,消息重复就会是大概率事件。另外,生产者主动重发、consumer负载变化也会导致重复消息。
如下方法可以设置消息重试策略:
(1)retryTimesWhenSendFailed: 同步发送失败重投次数,默认为2,因此生产者会最多尝试发送retryTimesWhenSendFailed + 1次,重投时不会选择上次失败的broker,会尝试向其它broker发送,最大程度保证消息不丢,超过重投次数,抛出异常,由客户端保证消息不丢,当出现RemotingException、MQClientException和部分MQBrokerException时会重投;

(2)retryTimesWhenSendAsyncFailed: 异步发送失败重试次数,异步重试不会选择其他broker,仅在同一个broker上做重试,不保证消息不丢;

(3)retryAnotherBrokerWhenNotStoreOK: 消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false,十分重要消息可以开启;
发送消息返回状态总共有4种结果:
public enum SendStatus {
SEND_OK,
FLUSH_DISK_TIMEOUT,
FLUSH_SLAVE_TIMEOUT,
SLAVE_NOT_AVAILABLE,
}

发送超时时间设置
producer.setSendMsgTimeout(6000); //默认是3000

2、消费者消息重试(接收消息重试)

如果Consumer端因为各种异常导致本次消费失败,为防止该消息丢失而需要将其重新发回给Broker端保存,保存这种因为异常无法正常消费而发回给Broker的消息队列称之为重试队列;
RocketMQ会为每个消费组都设置一个Topic名称为“%RETRY%+consumerGroup”的重试队列(这里需要注意的是,这个Topic的重试队列是针对消费组,而不是针对每个Topic设置的),用于暂时保存因为各种异常而导致Consumer端无法消费的消息。同时考虑到异常恢复需要一些时间,Broker会为重试队列设置多个重试级别,每个重试级别都有与之对应的重新投递延时,重试次数越多投递延时就越大;
RocketMQ对于重试消息的处理是先保存至Topic名称为“SCHEDULE_TOPIC_XXXX”的延迟队列中,后台定时按照对应的时间进行延迟后重新保存至“%RETRY%+consumerGroup”的重试队列中;

3、RocketMQ死信队列

死信队列用于处理无法被正常消费的消息,当一条消息初次消费失败,消息队列会自动进行消息重试(上面说的这个重试),当达到最大重试次数后(默认是16次),若消费依然失败,则表明消费者无法正确地消费该消息,

此时,消息队列不会立刻将该消息丢弃,而是将其发送到该消费者对应的特殊队列中

RocketMQ将这种无法被消费的消息称为死信消息(Dead-Letter Message),

将存储死信消息的特殊队列称为死信队列(Dead-Letter Queue),

在RocketMQ中,可以通过使用console控制台对死信队列中的消息进行重发来使得消费者实例再次进行消费;

一般应用在正常业务处理出现异常时,将消息拒绝则会进入到死信队列中,有助于统计异常数据并做后续处理;

死信队列中的消息再次发送,需要修改死信队列读写权限为6,死信队列中的消息需要通过重新订阅该死信队列的topic进行消费;

6:同时支持读写,
4:读,
2:写 (和Linux一样,4读 2写 1执行)

四、RocketMQ面试题

1、RocketMQ 中的消息被消费后会立即删除吗?

不会,每条消息都会持久化到CommitLog中,每个Consumer连接到Broker后会维持消费进度信息,当有消息消费后只是当前Consumer的消费进度(CommitLog的offset)更新了;
默认48小时后会删除不再使用的CommitLog文件;
检查这个文件最后访问时间,判断是否大于过期时间,然后在指定时间删除,默认凌晨4点进行删除;

2、RocketMQ如何做负载均衡?

producer端(发送端)
发送端指定message queue发送消息,来达到写入时的负载均衡,提升写入吞吐量,当多个producer同时向一个topic写入数据的时候,消息会被写入到多个message queue中,默认策略是随机选择: (实现了消息的分片,一部分现在在queue1,一部分现在在queue2,…)
具体实现:
producer维护一个index
每次取节点会自增
index向所有broker个数取余
自带容错策略
其他实现:

SelectMessageQueueByHash
SelectMessageQueueByRandom
SelectMessageQueueByMachineRoom
也可以自定义实现MessageQueueSelector接口中的select方法;

consumer端
采用的是平均分配算法来进行负载均衡;

  • 平均分配策略(默认)(AllocateMessageQueueAveragely)
    其他负载均衡算法
  • 环形分配策略(AllocateMessageQueueAveragelyByCircle)
  • 手动配置分配策略(AllocateMessageQueueByConfig)
  • 机房分配策略(AllocateMessageQueueByMachineRoom)
  • 一致性哈希分配策略(AllocateMessageQueueConsistentHash)
  • 靠近机房策略(AllocateMachineRoomNearby)

3、消费负载均衡的consumer个数和queue个数不对等的时候会发生什么?

queue个数默认是4个,但此时消费者个数是2个或者是10;
Consumer和queue会优先平均分配,如果Consumer少于queue的个数,则会存在部分Consumer消费多个queue的情况,如果Consumer等于queue的个数,那就是一个Consumer消费一个queue,如果Consumer个数大于queue的个数,那么会有部分Consumer不参与消费而浪费了;

4、是否会发生消息重复消费,如何解决?(幂等性问题)

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

ACK:RECONSUME_LATER

正常情况下在consumer真正消费完消息后应该发送ack,通知broker该消息已正常消费,从queue中剔除,当ack因为网络原因无法发送到broker,broker会认为此条消息没有被消费,此后会开启消息重投机制把消息再次投递到consumer;
消费模式在CLUSTERING模式下,消息在broker中会保证相同group的consumer消费一次,但是针对不同group的consumer会推送多次;

解决方案

  • 1、采用数据库表,处理消息前,使用消息主键在表中带有唯一约束的字段中insert;
  • 2、单机环境时可以使用map ConcurrentHashMap -> putIfAbsent或者本地guava cache,ehcache环境实现;
  • 3、分布式环境用Redis、zookeeper分布式锁;

5、怎么保证消息发到同一个queue?

我们之前介绍过顺序消费,就是发到同一个队列;
Rocket MQ给我们提供了MessageQueueSelector接口,可以自己重写里面的接口,实现自己的算法,举个最简单的例子:判断i % 2 == 0,那就都放到queue1里,否则放到queue2里;

6、RocketMQ如何保证消息不丢失

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

  • (1)Producer端如何保证消息不丢失
    采取send()同步发消息,发送结果是同步知道的;
    发送失败后可以重试,设置重试次数,默认3次;
    producer.setRetryTimesWhenSendFailed(10);
    集群部署保证高可用,比如发送失败了的原因可能是当前Broker宕机了,重试的时候会发送到其他Broker上;
  • (2)Broker端如何保证消息不丢失
    修改刷盘策略为同步刷盘,默认情况下是异步刷盘的;
    flushDiskType = SYNC_FLUSH
    集群部署,主从模式,保证高可用;
  • (3)Consumer端如何保证消息不丢失
    完全消费正常后在进行手动ack确认;
    即返回ConsumeConcurrentlyStatus.CONSUME_SUCCESS

7、高并发条件下如何优化生产者和消费者的性能?

开发角度
同一group下,多机部署,并行消费;
单个Consumer提高消费线程个数;consumer.setConsumeThreadMax(25);
消息批量拉取:consumer.setConsumeMessageBatchMaxSize(10);
运维角度
网卡调优
jvm调优
多线程与cpu调优
Page Cache (属于linux内核的优化)

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

在不开启容错的情况下,轮询队列进行发送,如果失败了,重试的时候排除掉失败的Broker,选择其他Broker进行发送;
如果开启了容错策略,会通过RocketMQ的预测机制来预测一个Broker是否可用;
如果上次失败的Broker可用那么还是会选择该Broker;
如果上述情况失败,则随机选择一个进行发送;
在发送消息的时候会记录一下调用的时间与是否报错,根据该时间去预测broker的可用时间;
具体源码实现参考代码:

org.apache.rocketmq.client.latency.MQFaultStrategy#selectOneMessageQueue()

9、Broker突然宕机了怎么办?

Broker需要集群高可用部署,Master收到消息后会同步给Slave,Master宕机了还有slave中的消息可用,保证了MQ的可靠性和高可用性,Rocket MQ4.5.0开始支持了Dlegder模式,基于raft算法,可以实现故障转移,做到了真正意义的HA;

10、Broker把自己的信息注册到哪个NameServer上?(问法是错的)

这个问题是一个坑,因为Broker会向所有的NameServer上注册自己的信息,而不是某一个;

11、RocketMQ对分布式事务支持的底层原理?

分布式系统中的事务可以使用TCC(Try、Confirm、Cancel)、2PC来解决分布式系统中的消息原子性;
RocketMQ 4.3+提供分布事务功能,通过 RocketMQ 事务消息能达到分布式事务的最终一致
RocketMQ实现方式:

Half Message:预处理消息,当broker收到此类消息后,会存储到RMQ_SYS_TRANS_HALF_TOPIC的消息消费队列中;

检查事务状态:Broker会开启一个定时任务,消费RMQ_SYS_TRANS_HALF_TOPIC队列中的消息,每次执行任务会向消息发送者确认事务执行状态(提交、回滚、未知),如果是未知,Broker会定时去回调在重新检查

超时:如果超过回查次数,默认回滚消息;
也就是他并未真正进入Topic的queue而是用了临时queue来放所谓的half message
,等提交事务后才会真正的将half message转移到topic下的queue;

12、线上是否遇到过消息积压的问题,

下游消费系统宕机了,导致几百万条消息在消息中间件里积压,如何处理?
首先要找到导致消息堆积的原因,是Producer太多了还是Consumer太少了导致的;
并且检查消息消费速度是否正常,正常的话,可以通过上线更多consumer临时解决消息堆积问题;
如果Consumer和Queue个数不对等,上线了多台Consumer在短时间内也无法消费完堆积的消息;
准备一个临时的topic(里面定义queue的个数非常大);
queue的数量是堆积的几倍;

上线一台Consumer,把原来Topic中的消息通过Consumer消费并投递到新的Topic中,不做业务逻辑处理,只是把消息消费并投递过去;

上线N台Consumer同时消费临时Topic中的数据;

至此完成了堆积消息的消费,后续再修改程序bug,恢复原来的Consumer,继续消费之前的Topic;
追问:堆积时间过长消息消失了?
RocketMQ中的消息只会在commitLog被删除的时候才会消失,也就是说未被消费的消息不会存在超时删除这情况;

追问:堆积的消息会不会进死信队列?

不会,消息在消费失败后才会进入重试队列(%RETRY%+ConsumerGroup),16次(默认16次)失败才会进入死信队列(%DLQ%+ConsumerGroup);

13、有没有看过RocketMQ 的源码,如果看过,说说你对RocketMQ 源码的理解?

看过一点,源码没有注释,写得不是很友好,里面的通信主要是采用Netty;

你可能感兴趣的:(04_分布式专题,java-rocketmq,rocketmq,学习)