背景
先从MessageQueue入手,即先从细节入手。
细节的话,就是从MessageQueue核心类入手。
为什么要从这个类入手?因为它包含了路由的核心字段。
说白了,路由的本质,就是写消息到哪个broker节点。
rocketmq比较特殊一点,除了要路由到哪一个broker节点,还需要知道路由到哪一个队列。因为一个broker节点包含了多个队列------本质是因为一个topic包含了多个队列,而实际生产环境,一般都是集群部署,比如2主2从,即包含了2个broker节点。那么平均下来,就是一个broker节点包含了2个队列。
MessageQueue
消息队列,包含了核心字段:
1、哪个节点
2、哪个队列
java
/**
* 作用:消息队列,主要作用是路由,不是逻辑存储,也不存储消息
*
* 只和发送者有关系,和消费者没有关系
*
* @author gzh
*/
public class MessageQueue implements Comparable<MessageQueue>, Serializable {
private String topic; //topic名字
private String brokerName; //存储节点名字。生产者可以根据名字找到存储节点。
private int queueId; //消息队列id。brokerName是哪个节点,queueId是哪个队列。
说白了,其实就是这个类,决定了消息写到哪个节点的哪个队列。
因为一个topic包含了多个队列,默认是包含4个队列。
架构图
队列长什么样子?
这个是实际的截图
队列id的值是什么?其实就是0、1、2、3。每个队列,对应一个物理文件。
架构图
一个topic,包含4个队列。每个队列,包含多个消息。
注意,前面的截图0123是在同一个broker节点,因为那是单机。实际生产环境,如果包含多个broker节点,比如包含2个broker节点,那么队列是会分布到不同的broker节点的,即每个broker节点包含2个队列,因为要负载均衡。
集群部署
每个topic,都包含了多个队列。
一般情况下,每个broker,都包含了多个队列------目的是把topic分片到不同的broker节点。
控制台看到的MessageQueue的样子
实际的发送结果,下面是发送成功的情况:
为什么要包含多个队列?
因为要负载均衡啊,如果只有一个队列,那么该topic所有的消息,都只会写在同一个broker节点,那会很要命,性能很低。所以本质其实是为了分片
,提高topic的性能。
你说是往同一个broker节点写数据快呢,还是分片到不同的broker节点快呢?答案显而易见。
队列是最小存储单元
一个队列也是对应一个具体的物理文件。
负载均衡
一般的负载均衡,只需要负载均衡到哪个节点即可。比如nginx负载均衡到哪个tomcat节点。
而rocketmq,稍微特殊一点,不仅要负载均衡到哪个broker,还要负载均衡到哪个队列。
因为一个broker一般都会包含多个队列。
哪个broker?
算法:默认是按顺序。
源码分析
入口是在发送消息
java
private SendResult sendDefaultImpl(
Message msg,
final CommunicationMode communicationMode,
final SendCallback sendCallback,
final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
this.makeSureStateOK();
Validators.checkMessage(msg, this.defaultMQProducer);
final long invokeID = random.nextLong();
long beginTimestampFirst = System.currentTimeMillis();
long beginTimestampPrev = beginTimestampFirst;
long endTimestamp = beginTimestampFirst;
//查询存储节点集合
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();
//选择一个消息队列
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;
}
//发送消息
sendResult = this.sendKernelImpl(msg, mq, communicationMode, sendCallback, topicPublishInfo, timeout - costTime);
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
switch (communicationMode) {
case ASYNC:
return null;
case ONEWAY:
return null;
case SYNC: //默认同步
if (sendResult.getSendStatus() != SendStatus.SEND_OK) {
if (this.defaultMQProducer.isRetryAnotherBrokerWhenNotStoreOK()) {
continue;
}
}
return sendResult;
default:
break;
}
} catch (RemotingException e) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
log.warn(msg.toString());
exception = e;
continue;
} catch (MQClientException e) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
log.warn(msg.toString());
exception = e;
continue;
} catch (MQBrokerException e) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
log.warn(String.format("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
log.warn(msg.toString());
exception = e;
if (this.defaultMQProducer.getRetryResponseCodes().contains(e.getResponseCode())) {
continue;
} else {
if (sendResult != null) {
return sendResult;
}
throw e;
}
} catch (InterruptedException e) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, false);
log.warn(String.format("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq), e);
log.warn(msg.toString());
log.warn("sendKernelImpl exception", e);
log.warn(msg.toString());
throw e;
}
} else {
break;
}
}
if (sendResult != null) {
return sendResult;
}
String info = String.format("Send [%d] times, still failed, cost [%d]ms, Topic: %s, BrokersSent: %s",
times,
System.currentTimeMillis() - beginTimestampFirst,
msg.getTopic(),
Arrays.toString(brokersSent));
info += FAQUrl.suggestTodo(FAQUrl.SEND_MSG_FAILED);
MQClientException mqClientException = new MQClientException(info, exception);
if (callTimeout) {
throw new RemotingTooMuchRequestException("sendDefaultImpl call timeout");
}
if (exception instanceof MQBrokerException) {
mqClientException.setResponseCode(((MQBrokerException) exception).getResponseCode());
} else if (exception instanceof RemotingConnectException) {
mqClientException.setResponseCode(ClientErrorCode.CONNECT_BROKER_EXCEPTION);
} else if (exception instanceof RemotingTimeoutException) {
mqClientException.setResponseCode(ClientErrorCode.ACCESS_BROKER_TIMEOUT);
} else if (exception instanceof MQClientException) {
mqClientException.setResponseCode(ClientErrorCode.BROKER_NOT_EXIST_EXCEPTION);
}
throw mqClientException;
}
validateNameServerSetting();
throw new MQClientException("No route info of this topic: " + msg.getTopic() + FAQUrl.suggestTodo(FAQUrl.NO_TOPIC_ROUTE_INFO),
null).setResponseCode(ClientErrorCode.NOT_FOUND_TOPIC_EXCEPTION);
}
调用selectOneMessageQueue方法
java
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
return this.mqFaultStrategy.selectOneMessageQueue(tpInfo, lastBrokerName);
}
真正的路由方法
java
/**
* 选择一个消息队列:按顺序选择。和选择队列的算法一样,也是按顺序。
*
* @param tpInfo
* @param lastBrokerName
* @return org.apache.rocketmq.common.message.MessageQueue
* @author gzh
*/
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
if (this.sendLatencyFaultEnable) {
try {
// 先累加
int index = tpInfo.getSendWhichQueue().incrementAndGet();
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);
if (latencyFaultTolerance.isAvailable(mq.getBrokerName()))
return mq;
}
// 选择一个相对好的broker,并获得其对应的一个消息队列,不考虑该队列的可用性
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().incrementAndGet() % writeQueueNums);
}
return mq;
} else {
latencyFaultTolerance.remove(notBestBroker);
}
} catch (Exception e) {
log.error("Error occurred when selecting message queue", e);
}
return tpInfo.selectOneMessageQueue();
}
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
先累加的本质,其实就是按顺序。
方法调用栈
java
- MQFaultStrategy.selectOneMessageQueue(TopicPublishInfo, String) (org.apache.rocketmq.client.latency)
- DefaultMQProducerImpl.selectOneMessageQueue(TopicPublishInfo, String) (org.apache.rocketmq.client.impl.producer)
- DefaultMQProducerImpl.sendDefaultImpl(Message, CommunicationMode, SendCallback, long) (org.apache.rocketmq.client.impl.producer)
哪个队列?
算法:也是按顺序。
其实刚才上面的源码,已经回答了这个问题,因为在选择哪一个broker节点的时候,就已经选择了哪一个队列------因为消息队列包含了哪一个节点字段和哪一个队列字段。
说白了,一开始是有一个消息队列集合:
java
private List<MessageQueue> messageQueueList = new ArrayList<MessageQueue>(); //消息队列集合。一个topic包含多个消息队列,每个消息队列可能分布在不同的broker存储节点。
然后呢,根据按顺序的算法,从集合里获取一个消息队列MessageQueue。然后这个消息队列MessageQueue,就已经包含了:
1、哪个broker节点
2、哪个队列
也就是说,创建消息队列集合的时候,每个元素就已经包含了哪个broker和哪个队列。
所谓路由,无非就是按某种算法,从中取一个即可。
这就是负载均衡的本质。
总结
单机
假设是单机,那么一个topic包含4个队列。
由于是单机,所以一个broker包含了4个队列。因为这个broker是唯一的broker嘛。
这个时候的路由,就不需要路由broker了,因为只有一个broker节点。所以这个时候,只需要路由队列即可。
可以发现,broker节点是节点粒度的路由,而队列是更细粒度的路由。本质是节点粒度分片之后,再在节点内部进行更细粒度的分片------说白了,就是分片的粒度更细了,从而更高的提高性能。
集群
假设是2个broker节点,那么一个topic的4个队列,平均分配到2个broker节点------那么每个broker节点就包含了2个队列。
所以,这个时候,既需要路由broker节点,又需要路由队列。
小结
路由或者负载均衡的本质,无非就是路由到哪个节点而已。
比如数据库分库的路由,redis的路由,基本上都差不多。
无非是负载均衡的算法,稍微有点区别。有的是哈希取模,有的是按顺序,有的是随机。
而rocketmq相当于在节点分片之后,继续在节点内部进行了更细粒度的分片,即rocketmq分片的最小单位是队列,而不是节点。
这个就有点类似数据库分库之后,还进行了分表。分库是不同的节点。而分表是一个数据库节点内部包含了多个表,就相当于这里的队列。