1. 前言
MQProducer是RocketMQ提供的生产者接口,默认实现为DefaultMQProducer,如果要发送事务消息,对应的实现类为TransactionMQProducer。本篇暂不讨论事务消息,主要分析下默认生产者的启动和发送消息的流程。
DefaultMQProducer仅仅是RocketMQ暴露给用户使用的外观类,它内部持有一个生产者实现DefaultMQProducerImpl,它才是具体的执行者。使用这种结构的好处是,RocketMQ在后续版本可以任意更换实现类,这一切对用户而言是零感知的。
RocketMQ基于Netty实现网络传输,Producer要和NameServer、Broker交互,所以它自然也是一个Netty客户端,Producer在启动时,会同时启动Netty客户端,然后向NameServer拉取Broker信息,向Broker发送心跳。发送消息时,从NameServer拉取Topic路由消息,轮询出一个MessageQueue,再将消息发到关联的Broker上。
2. 源码分析
笔者画了Producer启动和消息发送的时序图:
2.1 客户端启动
在发送消息前,首先需要创建Producer,然后启动它。
DefaultMQProducer producer = new DefaultMQProducer(GroupName);
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
1.在DefaultMQProducer的构造函数里,会创建生产者实现类DefaultMQProducerImpl。
public DefaultMQProducer(final String namespace, final String producerGroup, RPCHook rpcHook) {
// 命名空间
this.namespace = namespace;
// 生产者组名
this.producerGroup = producerGroup;
// 生产者实现
defaultMQProducerImpl = new DefaultMQProducerImpl(this, rpcHook);
}
2.启动Producer,因为仅仅是外观类,它的核心还是在于启动DefaultMQProducerImpl。如果开启消息轨迹追踪,还会额外启动TraceDispatcher服务,这里暂不讨论。
public void start() throws MQClientException {
this.setProducerGroup(withNamespace(this.producerGroup));
this.defaultMQProducerImpl.start();
if (null != traceDispatcher) {
try {
traceDispatcher.start(this.getNamesrvAddr(), this.getAccessChannel());
} catch (MQClientException e) {
log.warn("trace dispatcher start failed ", e);
}
}
}
DefaultMQProducerImpl的启动,主要做了以下事情:
- 校验GroupName
- 设置InstanceName
- 创建MQClientInstance
- 注册Producer到producerTable
- 启动MQClientInstance
- 向所有Broker发心跳
一步步看,首先checkConfig()方法会校验GroupName的合法性,不能过长,不能特殊字符,不能用DEFAULT_PRODUCER等。
如果没有设置InstanceName,会自动设置为进程PID+#+时间戳。
public void changeInstanceNameToPID() {
if (this.instanceName.equals("DEFAULT")) {
this.instanceName = UtilAll.getPid() + "#" + System.nanoTime();
}
}
然后,根据ClientID去获取MQClientInstance,不存在会创建新的客户端实例,将ProducerGroup注册到MQClientInstance中。
this.mQClientFactory = MQClientManager.getInstance().getOrCreateMQClientInstance(this.defaultMQProducer, rpcHook);
mQClientFactory.registerProducer(this.defaultMQProducer.getProducerGroup(), this);
注册成功后,就可以启动MQClientInstance了。
4.一个MQClientInstance代表一个客户端实例,它内部维护了一个Netty客户端NettyRemotingClient用来和服务端通信,该对象较重,避免频繁创建。 MQClientInstance的启动过程,主要做了以下事情:
- 发请求获取NameServerAddr
- 启动Netty客户端
- 启动各种定时任务
- 启动消息拉去服务(消费者)
- 启动负载均衡服务(消费者)
首先,如果你没有手动设置NameServerAddr,客户端会尝试读取环境变量rocketmq.namesrv.domain,它的期望值是一个URL链接,客户端会每2分钟发一次HTTP请求,并更新NameServerAddr。为什么要这么做呢?硬编码写死的NameServerAddr不够灵活,因为NameServer很可能是集群环境,具体有几台机器,机器的IP都是不固定的,你可以通过配置中心灵活的下发这些数据。
if (null == this.clientConfig.getNamesrvAddr()) {
this.mQClientAPIImpl.fetchNameServerAddr();
}
有了NameServerAddr,就可以启动Netty客户端进行通信了,MQClientAPIImpl内置了NettyRemotingClient。
this.mQClientAPIImpl.start();
Netty客户端启动后,就可以和服务端通信了,包括:从NameServer拉取Topic路由信息、发送心跳到Broker等,这些都是通过定时任务的方式进行的,所以Producer会开始启动各种定时任务。
this.startScheduledTask();
5.启动各种定时任务,如下: 每两分钟更新一次NameServerAddr:
if (null == this.clientConfig.getNamesrvAddr()) {
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
MQClientInstance.this.mQClientAPIImpl.fetchNameServerAddr();
} catch (Exception e) {
log.error("ScheduledTask fetchNameServerAddr exception", e);
}
}
}, 1000 * 10, 1000 * 60 * 2, TimeUnit.MILLISECONDS);
}
定时从NameServer更新Topic路由信息:
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
MQClientInstance.this.updateTopicRouteInfoFromNameServer();
} catch (Exception e) {
log.error("ScheduledTask updateTopicRouteInfoFromNameServer exception", e);
}
}
}, 10, this.clientConfig.getPollNameServerInterval(), TimeUnit.MILLISECONDS);
清理下线的Broker、以及发心跳:
this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
try {
MQClientInstance.this.cleanOfflineBroker();
MQClientInstance.this.sendHeartbeatToAllBrokerWithLock();
} catch (Exception e) {
log.error("ScheduledTask sendHeartbeatToAllBroker exception", e);
}
}
}, 1000, this.clientConfig.getHeartbeatBrokerInterval(), TimeUnit.MILLISECONDS);
6.Netty客户端启动,定时任务启动,最后Producer会向所有Broker主动发送一次心跳,发送心跳之后,如果是消费者,还会上传FilterClassSource到Broker,由Broker来执行消息过滤逻辑。
public void sendHeartbeatToAllBrokerWithLock() {
if (this.lockHeartbeat.tryLock()) {
try {
// 发送心跳给所有的Broker
this.sendHeartbeatToAllBroker();
// 消费者:上传FilterClassSource,Broker消息过滤用的
this.uploadFilterClassSource();
} catch (final Exception e) {
log.error("sendHeartbeatToAllBroker exception", e);
} finally {
this.lockHeartbeat.unlock();
}
} else {
log.warn("lock heartBeat, but failed. [{}]", this.clientId);
}
}
至此,服务就启动完成了。
2.2 消息发送
Producer消息发送有三种方式:同步发送、异步发送、单向发送。同步发送是阻塞的,需要等待客户端响应ACK才会继续往下走。异步发送是非阻塞的,发送时注册一个回调函数,消息发送成功/失败会执行回调。单向发送只管把请求发出去,不在乎发送是否成功,速度是最快的,在微妙级。
这里暂且只讨论同步发送,对应的API如下:
Message msg = new Message("Topic Name", "message body".getBytes());
SendResult result = producer.send(msg);
Producer消息发送过程可以拆分为三层:接口层、核心消息处理层、网络传输层。客户端调用的API就是接口层,非常的简单,但是后续Producer还要对消息做一系列的处理,最终通过Netty发送到服务端。
1.接口层没什么可看的,就是调用RocketMQ提供的API,直接看DefaultMQProducerImpl的sendDefaultImpl方法。它主要做了以下事:
- 校验消息
- 获取Topic发布信息
- 处理消息重试
首先对发送的消息进行校验,Topic必须合法,不允许发送到系统预定义的Topic里,消息Body不能为空,空消息对于RocketMQ而言是没有意义的。
Validators.checkMessage(msg, this.defaultMQProducer);
消息校验完毕后就可以发了,但是该发到哪里呢?
Producer会从缓存中读取Topic对应的TopicPublishInfo,它是Topic的发布信息,包含了Topic下有哪些队列,以及Topic的路由信息。如果缓存没有数据,会请求NameServer拉取。
private TopicPublishInfo tryToFindTopicPublishInfo(final String topic) {
// 先从本地缓存获取
TopicPublishInfo topicPublishInfo = this.topicPublishInfoTable.get(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 {
this.mQClientFactory.updateTopicRouteInfoFromNameServer(topic, true, this.defaultMQProducer);
topicPublishInfo = this.topicPublishInfoTable.get(topic);
return topicPublishInfo;
}
}
获取到TopicPublishInfo,就可以发送了,首先会计算重试次数,如果是同步发送,会重试2次,单向和异步这里不重试。
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
使用String数组记录发送失败的Broker,重试的时候跳过这些Broker。
String[] brokersSent = new String[timesTotal];
为了提高消息生产和消费的效率,也为了消息可以分片存储,单个Topic下是可以有多个消息队列MessageQueue的,那消息该发往哪个MessageQueue呢?
Producer会调用selectOneMessageQueue方法,从列表中选择一个MessageQueue,默认是轮询算法。
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
选中的MessageQueue包含了队列存储在哪个Broker上,它的QueueID是多少。
// 所属Topic
private String topic;
// 存储的Broker
private String brokerName;
// 队列ID
private int queueId;
有了MessageQueue,Producer就知道该把消息发到哪个Broker上了,接下来会调用消息发送的核心方法sendKernelImpl进行发送。
2.sendKernelImpl是消息发送的核心方法,主要做了如下事情:
- 根据BrokerName找到Master地址
- 消息压缩处理
- 设置消息sysFlag
- 执行钩子函数
- 构建Header,调用API发送
根据MessageQueue所在的BrokerName找到Broker主机地址,Broker是可以部署集群的,但是消息发送只会往Master发,所以必须找到brokerId=0的机器。
public String findBrokerAddressInPublish(final String brokerName) {
HashMap<Long/* brokerId */, String/* address */> map = this.brokerAddrTable.get(brokerName);
if (map != null && !map.isEmpty()) {
// 查找brokerId为0的Broker地址
return map.get(MixAll.MASTER_ID);
}
return null;
}
如果本地缓存没有找到,会从NameServer拉取。
if (null == brokerAddr) {
// 缓存没找到,尝试从NameServer拉取
tryToFindTopicPublishInfo(mq.getTopic());
brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
}
有了Broker主机地址,就可以发送了,但是发送前,Producer会做一系列处理。例如,调用tryToCompressMessage方法尝试对消息Body进行压缩传输,节省带宽。使用Zip压缩算法,压缩的前提是Body大于4KB。如果Body压缩了,必须让Broker知道啊,消息存储的时候必须解压才行,否则消息就不可读了。如何让Broker知道呢?Producer会使用消息的sysFlag属性进行标记,每一个Bit位的标记都有不同含义,第1位代表消息Body是否被压缩。
if (this.tryToCompressMessage(msg)) {
// 尝试压缩消息体,如果压缩了,将sysFlag第一位置为1
sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
msgBodyCompressed = true;
}
第3位代表是否是事务消息,如果是就置为1。
final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
}
消息处理完毕后,准备发送,发送前执行前置钩子函数,这里就不看了。然后重点来了,创建请求头对象,发送消息对应的请求头是SendMessageRequestHeader,它会告诉Broker消息由谁生产的,属于哪个Topic,消息有哪些属性等等。
public class SendMessageRequestHeader implements CommandCustomHeader {
@CFNotNull
// 消息来自哪个生产者组?
private String producerGroup;
@CFNotNull
// 消息所属Topic
private String topic;
@CFNotNull
// 默认Topic
private String defaultTopic;
@CFNotNull
// 默认Queue数量
private Integer defaultTopicQueueNums;
@CFNotNull
// 消息要发送到MessageQueue的ID
private Integer queueId;
@CFNotNull
/**
* 系统标记,按Bit位标识
* 第1位 代表Body是否压缩
*/
private Integer sysFlag;
@CFNotNull
// 消息的创建时间
private Long bornTimestamp;
@CFNotNull
// 消息Flag
private Integer flag;
@CFNullable
// 自定义属性,HashMap拼接成字符串
private String properties;
@CFNullable
// 重复消费次数
private Integer reconsumeTimes;
@CFNullable
//
private boolean unitMode = false;
@CFNullable
// 是否批量消息
private boolean batch = false;
// 最大重复消费次数
private Integer maxReconsumeTimes;
}
请求头设置好了,就可以创建RemotingCommand对象了。RemotingCommand是RocketMQ的通信协议对象,不管是请求还是响应,通过Netty传输的都是RemotingCommand序列化后的字节序列。通过RequestCode和RequestHeader就可以快速创建,RequestCode对应请求码,告诉服务端自己要做什么请求,RequestHeader相当于请求参数,消息发送对应的RequestCode是SEND_MESSAGE。
RemotingCommand request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
消息发送的RequestCode还有一个优化的V2版本,它只不过是简化了RequestHeader的属性名,这样FastJson的性能会更好一些。
RemotingCommand创建好了以后,就可以通过Netty将它发送给Broker了,对应的方法为invokeSyncImpl,在网络请求前后,会调用RPC的钩子函数。
doBeforeRpcHooks(addr, request);
RemotingCommand response = this.invokeSyncImpl(channel, request, timeoutMillis - costTime);
doAfterRpcHooks(RemotingHelper.parseChannelRemoteAddr(channel), request, response);
invokeSyncImpl最终就是将RemotingCommand通过Channel发送到服务端,然后注册一个回调,Broker响应后将结果写回ResponseFuture。
channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture f) throws Exception {
if (f.isSuccess()) {
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.");
}
});
至此,消息发送整个流程就结束了。
3. 总结
DefaultMQProducer是RocketMQ提供的生产者外观类,核心实现是DefaultMQProducerImpl,这么做的目的是可以随时更换生产者实现,让用户无感知。生产者要和Broker通信,因此它也是一个Netty客户端,生产者启动的同时也会启动Netty客户端,然后通过一系列的定时任务去从NameServer拉取消息,以及和Broker发送心跳。
消息发送时,Producer会先对消息做前置校验,校验通过后去NameServer查找Topic对应的路由信息,然后从众多MessageQueue中轮询出一个,根据MessageQueue对应的BrokerName查找Master主机地址,构建请求头SendMessageRequestHeader和RemotingCommand对象,通过Netty将请求发送给Broker。