RocketMQ采用发布订阅模式,基本参与组件主要包括:消息发送者、消息服务器(消息存储)、消息消费、路由发现。
功能:
(1) RocketMQ可以严格保证消息有序
(2)RocketMQ支持消息过滤(消息消费时可以对同一主题下的消息按照规则只消费自己感兴趣的消息)。
(3)RocketMQ引入内存映射机制,所有主题的消息顺序存储在同一个文件中。有消息文件过期机制和文件存储空间报警机制。
(4)RocketMQ在不发生消息堆积时,以长轮询模式实现准实时的消息推送模式。
(5)确保消息必须被消费一次,有重复消费的可能。
(6)支持回溯消息(已消费的消息可以按时间回溯)
(7)不支持任意进度的定时消息,支持特定延迟级别。
RocketMQ摒弃了业界常用的使用zookeeper充当信息管理的“注册中心”,而是自研NameServer来实现元数据的管理(Topic路由信息等)。
Broker 消息服务器在启动的时候向所有NameServer注册,消息生产者在发送消息之前先从NameServer获取Broker服务器地址列表,根据负载算法从列表中选择一台消息服务器进行消息发送。
NameServer的启动类是NamesrvStartup, 通过执行这个类的main方法可以启动一个NameServer
package org.apache.rocketmq.namesrv;
public class NamesrvStartup {
public static void main(String[] args) {main0(args);}
public static NamesrvController main0(String[] args) {
...
NamesrvController controller = createNamesrvController(args);
start(controller); // start里面调用了initialize()和start()
...
}
}
package org.apache.rocketmq.namesrv;
public class NamesrvController {
public boolean initialize() {
// 加载配置
this.kvConfigManager.load();
// 初始化netty server 连接就靠它
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
this.remotingExecutor = Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
this.registerProcessor();
// 定时任务,每隔10s扫一次 brokerLiveTable
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
...
}
}
路由元数据都存到RouteInfoManager。
public class RouteInfoManager {
private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.NAMESRV_LOGGER_NAME);
private final static long BROKER_CHANNEL_EXPIRED_TIME = 1000 * 60 * 2;
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final HashMap<String/* topic */, List<QueueData>> topicQueueTable;
private final HashMap<String/* brokerName */, BrokerData> brokerAddrTable;
private final HashMap<String/* clusterName */, Set<String/* brokerName */>> clusterAddrTable;
private final HashMap<String/* brokerAddr */, BrokerLiveInfo> brokerLiveTable;
private final HashMap<String/* brokerAddr */, List<String>/* Filter Server */> filterServerTable;
}
RocketMQ路由注册通过Broker与NameServer的心跳功能实现。Bocker启动时向集群所有的NameServer发送心跳语句,每隔30s向集群中所有NameServer发送心跳包,NameServer收到Bocker心跳包会更新brokerLiveTable缓存中的BrokerLiveInfo的lastUpdateTimestamp, 然后NameServer每隔10s扫描brokerLiveTable, 如果连续120s没有收到心跳包,NameServer将移除Broker的路由信息同时关闭Socket连接。
发心跳包的代码在: org.apache.rocketmq.broker.BrokerController#start 一样是启动一个定时任务,每隔30s, 遍历nameServer列表逐个发消息。(网络都是基于netty)
NameServer的DefaultRequestProcessor收到网络请求后,如果是broker的心跳,会调用RouteInfoManager#registerBroker。 代码也比较简单,先加锁,更新元数据信息(上面的RouteInfoManager里面的一些hashmap)。
RocketMQ有两个触发点来触发路由删除
RocketMQ路由发现是非实时的,当Topic路由出现变化后,NameServer不主动推送给客户端,而是由客户端定时拉取主题最新的路由。
org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic
RocketMQ 支持3种消息发送方式:同步(sync)、异步(async)、单向(oneway)。
可以看到,RocketMQ的消息包含了主题topic、flag、properties扩展属性、body消息体
public class Message implements Serializable {
private static final long serialVersionUID = 8445773977080406428L;
private String topic;
private int flag;
private Map<String, String> properties;
private byte[] body;
private String transactionId;
}
properties里面包含:
消息发送流程主要步骤:验证消息、查找路由、消息发送(包含异常处理机制)
1)验证消息
消息发送之前,首先确保生产者处于运行状态,然后主题名称、消息体不能为空、消息长度不能等于0且默认不能超过4M
2)查找路由
如果生产者中缓存了topic的路由信息,且该路由信息中包含了消息队列,直接返回该路由信息。反之向NameServer查询该topic的路由信息。最终没有找到,会抛异常。
org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#tryToFindTopicPublishInfo
3)消息发送
消息发送的核心方法:org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendKernelImpl
private SendResult sendKernelImpl(final Message msg, // 待发送的消息
final MessageQueue mq, // 消息发送到该队列上
final CommunicationMode communicationMode, // 消息发送模式 SYNC、ASYNC、ONEWAY
final SendCallback sendCallback, // 异步消息回调函数
final TopicPublishInfo topicPublishInfo, // 主题路由信息
final long timeout) // 超时时间
消息批量发送的流程和上述单条消息流程一致。对于批量发送消息,要解决的消息的编码和解码问题。批量的消息也是存于body中。RocketMQ会对单条消息使用固定格式存储。方便服务端能正确解析出消息。
针对异常,RocketMQ有重试与Broker规避机制。Broker规避就是在一次消息发送过程中发现错误,在某一时间段内,消息生产者不会选择该Broker(消息服务器)上的消息队列,提高发送消息的成功率。
主要存储的文件包括:Commitlog文件、ConsumeQueue文件、IndexFile文件.
Rocket所有消息顺序的写在Comitlog中,确保高性能。但由于消息中间件一般是基于消息主题的订阅机制,顺序写机制给基于主题的检索带来不便。RocketMQ引入了ConsumeQueue消息队列文件,每个主题包含多个消息队列,每个队列是一个文件。IndexFile索引文件主要是为了加速消息的检索性能,根据消息的属性快速从Commitlog文件中检索消息。
消息存储入口:
org.apache.rocketmq.store.DefaultMessageStore#putMessage
调用
org.apache.rocketmq.store.CommitLog#putMessage
RocketMQ的存储是基于JDK NIO的内存映射机制(MapperByteBuffer)的,消息存储时首先将消息追加到内存,再根据配置的刷盘策略在不同时间进行刷写磁盘。默认是异步刷盘,RocketMQ使用一个单独的线程按照某个设定的频率执行刷盘操作。(索引文件每次更新就会将上一次改动刷写到磁盘)
由于RocketMQ CommitLog、ConsumeQueue文件是基于内存的映射机制并在启动的时候会加载commitLog、consumequeue目录下的所有文件,为了避免内存与磁盘的浪费,所以需要引入一种机制来删除已过期的文件。
RocketMQ顺序写文件,所有的写操作都会落到最后一个CommitLog、ConsumeQueue文件上,之前的文件的文件在下一个文件创建后不会被更新。
如果非当前写文件在一定时间间隔内没有再次被更新,RocketMQ会认为是过期文件,可以被删除。默认每个文件的删除时间是72小时。(RocketMQ不会关注这个文件上的消息是否全部被消费)。
消息消费以组的模式展开,一个消费组内可以包含多个消费者,每个消费组可订阅多个主题,消费组直接有集群模式和广播模式两种消费模式。集群模式,主题下的同一条消息只允许被其中的一个消费者消费。广播模式,主题下的同一条消息将被集群内所有消费者消费一次。
消息服务器与消费者之间的消息传送方式也有两种方式:推模式(消息由消息服务器推送给消息消费者)、拉模式(消费端主动发起拉消息请求)。RocketMQ消息推模式的实现基于拉模式,在拉模式上包装一层,一个拉取任务完成后开始下一个拉取任务。
集群模式,消息队列负载机制遵循一个通用的思想:一个消息队列同一时间只允许被一个消费者消费,一个消费者可以消费多个消息队列。
RocketMQ支持同一消息队列上的消息顺序消费,不支持消息全局顺序消费。如果要实现某一主题的全局顺序消息消费,可以将该主题的队列数设置为1,牺牲高可用性。(这里为什么是牺牲高可用,不同队列的消息也是不一样的吧?)
RocketMQ支持两种消息过滤模式:表达式(TAG、SQL92)与类过滤模式。
RocketMQ使用一个单独的线程PullMessageService来负责消息的拉取。
package org.apache.rocketmq.client.impl.consumer;
public class PullMessageService extends ServiceThread { // implements Runnable
@Override
public void run() {
while (!this.isStopped()) {
PullRequest pullRequest = this.pullRequestQueue.take();
this.pullMessage(pullRequest);
}
}
......
}
可以看到消息拉取的请求参数应该在PullRequest , 以下是PullRequest核心属性
package org.apache.rocketmq.client.impl.consumer;
public class PullRequest {
private String consumerGroup; // 消费者组
private MessageQueue messageQueue; // 待拉取消费队列
private ProcessQueue processQueue; // 消息处理队列, 从Broker拉取到的消息先存入ProcessQueue,然后再提交到消费者消费线程池消费
private long nextOffset; // 待拉取的MessageQueue偏移量
private boolean lockedFirst = false; // 是否被锁定
}
ProcessQueue是MessageQueue在消费端的重现、快照。PullMessageService从消费服务器默认每次拉取32条消息,按照消息的队列偏移量顺序存放在ProcessQueue中,PullMessageService然后将消息提交到消费者消费线程池,消息成功消费后从ProcessQueue中移除。
消息拉取分为3个主要步骤:
1)消息拉取客户端消息拉取请求封装。
2)消息服务器查找并返回消息。
3)消息拉取客户端处理返回的消息。
上述步骤完成后,RocketMQ通过MQClientAPIImpl#pullMessageAsync方法异步向Broker拉取消息
服务端处理消息拉取到入口:org.apache.rocketmq.broker.processor.PullMessageProcessor
消息拉取客户端调用入口:MQClientAPIImpl#pullMessageAsync,NettyRemotingClient在收到服务器响应结构后会回调PullCallback的onSuccess或者onException, PullCallBack对象在pullMessage方法中创建。
public class PullResult {
private final PullStatus pullStatus; // 拉取结果
private final long nextBeginOffset; // 下次拉取偏移量
private final long minOffset; // 消息队列最小偏移量
private final long maxOffset; // 消息队列最大偏移量
private List<MessageExt> msgFoundList; // 具体拉取的消息列表
}
RocketMQ 并没有真正实现推模式,而是消费者主动向消息服务器拉取消息,RocketMQ推模式是循环想消息服务器发送消息拉取请求,如果消息消费者向RocketMQ发送消息拉取时,消息并未到达消费队列,如果不启用长轮询机制,则会在服务端等待shortPollingTimeMills时间后(挂起)再去判断消息是否已到达消息队列,如果消息未到达则提示消息拉取客户端PULL_NOT_FOUNT;如果开启长轮询模式,RocketMQ一方面会每5s轮询检查一次消息是否可达,同时一有消息到达后立马通知挂起线程再次验证新消息是否是自己感兴趣的消息,如果是则从commitlog文件提取消息返回给消息拉取客户端,否则直到挂起超时。超时时间由消息拉取方在消息拉取时封装在请求参数中,PUSH模式默认为15s,PULL模式通过DefaultMQPullConsumer#setBrokerSuspendMaxTimeMillis设置。RocketMQ通过在Broker端配置longPollingEnable为true来开启长轮询模式。
RocketMQ轮询机制由两个线程共同来完成。
1)PullRequestHoldService:每隔5s重试一次
2)DefaultMessageStore#ReputMessageService,每处理一次重新拉取,Thread.sleep(1),继续下一次检查。
一个topic可以有多个消费队列是,RocketMQ默认提供5种分配算法。
AllocateMessageQueueAveragely:平均分配
如果有5个消费队列(q1, q2, q3, q4, q5),2个消费者(c1, c2)。消息队列分配如下:
c1:q1, q2, q3
c2:q4, q5
AllocateMessageQueueAveragelyByCircle:平均轮询分配,如上例子,分配如下:
c1:q1, q3, q5
c2: q2, q4
AllocateMessageQueueConsistentHash:一致性hash
AllocateMessageQueueByConfig:根据配置,为每一个消费者配置固定的消息队列。
AllocateMessageQueueByMachineRoom:根据Broker部署机房名,对每个消费者负责不同的Broker上的队列。
下图是PullMessageService线程与RebalanceService线程交互图
RocketMQ使用ConsumeMessageService来实现消息消费的处理逻辑,支持顺序消费与并发消费。
前面说到从服务器拉取消息后会回调PullCallBack回调方法,将消息放入到ProcessQueue中,然后把消息提交到消费线程池中执行,也就是调用ConsumeMessageService#submitConsumeRequest开始进入到消息消费的过程。
定时消息是指消息发送到Broker后,并不立即被消费者消费而是要等到特定的时间后才能被消费。
RocketMQ不支持任意时间精度,消息延迟级别在Broker端通过messageDelayLevel配置,默认为“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”, delayLevel = 1 表示延迟1s, delayLevel = 2 表示延迟5s, 以此类推。
消息重试是通过定时实现的,定时消息实现类为org.apache.rocketmq.store.schedule.ScheduleMessageService。该类的实例在DefaultMessageStre中创建,通过在DefaultMessageStore中调用laod方法加载并调用start方法进行启动。
定时消息的第一个设计关键点是,定时消息单独一个主题SCHEDULE_TOPIC_XXXX, 该主题下队列数量等于配置的延迟级别数量。对应关系是queueId等于延迟级别减1.ScheduleMessageService为每隔延迟级别创建一个定时Timer根据延迟级别对应的延迟时间进行延迟调度。在消息发送时,如果消息的延迟级别delayLevel大于0,将消息的原主题名称、队列ID存入消息的属性中,然后改变消息的主题、队列与延迟主题与延迟主题所属队列,消息将最终发到延迟队列的消费队列。
定时消息第二个设计关键点:消息存储时如果消息的延迟级别属性delayLevel大于0,则会备份原主题、原队列到消息属性中,通过为不同的延迟级别创建不同的调度任务,当时间到达后执行调度任务,调度任务主要就是根据延迟拉取消息消费进度从延迟队列中拉取消息,然后从commitlog中加载完整消息,清除延迟级别属性并恢复原先的主题、队列,再次创建一条新的消息存入到commitlog中并转发到消息队列供消息消费者消费。
为了避免Broker发生单点故障引起存储在Broker上的消息无法及时消费,RocketMQ引入Broker主备机制。消息到达主服务器后需要将消息同步到消息从服务器,如果从服务器Broker宕机, 消息消费者可以从从服务器拉取消息。
实现:
RocketMQ事务消息的实现原理基于两阶段提交和定时事务状态回查来决定消息最终是提交还是回滚。
本文为读书笔记,内容大多来自《RocketMQ技术内幕 RocketMQ架构设计与实现原理》
[1] 丁威 周继锋 RocketMQ技术内幕 RocketMQ架构设计与实现原理 机械工业出版社