1. 背景
最近一直再做一些系统上的压测,并对一些问题做了优化,从这些里面收获了一些很多好的优化经验,后续的文章都会以这方面为主。
这次打压的过程中收获比较的大的是,对RocketMq的一些优化。最开始我们公司使用的是RabbitMq,在一些流量高峰的场景下,发现队列堆积比较严重,导致RabbitMq挂了。为了应对这个场景,最终我们引入了阿里云的RocketMq,RocketMq可以处理可以处理很多消息堆积,并且服务的稳定不挂也可以由阿里云保证。引入了RocketMq了之后,的确解决了队列堆积导致消息队列宕机的问题。
本来以为使用了RocketMq之后,可以万事无忧,但是其实在打压过程中发现了不少问题,这里先提几个问题,大家带着这几个问题在文中去寻找答案:
在RocketMq中,如果消息队列发生堆积,consumer会发生什么样的影响?
在RocketMq中,普通消息和顺序消息有没有什么办法提升消息消费速度?
消息失败重试次数怎么设置较为合理?顺序消息和普通消息有不同吗?
2. 普通消息 VS 顺序消息
在RocketMq中提供了多种消息类型让我们进行配置:
普通消息:没有特殊功能的消息。
分区顺序消息:以分区纬度保持顺序进行消费的消息。
全局顺序消息:全局顺序消息可以看作是只分一个区,始终在同一个分区上进行消费。
定时/延时消息:消息可以延迟一段特定时间进行消费。
事务消息:二阶段事务消息,先进行prepare投递消息,此时不能进行消息消费,当二阶段发出commit或者rollback的时候才会进行消息的消费或者回滚。
虽然配置种类比较繁多,但是使用的还是普通消息和分区顺序消息。后续主要讲的也是这两种消息。
2.1 发送消息
2.1.1 普通消息
普通消息的发送的代码比较简单,如下所示:
public static void main(String[] args) throws MQClientException, InterruptedException {
DefaultMQProducer producer = new DefaultMQProducer("test_group_producer");
producer.setNamesrvAddr("127.0.0.1:9876");
producer.start();
Message msg =
new Message("Test_Topic", "test_tag", ("Hello World").getBytes(RemotingHelper.DEFAULT_CHARSET));
SendResult sendResult = producer.send(msg);
System.out.printf("%s%n", sendResult);
producer.shutdown();
}
其内部核心代码为:
private SendResult sendDefaultImpl(Message msg, final CommunicationMode communicationMode, final SendCallback sendCallback, final long timeout
) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
// 1. 根据 topic找到publishInfo
TopicPublishInfo topicPublishInfo = this.tryToFindTopicPublishInfo(msg.getTopic());
if (topicPublishInfo != null && topicPublishInfo.ok()) {
boolean callTimeout = false;
MessageQueue mq = null;
Exception exception = null;
SendResult sendResult = null;
// 如果是同步 就三次 否则就1次
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;
}
}
} else {
break;
}
}
// 省略
}
主要流程如下:
Step 1:根据Topic 获取TopicPublishInfo,TopicPublishInfo中有我们的Topic发布消息的信息(),这个数据先从本地获取如果本地没有,则从NameServer去拉取,并且定时每隔20s会去获取TopicPublishInfo。
Step 2:获取总共执行次数(用于重试),如果发送方式是同步,那么总共次数会有3次,其他情况只会有1次。
Step 3: 从MessageQueue中选取一个进行发送,MessageQueue的概念可以等同于Kafka的partion分区,看作发送消息的最小粒度。这个选择有两种方式:
-
根据发送延迟进行选择,如果上一次发送的Broker是可用的,则从当前Broker选择遍历循环选择一个,如果不可用那么需要选择一个延迟最低的Broker从当前Broker上选择MessageQueue。
通过轮训的方式进行选择MessageQueue。
Step 4: 将Message发送至选择出来的MessageQueue上的Broker。
Step 5: 更新Broker的延迟。
Step 6: 根据不同的发送方式来处理结果:
-
Async: 异步发送,通过callBack关心结果,所以这里不进行处理。
OneWay: 顾名思义,就是单向发送