RocketMQ学习笔记-入门、注册与消息发送
一、概述
RocketMQ是由阿里开源的一款高性能、高吞吐量的消息中间件。
RocketMQ设计基于主题的发布与订阅模式,其核心功能包括消息发送(producer)、消息存储(Broker)和消息消费(consumer)。
1.1 组成部分
RocketMQ在架构上主要分为四部分:
1.1.1 NameServer
作为Topic路由信息的注册中心,类似于dubbo的ZooKeeper角色。NameServer应保存Broker相关元信息并给Producer和Consumer查找Broker信息。NameServer被设计成无状态的,可以横向拓展,节点之间没有通信。NameServer会维持一个路由注册表,每个Broker在启动时都会到NameServer注册,Producer在发送消息前会根据Topic到NameServer获取到Broker的路由信息,Consumer也会定时获取Topic的路由信息。
1.1.2 Broker
消息服务器是消息存储中心,主要作用是接受来自Producer的消息并存储,Consumer从这里获取,它还存储与小型相关的元数据,包括用户组、消费进度偏移量、队列信息等。从上面的部署图可以看出,多台Broker组成Master-Slave架构集群,brokerId为0代表Master,大于0代表Slave。其中,Master既可以写也可以读,Slave不可以写只可以读。
Broker消息服务器在启动时向所有NameServer注册,注册中心也与每台Broker保持长连接,并间隔10s检测Broker是否存活,如果发现宕机,则从路由注册表中移除。
1.1.3 Producer
消息发布的角色,支持分布式集群方式部署。生产者向消息服务器发送由业务应用程序系统产生的消息,RocketMQ提供了三种消息发送方法:同步、异步和单向。
- 同步:发送者向MQ执行发送消息API时,同步等待,直到消息服务器返回发送结果。
- 异步:发送者向MQ执行发送消息API时,指定消息发送成功后的回调函数,然后调用消息发送API后,立即返回不等待,消息发送者线程不阻塞,直到运行结束。消息发送成功或失败事件的回调任务在一个新的线程内执行。
- 单向:发送者向MQ执行发送消息API时,不等待直接返回,也不注册回调任务。简单来说,就是只管发,不在乎消息是否成功存储在消息服务器上。
1.1.4 Comsumer
消息消费的角色,支持分布式集群方式部署。从用户角度来说,获取消息有pull/push两种方式,分别看作拉取型消费者和推送型消费者。
二、开发实践
2.1 开发环境搭建
1、下载RocketMQ开发工具包,链接。或者
wget https://downloads.apache.org/rocketmq/4.7.1/rocketmq-all-4.7.1-bin-release.zip
unzip rocketmq-all-4.7.1-bin-release.zip
cd rocketmq-all-4.7.1-bin-release
2、配置Broker公网ip
vim conf/broker.conf
# 增加配置
brokerIP1:服务器公网IP
3、修改namesrv和broker启动参数
vim bin/runserver.sh
vim bin/runbroker.sh
vim bin/tools.sh
# 修改JAVA路径为本地路径
[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home
#[ ! -e "$JAVA_HOME/bin/java" ] && JAVA_HOME=/usr/java
#[ ! -e "$JAVA_HOME/bin/java" ] && error_exit "Please set the JAVA_HOME variable in your environment, We need java(x64)!"
# JVM具体参数
JAVA_OPT="${JAVA_OPT} -server -Xms4g -Xmx4g -Xmn2g -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
其中,查找JDK路径
rocketmq-all-4.7.1-bin-release /usr/libexec/java_home -v
java_home: option requires an argument -- v
/Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home
4、启动服务
# 启动namesrv
nohup sh bin/mqnamesrv &
# 查看日志
tail -f ~/logs/rocketmqlogs/namesrv.log
INFO main - The Name Server boot success. serializeType=JSON
# 启动broker
nohup sh bin/mqbroker -n localhost:9876 autoCreateTopicEnable=true -c conf/broker.conf &
# 查看日志
tail -f ~/logs/rocketmqlogs/broker.log
INFO main - The broker[broker-a, 30.38.52.65:10911] boot success. serializeType=JSON and name server is localhost:9876
注意的是,线下环境中autoCreateTopicEnable建议为true,线上环境为false。
5、关闭服务
# 关闭broker
sh bin/mqshutdown broker
# 关闭name server:
sh bin/mqshutdown namesrv
2.2 控制台搭建
1、在GitHub克隆源代码
git clone git clone git@github.com:apache/rocketmq-externals.git
2、修改配置文件
cd rocketmq-externals/rocketmq-console/
vim src/main/resources/application.properties
# 修改为namesrv地址
rocketmq.config.namesrvAddr=127.0.0.1:9876
# 访问端口
server.port=8080
3、打包运行
# 忽略测试用例
mvn clean package -DskipTests
# 启动程序
nohup java -jar rocketmq-console-ng-1.0.1.jar &
nohup java -jar rocketmq-console-ng-1.0.1.jar -server.port=8080--rocketmq.config.namesrvAddr=127.0.0.1:9876
# 查看日志
tail -f ~/logs/consolelogs/rocketmq-console.log
4、访问localhost:8080即可进入控制台页面。
2.3 实例应用
添加依赖
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.1</version>
</dependency>
如果该版本在消费消息时报错,就替换为4.5.0版本。
2.3.1 发送顺序消息
一个topic下有多个queue(默认是4个),如果将相同类型的消息发送到同一个队列中,那就保证了发送有序。
在send函数时,去实现一个MessageQueueSelector的匿名类,保证每条消息选择的队列是同一个。
public class Producer {
public static void main(String[] args) throws MQClientException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("TestRocketMQProducer");
producer.setSendMsgTimeout(60000);
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
int messageCount = 5;
for (int i = 0; i < messageCount; i++) {
try {
Message msg = new Message("TopicTest", "TagA",
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
SendResult sendResult = producer.send(msg,
(list, message, o) -> list.get((int) message.hashCode() % list.size()),
0);
System.out.printf("%s%n", sendResult);
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(10);
}
}
producer.shutdown();
}
}
2.3.2 发送异步消息
异步消息需要在发送时实现sendCallback()的匿名类,里面继承了发送成功/失败的处理方法。
但需要注意的是:如果只是简单的将上面那份demo代码改为异步方式,会报错,这是因为主线程提前关闭,导致异步线程受到影响,所以我采用CountDownLatch来解决。保证异步任务都执行后才关闭主线程。后面还有一个地方用到了CountDownLatch。
public class AsynProducer {
public static void main(String[] args) throws MQClientException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("TestRocketMQProducer-Asyn");
producer.setSendMsgTimeout(60000);
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
int messageCount = 5;
final CountDownLatch countDownLatch = new CountDownLatch(messageCount);
for (int i = 0; i < messageCount; i++) {
try {
final int index = i;
Message msg = new Message("TopicTest", "TagA",
("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
);
producer.send(msg,
(list, message, o) -> list.get((int) message.hashCode() % list.size()),
0,
new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
countDownLatch.countDown();
System.out.printf("%s%n", sendResult);
}
@Override
public void onException(Throwable e) {
countDownLatch.countDown();
System.out.println(e);
}
});
} catch (Exception e) {
e.printStackTrace();
Thread.sleep(10);
}
}
countDownLatch.await();
producer.shutdown();
}
}
2.3.3 接受消息
消费者注册监听有两种模式,有序消费MessageListenerOrderly和并发消费MessageListenerConcurrently,这两种模式返回值不同。
此处先用MessageListenerConcurrently实现监听。后续再分析两者不同,以及push和pull的具体区别。
public class Consumer {
public static void main(String[] args) throws InterruptedException, MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("TestRocketMQPushConsumer");
consumer.setNamesrvAddr("127.0.0.1:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 订阅Topic
consumer.subscribe("TopicTest", "*");
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs,
ConsumeConcurrentlyContext context) {
System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs);
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
consumer.start();
System.out.printf("Consumer Started.%n");
}
}
三、路由中心NameServer
RocketMQ没有去选用Zookeeper充当消息管理的“注册中心”,而是设计NameServer来实现元数据(Topic路由消息等)的管理。Topic路由信息无须在集群之间保持强一致,无须去追求最终一致性,基于这种理念,RocketMQ的NameServer集群之间互不通信,降低了注册中心的实现复杂度,对网络的要求也降低了不少。
3.1 启动
NameServer启动主函数是:org.apache.rocketmq.namesrv.NamesrvStartup#main,主要分为三个步骤。
3.1.1 填充配置属性
final NamesrvConfig namesrvConfig = new NamesrvConfig();
final NettyServerConfig nettyServerConfig = new NettyServerConfig();
在启动前,NameServer会先解析配置文件,然后填充NamesrvConfig、NettyServerConfig等属性类,包括监听端口(默认是9876)、Netty业务线程池个数等参数。
3.1.2 创建NamesrvController
NamesrvController实例是NameServer的核心控制器,里面包含了各类配置、注册表信息和服务,看一下其初始化方法。
public boolean initialize() {
// 从持久化文件中加载KV配置到内存
this.kvConfigManager.load();
// 创建基于netty的y远程服务
this.remotingServer = new NettyRemotingServer(this.nettyServerConfig, this.brokerHousekeepingService);
// 创建线程池
this.remotingExecutor =
Executors.newFixedThreadPool(nettyServerConfig.getServerWorkerThreads(), new ThreadFactoryImpl("RemotingExecutorThread_"));
// 注册处理远程请求的处理器
this.registerProcessor();
// 每隔10s扫描一次Broker,移除处于不激活状态的Broker
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.routeInfoManager.scanNotActiveBroker();
}
}, 5, 10, TimeUnit.SECONDS);
// 每隔10分钟打印一次KV配置
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
NamesrvController.this.kvConfigManager.printAllPeriodically();
}
}, 1, 10, TimeUnit.MINUTES);
}
主要是加载KV配置,创建NettyServer网络处理对象,然后开启两个定时任务,在RocketMQ中将此类定时器统称为心跳检测。
在rocketmq中,默认使用json数据格式存储KV配置。
3.1.3 启动服务器
注册JVM钩子函数并启动服务器,以便监听Broker、Producer的网络请求。
// JVM钩子函数,在JVM进程关闭之前,先将线程池关闭,释放资源
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, new Callable<Void>() {
@Override
public Void call() throws Exception {
controller.shutdown();
return null;
}
}));
// 启动服务器
controller.start();
这里用了钩子函数,可以参考一下。如果在代码中用了线程池,我们可以注册一个JVM钩子函数,在JVM进程关闭之前,先将线程池关闭,释放资源。
3.2 路由注册
首先,我们先去看一下NameServer都有哪些路由元信息。
3.2.1 路由元信息
NameServer路由实现类:org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager
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;
- topicQueueTable:Topic消息队列路由信息,生产者消息发送时根据路由表进行负载均衡。
其中,QueueData的数据如下:
private String brokerName;
private int readQueueNums;
private int writeQueueNums;
private int perm;//读写权限,具体含义参考PermName
private int topicSynFlag;//topic同步标记,具体含义参考TopicSysFlag
- breakerAddrTable:Broker基础信息,包含brokerName、所属集群名称、主备Broker地址,具体如下
private String cluster;
private String brokerName;
private HashMap<Long/* brokerId */, String/* broker address */> brokerAddrs;//brokerId=0表示Master,大于0表示从Slave
- clusterAddrTable:Broker集群信息,存储集群内所有Broker名称。
- brokerLiveTable:Broker状态信息,NameServer每次收到心跳包时就会替换该信息,里面的lastUpdateTimestamp字段就用于存储上次收到Broker心跳包的时间。
- filterServerTable:类模式信息过滤。
了解上述路由元信息后,去看路由注册功能的实现。
RocketMQ路由注册是通过Broker和NameServer的心跳功能来实现的。Broker启动时向集群中所有的NameServer发送心跳语句,然后每隔10s~60s再发心跳包;NameServer收到Broker心跳包后会更新brokerLiveTable缓存,然后NameServer每隔10s扫描brokerLiveTable,如果连续120s没有收到心跳包,NameServer将移除该Broker的路由信息并同时关闭Broker连接。
3.2.2 Broker发送心跳包
这时,需要先找到Broker的源代码。在启动函数org.apache.rocketmq.broker.BrokerController#start中,启动了一个定时任务,用于注册心跳包。
public void start() throws Exception {
//...
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
BrokerController.this.registerBrokerAll(true, false, brokerConfig.isForceRegister());
} catch (Throwable e) {
log.error("registerBrokerAll Exception", e);
}
}
}, 1000 * 10, Math.max(10000, Math.min(brokerConfig.getRegisterNameServerPeriod(), 60000)), TimeUnit.MILLISECONDS);
//...
}
具体在看org.apache.rocketmq.broker.BrokerController#registerBrokerAll函数。
public synchronized void registerBrokerAll(final boolean checkOrderConfig, boolean oneway, boolean forceRegister) {
// 将topicConfigTable封装到TopicConfigSerializeWrapper中
TopicConfigSerializeWrapper topicConfigWrapper = this.getTopicConfigManager().buildTopicConfigSerializeWrapper();
// 如果当前Broker没有读写权限,重新拼装topicConfigTable到TopicConfigSerializeWrapper中
// 就是重新将不可读或者不可写的权限封装进TopicConfigSerializeWrapper中
if (!PermName.isWriteable(this.getBrokerConfig().getBrokerPermission())
|| !PermName.isReadable(this.getBrokerConfig().getBrokerPermission())) {
ConcurrentHashMap<String, TopicConfig> topicConfigTable = new ConcurrentHashMap<String, TopicConfig>();
for (TopicConfig topicConfig : topicConfigWrapper.getTopicConfigTable().values()) {
TopicConfig tmp =
new TopicConfig(topicConfig.getTopicName(), topicConfig.getReadQueueNums(), topicConfig.getWriteQueueNums(),
this.brokerConfig.getBrokerPermission());
topicConfigTable.put(topicConfig.getTopicName(), tmp);
}
topicConfigWrapper.setTopicConfigTable(topicConfigTable);
}
// forceRegister是否强制注册
// needRegister方法是与配置的每个NameServer进行通信
// 判断topicConfigTable是否改变了,只要其中一个namesrv改变了就需要发起注册更新
if (forceRegister || needRegister(this.brokerConfig.getBrokerClusterName(),
this.getBrokerAddr(),
this.brokerConfig.getBrokerName(),
this.brokerConfig.getBrokerId(),
this.brokerConfig.getRegisterBrokerTimeoutMills())) {
doRegisterBrokerAll(checkOrderConfig, oneway, topicConfigWrapper);
}
}
进一步到org.apache.rocketmq.broker.out.BrokerOuterAPI#registerBrokerAll函数,里面主要的就是开始并发向namesrv集群注册。
// 并发向namesrv集群注册broker
for (final String namesrvAddr : nameServerAddressList) {
brokerOuterExecutor.execute(new Runnable() {
@Override
public void run() {
try {
// 注册broker服务任务分
RegisterBrokerResult result = registerBroker(namesrvAddr,oneway, timeoutMills,requestHeader,body);
if (result != null) {
registerBrokerResultList.add(result);
}
log.info("register broker[{}]to name server {} OK", brokerId, namesrvAddr);
} catch (Exception e) {
log.warn("registerBroker Exception, {}", namesrvAddr, e);
} finally {
countDownLatch.countDown();
}
}
});
}
再往下的函数就是打包网络请求之类的,不再深究。Rocket网络传输是基于Netty。
3.3.3 NameServer处理心跳包
org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest网络处理器解析请求类型,如果请求类型为RequestCode.REGISTER_BROKER,则请求最终转发到org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#registerBroker函数。这个函数有点长,我在关键语句上加了注释,就不再一一解释了。
public RegisterBrokerResult registerBroker(
final String clusterName,
final String brokerAddr,
final String brokerName,
final long brokerId,
final String haServerAddr,
final TopicConfigSerializeWrapper topicConfigWrapper,
final List<String> filterServerList,
final Channel channel) {
RegisterBrokerResult result = new RegisterBrokerResult();
try {
try {
// 加写锁,防止并发修改RouteInfoManager中的路由表
this.lock.writeLock().lockInterruptibly();
// 判断Broker所属集群是否存在,如果不存在,则创建,然后将broker名加入到集群集合中。
Set<String> brokerNames = this.clusterAddrTable.get(clusterName);
if (null == brokerNames) {
brokerNames = new HashSet<String>();
this.clusterAddrTable.put(clusterName, brokerNames);
}
brokerNames.add(brokerName);
boolean registerFirst = false;
// 从brokerAddrTable根据BrokerName尝试获取Broker信息
BrokerData brokerData = this.brokerAddrTable.get(brokerName);
if (null == brokerData) {
// 如果不存在,则说明是首次注册,并新建BrokerData放入到brokerAddrTable
registerFirst = true;
brokerData = new BrokerData(clusterName, brokerName, new HashMap<Long, String>());
this.brokerAddrTable.put(brokerName, brokerData);
}
Map<Long, String> brokerAddrsMap = brokerData.getBrokerAddrs();
//Switch slave to master: first remove <1, IP:PORT> in namesrv, then add <0, IP:PORT>
//The same IP:PORT must only have one record in brokerAddrTable
// 必须保证一个地址只能有一条记录,若有重复记录,则删除掉。
Iterator<Entry<Long, String>> it = brokerAddrsMap.entrySet().iterator();
while (it.hasNext()) {
Entry<Long, String> item = it.next();
if (null != brokerAddr && brokerAddr.equals(item.getValue()) && brokerId != item.getKey()) {
it.remove();
}
}
// 如果存在,替换原先的地址
String oldAddr = brokerData.getBrokerAddrs().put(brokerId, brokerAddr);
// 表示非第一次注册
registerFirst = registerFirst || (null == oldAddr);
//如果Broker为Master,并且Broker Topic配置信息发生变化或者是初次注册,则需要创建或更新Topic路由元数据
//填充topicQueueTable就是为默认主题自动注册路由信息
if (null != topicConfigWrapper
&& MixAll.MASTER_ID == brokerId) {
if (this.isBrokerTopicConfigChanged(brokerAddr, topicConfigWrapper.getDataVersion())
|| registerFirst) {
ConcurrentMap<String, TopicConfig> tcTable =
topicConfigWrapper.getTopicConfigTable();
if (tcTable != null) {
for (Map.Entry<String, TopicConfig> entry : tcTable.entrySet()) {
// 根据TopicConfig创建QueueData数据结构,然后更新topicQueueTable
this.createAndUpdateQueueData(brokerName, entry.getValue());
}
}
}
}
// 更新BrokerLiveInfo,尤其是lastUpdateTimestamp信息,这是执行路由删除的重要凭证
BrokerLiveInfo prevBrokerLiveInfo = this.brokerLiveTable.put(brokerAddr,
new BrokerLiveInfo(
System.currentTimeMillis(),
topicConfigWrapper.getDataVersion(),
channel,
haServerAddr));
if (null == prevBrokerLiveInfo) {
log.info("new broker registered, {} HAServer: {}", brokerAddr, haServerAddr);
}
if (filterServerList != null) {
if (filterServerList.isEmpty()) {
this.filterServerTable.remove(brokerAddr);
} else {
this.filterServerTable.put(brokerAddr, filterServerList);
}
}
if (MixAll.MASTER_ID != brokerId) {
// 如果此Broker为从节点,则需要查找该Broker的Master节点信息,并更新对应的masterAddr属性
String masterAddr = brokerData.getBrokerAddrs().get(MixAll.MASTER_ID);
if (masterAddr != null) {
BrokerLiveInfo brokerLiveInfo = this.brokerLiveTable.get(masterAddr);
if (brokerLiveInfo != null) {
result.setHaServerAddr(brokerLiveInfo.getHaServerAddr());
result.setMasterAddr(masterAddr);
}
}
}
} finally {
// 卸锁
this.lock.writeLock().unlock();
}
} catch (Exception e) {
log.error("registerBroker Exception", e);
}
return result;
}
NameServer与Broker保持长连接,Broker状态存储在brokerLiveTable中,NameServer每收到一个心跳包,将更新brokerLiveTable中关于Broker的状态信息以及路由表(topicQueueTable、brokerAddrTable、brokerLiveTable、filterServerTable)。更新上述路由表(HashTable)使用了锁粒度较少的读写锁,允许多个消息发送者(Producer)并发读,保证消息发送时的高并发。但同一时刻NameServer只处理一个Broker心跳包,多个心跳包请求串行执行。
3.3 路由删除
RocketMQ有两个触发点来触发路由删除:
- NameServer定时扫描brokerLiveTable检测上次心跳包与当前系统时间的时间差,如果时间戳大于120s,则需要移除该Broker信息。
- Broker在正常关闭的情况下,会执行unregisterBroker指令。
由于不管是何种方式触发的路由删除,最终的方法都是一致的,就是从topicQueueTable、brokerAddrTable、brokerLiveTable、filterServerTable删除与该Broker相关的信息。
先来看第一种方式,定位到org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#scanNotActiveBroker函数。
public void scanNotActiveBroker() {
Iterator<Entry<String, BrokerLiveInfo>> it = this.brokerLiveTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, BrokerLiveInfo> next = it.next();
long last = next.getValue().getLastUpdateTimestamp();
// BROKER_CHANNEL_EXPIRED_TIME是120s
if ((last + BROKER_CHANNEL_EXPIRED_TIME) < System.currentTimeMillis()) {
RemotingUtil.closeChannel(next.getValue().getChannel());
it.remove();
log.warn("The broker channel expired, {} {}ms", next.getKey(), BROKER_CHANNEL_EXPIRED_TIME);
this.onChannelDestroy(next.getKey(), next.getValue().getChannel());
}
}
}
逻辑很简单,遍历brokerLiveTable路由表集合,检测BrokerLiveInfo的lastUpdateTimestamp上次收到心跳包的时间如果超过当前时间120s,则认定该Broker已不可用,需要将它移除,关闭Channel,然后删除与该Broker相关的路由信息,具体看org.apache.rocketmq.namesrv.routeinfo.RouteInfoManager#onChannelDestroy函数,逻辑很简单,一层层删除。具体代码不再赘述。需要注意的是,删除信息时需要申请写锁。
3.4 路由发现
RocketMQ路由发现是非主动的,当Topic路由出现变化后,NameServer不主动推送给客户端,而是由客户端定时拉取主题最新的路由。根据主题名称拉取路由信息的命令编码为:RequestCode.GET_ROUTEINFO_BY_TOPIC,解析请求同样也在org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#processRequest函数内,最终请求转发至org.apache.rocketmq.namesrv.processor.DefaultRequestProcessor#getRouteInfoByTopic。
public RemotingCommand getRouteInfoByTopic(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final GetRouteInfoRequestHeader requestHeader =
(GetRouteInfoRequestHeader) request.decodeCommandCustomHeader(GetRouteInfoRequestHeader.class);
// 获取填充好的TopicRouteData路由信息
TopicRouteData topicRouteData = this.namesrvController.getRouteInfoManager().pickupTopicRouteData(requestHeader.getTopic());
if (topicRouteData != null) {
if (this.namesrvController.getNamesrvConfig().isOrderMessageEnable()) {
// 如果找到主题对应的路由信息并且该主题为顺序消息,则从NameServer的KVConfig中获取关于顺序消息相关的配置填充路由信息
String orderTopicConf =
this.namesrvController.getKvConfigManager().getKVConfig(NamesrvUtil.NAMESPACE_ORDER_TOPIC_CONFIG,
requestHeader.getTopic());
topicRouteData.setOrderTopicConf(orderTopicConf);
}
byte[] content = topicRouteData.encode();
response.setBody(content);
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}
// 如果找不到路由信息,直接返回无
response.setCode(ResponseCode.TOPIC_NOT_EXIST);
response.setRemark("No topic route info in name server for the topic: " + requestHeader.getTopic()
+ FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
return response;
}
四、消息发送
4.1 消息类
发送消息的封装类是org.apache.rocketmq.common.message.Message,基本属性包括消息所属主题topic、消息Flag(不作处理)、扩展属性properties和消息体body。
public class Message implements Serializable {
/**
* 消息所属主题
*/
private String topic;
/**
* 消息Flag(RocketMQ不作处理)
*/
private int flag;
/**
* 扩展属性
*/
private Map<String, String> properties;
/**
* 消息体
*/
private byte[] body;
/**
* 交易编号
*/
private String transactionId;
}
Message扩展属性主要包含下面几个:
- tag:消息TAG,用于消息过滤。
- keys:Message索引键,多个用空格隔开,rocketmq可以根据这些key来检索到消息。
- waitStoreMsgOK:消息发送时是否等消息存储完成后再返回。
- delayTimeLevel:消息延迟级别,用于定时消息或者消息重试。
4.2 启动流程
启动流程体现在DefaultMQProducerImpl.start()函数。
this.serviceState = ServiceState.START_FAILED;
// 检查启动参数
this.checkConfig();
// 检查producerGroup是否符合要求,并改变生产者的instanceName为进程ID(为了后面创建不同进程下的clientId)
if (!this.defaultMQProducer.getProducerGroup().equals(MixAll.CLIENT_INNER_PRODUCER_GROUP)) {
this.defaultMQProducer.changeInstanceNameToPID();
}
// 创建MQClientInstance实例,整个JVM中只有一个MQClientManager管理器,里面维护一个MQClientInstance缓存表
// clientID为IP+instanceName+(unitname可选)
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
// 向MQClientFactory注册,将当前生产者加入到MQClientInstance管理中,方便后续网络请求、心跳检测等。
boolean registerOK = mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
if (!registerOK) {
this.serviceState = ServiceState.CREATE_JUST;
throw new MQClientException("The producer group[" + this.defaultMQProducer.getProducerGroup()
+ "] has been created before, specify another name please." + FAQUrl.suggestTodo(FAQUrl.GROUP_NAME_DUPLICATE_URL),
null);
}
// topic主题注册
this.topicPublishInfoTable.put(this.defaultMQProducer.getCreateTopicKey(), new TopicPublishInfo());
if (startFactory) {
// 启动
mQClientFactory.start();
}
log.info("the producer [{}] start OK. sendMessageWithVIPChannel={}", this.defaultMQProducer.getProducerGroup(),
this.defaultMQProducer.isSendMessageWithVIPChannel());
this.serviceState = ServiceState.RUNNING;
大体流程如下所示:
需要关注的有几个点:
- 创建MQClientInstance实例是通过MQClientManager实现的,其中,MQClientManager是一个饿汉模式的单例,在JVM中只有一个,里面维护了一个key为clientId(IP@instanceName@unitName),value为MQClientInstance的缓存集合。
- MQClientInstance才是具体客户端的配置管理中心,它会将当前producer对象注册到MQClientInstance实例对象的producerTable集合(<String/* group */, MQProducerInner>)中,一个jvm(一个客户端)中一个producerGroup只能有一个实例。此外,MQClientInstance中也包括ClientConfig等具体配置。
- producer.start()函数结尾,也调用了mQClientFactory.start(),在MQClientInstance实例启动中,会打开多个定时任务(startScheduledTask),里面包括从namesrv更新路由主题信息、向broker发送心跳包等。
4.3消息发送流程
消息发送过程以最基础的org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#send(Message msg)为例。大体的流程如下图所示。
把不相关代码,函数如下。我们将按照注释的顺序进行分析。
private SendResult sendDefaultImpl(
Message msg,
final CommunicationMode communicationMode,
final SendCallback sendCallback,
final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
this.makeSureStateOK();
// 1、消息验证(比如长度等)
Validators.checkMessage(msg, this.defaultMQProducer);
final long invokeID = random.nextLong();
long beginTimestampFirst = System.currentTimeMillis();
long beginTimestampPrev = beginTimestampFirst;
long endTimestamp = beginTimestampFirst;
// 2、获取主题路由信息
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
boolean callTimeout = false;
MessageQueue mq = null;
Exception exception = null;
SendResult sendResult = null;
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
String[] brokersSent = new String[timesTotal];
for (; times < timesTotal; times++) {
String lastBrokerName = null == mq ? null : mq.getBrokerName();
// 3、消息队列选择
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
mq = mqSelected;
brokersSent[times] = mq.getBrokerName();
try {
beginTimestampPrev = System.currentTimeMillis();
if (times > 0) {
//Reset topic with namespace during resend.
msg.setTopic(this.defaultMQProducer.withNamespace(msg.getTopic()));
}
long costTime = beginTimestampPrev - beginTimestampFirst;
if (timeout < costTime) {
callTimeout = true;
break;
}
// 4、发送消息
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
endTimestamp = System.currentTimeMillis();
// 5、更新Broker规避规则
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
switch (communicationMode) {
case ASYNC:
case ONEWAY:
return null;
case SYNC:
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
continue;
}
}
return sendResult;
default:
break;
}
}/** catch信息忽略掉 **/
}
}
}
if (sendResult != null) {
return sendResult;
}
}
4.3.1 消息长度验证
消息发送之前,首先保证Producer处于运行状态,然后验证消息是否符合相应的规范,具体的规范要求是主题名称不为空且符合命名规范、消息体不能为空、消息长度不能等于0且默认不能超过允许发送消息的最大长度4M(maxMessageSize = 1024 * 1024 * 4; )。具体体现在org.apache.rocketmq.client.Validators#checkMessage函数中。
4.3.2 查找主题路由消息
查找主题路由消息体现在org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#tryToFindTopicPublishInfo函数。
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
// 首先从本地存储的路由信息表中获取
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(topic);
// 第一次发送消息时,本地没有缓存topic的路由信息
if (null == topicPublishInfo || !topicPublishInfo.ok()) {
this.topicPublishInfoTable.putIfAbsent(topic, new TopicPublishInfo());
// 通过查询NameServer获取主题信息
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
}
if (topicPublishInfo.isHaveTopicRouterInfo() || topicPublishInfo.ok()) {
return topicPublishInfo;
} else {
// 如果还未找到路由信息,使用默认Topic名称去获取,目的是当Broker开启自动创建Topic开关时,Broker接收到信息后自动创建Topic
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
}
}
在org.apache.rocketmq.client.impl.factory.MQClientInstance#updateTopicRouteInfoFromNameServer(String topic, boolean isDefault, DefaultMQProducer defaultMQProducer)函数中,有一部分需要注意,如果获取的主题路由信息有变化,需要更新。
TopicPublishInfo publishInfo = topicRouteData2TopicPublishInfo(topic, topicRouteData);
publishInfo.setHaveTopicRouterInfo(true);
Iterator<Entry<String, MQProducerInner>> it = this.producerTable.entrySet().iterator();
while (it.hasNext()) {
Entry<String, MQProducerInner> entry = it.next();
MQProducerInner impl = entry.getValue();
if (impl != null) {
impl.updateTopicPublishInfo(topic, publishInfo);
}
}
这里最终是要更新每个生产者producer的TopicPublishInfo,需要先循环this.producerTable,然后把TopicPublishInfo赋值给了producerTable里的每一个Impl(生产者),这时我们需要搞清楚这几个之间的关系。
一个生产者(MQProducerInner)拥有一个topicPublishInfoTable,存储Topic路由信息。
private final ConcurrentMap<String/* topic */, TopicPublishInfo> topicPublishInfoTable = new ConcurrentHashMap<String, TopicPublishInfo>();
一个生产者组(MQClientInstance)拥有一个producerTable,存储着组内所有生产者信息。
private final ConcurrentMap<String/* group */, MQProducerInner> producerTable = new ConcurrentHashMap<String, MQProducerInner>();
4.3.3 选择消息队列
消息在发送时要根据上文中查询到的路由信息选择消息队列,在选择消息队列时有两种方式:
- MQFaultStrategy.sendLatencyFaultEnable=false,默认不启用Broker故障延迟机制。
- MQFaultStrategy.sendLatencyFaultEnable=true,启用Broker故障延迟机制。
不启用Broker故障延迟机制
该流程较为简单,调用org.apache.rocketmq.client.impl.producer.TopicPublishInfo#selectOneMessageQueue函数。
public MessageQueue selectOneMessageQueue(final String lastBrokerName) {
if (lastBrokerName == null) {
return selectOneMessageQueue();
} else {
int index = this.sendWhichQueue.getAndIncrement();
for (int i = 0; i < this.messageQueueList.size(); i++) {
int pos = Math.abs(index++) % this.messageQueueList.size();
if (pos < 0)
pos = 0;
MessageQueue mq = this.messageQueueList.get(pos);
if (!mq.getBrokerName().equals(lastBrokerName)) {
return mq;
}
}
return selectOneMessageQueue();
}
}
public MessageQueue selectOneMessageQueue() {
int index = this.sendWhichQueue.getAndIncrement();
int pos = Math.abs(index) % this.messageQueueList.size();
if (pos < 0)
pos = 0;
return this.messageQueueList.get(pos);
}
lastBrokerName表示上一次选择的执行发送消息失败的Broker,第一次执行消息队列选择时,lastBrokerName为null,此时直接用sendWhichQueue自增获取值,与当前路由表中信息队列个数取模,返回该位置的MessageQueue,如果消息再次发送失败的话,下次进行消息队列选择时规避上次MessageQueue所在的Broker,否则还是很有可能失败。
该方法是存在性能缺陷的,因为路由算法中的消息队列是按照Broker排序的,如果上一次选择的是宕机的Broker的第一个队列,那么随后下一次选择的宕机Broker的第二个队列,消息发送同样很有可能失败,再次引发重试,带来不必要的性能损耗。那么有什么方法,可以在一次消息发送失败后,暂时将该Broker排除在消息队列选择范围外呢。就是下面的方案。
启用Broker故障延迟机制
此时sendLatencyFaultEnable=true。先对消息队列集合进行轮询获取一个合适的消息队列,并验证是否可用;如果最后没有最佳可用队列的话,就尝试从规避的broker中选一个凑付的,然后判断这个凑付的是否能用。这里有一行代码我认为逻辑写反了,当前brokerName与上一次规避brokerName不同的时候,才允许当前可用。
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
if (this.sendLatencyFaultEnable) {
try {
int index = tpInfo.getSendWhichQueue().getAndIncrement();
for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (pos < 0)
pos = 0;
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
// 验证此broker是否需要规避
if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
// 我自己认为写错了
if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
return mq;
}
}
// 如果找不到合适的broker,就尝试从规避的Broker中找出一个可用的,找不到的话就返回null
final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
if (writeQueueNums > 0) {
final MessageQueue mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
}
return mq;
} else {
latencyFaultTolerance.remove(notBestBroker);
}
} catch (Exception e) {
log.error("Error occurred when selecting message queue", e);
}
return tpInfo.selectOneMessageQueue();
}
这时候涉及到了一个新的实现类LatencyFaultToleranceImpl。
public class LatencyFaultToleranceImpl implements LatencyFaultTolerance<String> {
// 用于保存规避的Broker
private final ConcurrentHashMap<String, FaultItem> faultItemTable = new ConcurrentHashMap<String, FaultItem>(16);
private final ThreadLocalIndex whichItemWorst = new ThreadLocalIndex();
/**
* 更新失败条目
* @param name brokerName
* @param currentLatency 消息发送故障延迟时间
* @param notAvailableDuration 不可用持续时长,在这个时间内,Broker将被规避
*/
@Override
public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration);
/**
* 判断Borker是否可用
* @param name brokerName
* @return
*/
@Override
public boolean isAvailable(final String name);
/**
* 移除Fault条目,意味着Broker重新参与路由计算
* @param name
*/
@Override
public void remove(final String name);
/**
* 尝试从规避的Broker中选择一个可用的Broker,如果没有找到,将返回null
* 排序链表的前半部分选出一个broker,即至少不是最差策略
* @return
*/
@Override
public String pickOneAtLeast();
class FaultItem implements Comparable<FaultItem> {
// BrokerName
private final String name;
// 消息发送故障延迟时间
private volatile long currentLatency;
// 预测可用时间(当前时间+notAvailableDuration)
private volatile long startTimestamp;
}
}
在发送函数的第5步中,涉及到更新broker规避规则。
private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
if (this.sendLatencyFaultEnable) {
// 获取故障延迟时间
long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
// 更新规则规则
this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
}
}
private long computeNotAvailableDuration(final long currentLatency) {
for (int i = latencyMax.length - 1; i >= 0; i--) {
if (currentLatency >= latencyMax[i])
return this.notAvailableDuration[i];
}
return 0;
}
updateFaultItem函数是根据超时时间(latencyMax)来预测故障持续时间/规避时长(notAvailableDuration)的,两者有一一对应关系,比如:
- currentLatency大于等于50小于100,则notAvailableDuration为0
- currentLatency大于等于100小于550,则notAvailableDuration为0
- currentLatency大于等于550小于1000,则notAvailableDuration为300000
- 等等
故障持续时间又决定了可用时间,startTimestamp=当前时间+notAvailableDuration。以上便是消息队列选择机制。
4.3.4 消息发送
netty发送
消息发送主要在org.apache.rocketmq.client.impl.producer.DefaultMQProducerImpl#sendKernelImpl函数中,又分为同步、异步和单向发送。其主要流程如下:
在内部具体信息发送函数中:
- 异步消息调用的是 NettyRemotingClient.invokeAsync方法
- 同步消息调用的是 NettyRemotingClient.invokeSync方法
从发送消息的关键代码开始分析:NettyRemotingAbstract.invokeSyncImpl()
public RemotingCommand invokeSyncImpl(final Channel channel, final RemotingCommand request,
final long timeoutMillis)
throws InterruptedException, RemotingSendRequestException, RemotingTimeoutException {
// 发送请求的标识,请求、响应均会包含该字段
// 用于接收异步响应时,根据Opaque 找到对应的回调函数或者设置返回结果
final int opaque = request.getOpaque();
try {
final ResponseFuture responseFuture = new ResponseFuture(channel, opaque, timeoutMillis, null, null);
this.responseTable.put(opaque, responseFuture);
final SocketAddress addr = channel.remoteAddress();
// 使用writeAndFlush发请求,建立返回值的监听,这是netty-client发送消息,还有netty-server收消息
channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
if (f.isSuccess()) {
// 执行成功后return,跳出这个监听
responseFuture.setSendRequestOK(true);
return;
} else {
responseFuture.setSendRequestOK(false);
}
responseTable.remove(opaque);
responseFuture.setCause(f.cause());
responseFuture.putResponse(null);
log.warn("send a request command to channel <" + addr + "> failed.");
}
});
// 阻塞等待结果,使用countDownLatch挂起线程,等待收到netty的返回值
RemotingCommand responseCommand = responseFuture.waitResponse(timeoutMillis);
// 在clinet的channelRead0将responseCommand清零,自动返回
// 若为空,说明超过一定时间还未获取返回值,这时抛出异常,交由用户处理
if (null == responseCommand) {
if (responseFuture.isSendRequestOK()) {
throw new RemotingTimeoutException(RemotingHelper.parseSocketAddressAddr(addr), timeoutMillis,
responseFuture.getCause());
} else {
throw new RemotingSendRequestException(RemotingHelper.parseSocketAddressAddr(addr), responseFuture.getCause());
}
}
return responseCommand;
} finally {
this.responseTable.remove(opaque);
}
}
通过上面函数,有几个关键点:
- 请求与响应的RemotingCommand中,有一个关键的opaque,用于标识一次请求。并通过opaque获取ResponseFuture,得到对应的响应结果。
- 发送消息是使用writeAndFlush发请求,并建立返回值的监听。
- 发送消息通过CountDownLatch阻塞,并等待响应结果。有两种情况,一个是超时后自动唤醒;另一个是client监听到响应结果后,并通过countdown()唤醒发送时的阻塞。
- 回调函数是在netty的ResponseFuture中设置,具体流程还不清楚。
异常处理
在发送消息时候,如果遇到异常情况(比如服务端没有response返回给客户端或者response因网络而丢失),上面所述的responseTable的本地缓存Map将会出现堆积情况。这个时候需要一个定时任务来专门做responseTable的清理回收。这就是NettyRemotingAbstract.scanResponseTable所做的事情。
public void scanResponseTable() {
final List<ResponseFuture> rfList = new LinkedList<ResponseFuture>();
Iterator<Entry<Integer, ResponseFuture>> it = this.responseTable.entrySet().iterator();
while (it.hasNext()) {
Entry<Integer, ResponseFuture> next = it.next();
ResponseFuture rep = next.getValue();
if ((rep.getBeginTimestamp() + rep.getTimeoutMillis() + 1000) <= System.currentTimeMillis()) {
rep.release();
it.remove();
rfList.add(rep);
log.warn("remove timeout request, " + rep);
}
}
for (ResponseFuture rf : rfList) {
try {
executeInvokeCallback(rf);
} catch (Throwable e) {
log.warn("scanResponseTable, operationComplete Exception", e);
}
}
}
在RocketMQ客户端/服务端启动的时候,会开启一个间隔时间为1s的定时任务,来执行scanResponseTable函数,检查所有的responseTable缓存中的responseFuture变量。
五、总结
- name server的路由发现机制可以用下图来总结表示:
- 消息生产者在发送消息时,如果本地路由表中未缓存topic的路由信息,则向nameServer发送获取路由信息请求,更新本地路由信息表,并且生产者每隔30s就会从nameServer更新路由表。
- 消息发送高可用性主要通过两个手段来体现:重试与Broker规避。Broker规避就是如果在一次发送时出现了错误,那么在过来的一段时间内,生产者不会再选用该Broker上的消息队列,以提高发送消息的成功率。
- 写的太啰嗦了,啰哩啰嗦。
六、参考文献