RocketMQ源码阅读
RocketMQ中的领域概念,RTFM!!!!
NameServer篇
作为一个MQ的设计者,如何路由用户的消息
项目中肯定不止一台MQ broker在服务,如果生产者和我一样路痴,它会迷路的!如何路由生产者消息?
我们貌似也需要一个类似注册中心的东西,它需要
- 记录每个broker上的topic
- 了解各个broker的状态
- 负载均衡,如果有多个可以投递的broker,选择最优者
所以MQ系统中肯定有一种用于在集群中路由消息的角色,在RocketMQ中,这玩意儿就是NameServer
MQ | 解决办法 |
---|---|
RabbitMQ | 抽象出Exchanger用于消息路由,集群设备间共享Exchanger元数据,集群内部的机器根据共享的元数据可以帮助路由消息。客户端持有所有MQ实例地址。 |
Kafka | 之前使用Zookeeper来管理集群broker信息,后面我也不知道了… |
RocketMQ | NameServer服务器 |
你会怎么设计Namesrv?
现在MQ里有了这些组件,快给他们连上线吧!它们至少要按如下方式协作
- Broker启动时肯定要和Namesrv上报自己的topic信息
- Producer以及Consumer肯定要从Namesrv拉取各个broker中的topic信息,知道自己该把消息投送到哪
- Namesrv不应该是单体的,它存在的目的就是提供单体broker无法提供的高可用和高负载,它不应该成为瓶颈
如何协作才合理?
- Producer和Consumer不可能每条消息发送前都找Namesrv拿最新的信息吧,应该以固定的频率(30s)
- 这样一来Producer肯定也得知道Broker的状态吧,它是不是已经死了
- Broker肯定也要按固定时间向Namesrv上报自己的最新状态(30s)
- Namesrv之间的数据暂时不一致也不影响啥吧,它们甚至没必要相互通信同步数据,Producer拿到失效的数据导致投送失败,再试一次别的呗
在MQ的设计里,broker和namesrv没必要(有些时候也可以说是没办法)解决所有问题,可以适当摆烂,并把问题推给Producer和Consumer(在SDK中提供)
可以适当看点Namesrv的代码了
rocketmq中和NameServer相关的代码都在namesrv
这个模块里,它的代码很简单
看下主类有的方法,大概知道有做一些命令行参数和配置文件的解析,以及NameServer服务器的启停,而且用到了Netty:
这个类没啥可看的,不是核心逻辑,看到NamesrvController
:
// 初始化流程
public boolean initialize() {
loadConfig(); // 加载配置
initiateNetworkComponents(); // 初始化网络组件
initiateThreadExecutors(); // 初始化线程执行器(线程池)
registerProcessor(); // 注册处理器
startScheduleService(); // 开始定时任务
initiateSslContext(); // 初始化SSL上下文
initiateRpcHooks(); // 初始化Rpc钩子
return true;
}
// 注册处理器
private void registerProcessor() {
// 如果开启了集群测试功能,注册对应的processor
if (namesrvConfig.isClusterTest()) {
this.remotingServer.registerDefaultProcessor(new ClusterTestRequestProcessor(this, namesrvConfig.getProductEnvName()), this.defaultExecutor);
} else {
// 注册客户端请求处理器来处理GET_ROUTEINTO_BY_TOPIC请求
ClientRequestProcessor clientRequestProcessor = new ClientRequestProcessor(this);
this.remotingServer.registerProcessor(RequestCode.GET_ROUTEINFO_BY_TOPIC, clientRequestProcessor, this.clientRequestExecutor);
// 注册默认的请求处理器
this.remotingServer.registerDefaultProcessor(new DefaultRequestProcessor(this), this.defaultExecutor);
}
}
看代码只看自己目前关注的,所以只需要注意到
ClientRequestProcessor
用来处理topic路由,DefaultRequestProcessor
处理其它到Namesrv的请求即可,RequestCode
中可以查看所有的请求类型。
private void startScheduleService() {
// 扫描非活跃broker的定时任务
this.scanExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker,
5, this.namesrvConfig.getScanNotActiveBrokerInterval(), TimeUnit.MILLISECONDS);
// maybe周期性打印kv配置的定时任务?
this.scheduledExecutorService.scheduleAtFixedRate(NamesrvController.this.kvConfigManager::printAllPeriodically,
1, 10, TimeUnit.MINUTES);
// maybe周期性打印水位信息的定时任务?
this.scheduledExecutorService.scheduleAtFixedRate(() -> {
try {
NamesrvController.this.printWaterMark();
} catch (Throwable e) {
LOGGER.error("printWaterMark error.", e);
}
}, 10, 1, TimeUnit.SECONDS);
}
这里有一个需要关注的:扫描非活跃broker的方法:routeInfoManager::scanNotActiveBroker
,它会被一个周期调度的Executor定时执行。
这里我们就针对处理用户的路由请求以及扫描非活跃broker两个点来阅读代码,在DefaultRequestProcessor
中的请求处理方法processRequest
里有针对各种类型消息的处理函数调用,和我们要研究的功能相关的大概有下面几个:
case RequestCode.REGISTER_BROKER:
return this.registerBroker(ctx, request);
case RequestCode.UNREGISTER_BROKER:
return this.unregisterBroker(ctx, request);
case RequestCode.BROKER_HEARTBEAT:
return this.brokerHeartbeat(ctx, request);
case RequestCode.GET_BROKER_MEMBER_GROUP:
return this.getBrokerMemberGroup(ctx, request);
case RequestCode.GET_BROKER_CLUSTER_INFO:
return this.getBrokerClusterInfo(ctx, request);
case RequestCode.WIPE_WRITE_PERM_OF_BROKER:
return this.wipeWritePermOfBroker(ctx, request);
case RequestCode.ADD_WRITE_PERM_OF_BROKER:
return this.addWritePermOfBroker(ctx, request);
case RequestCode.GET_ALL_TOPIC_LIST_FROM_NAMESERVER:
return this.getAllTopicListFromNameserver(ctx, request);
case RequestCode.DELETE_TOPIC_IN_NAMESRV:
return this.deleteTopicInNamesrv(ctx, request);
case RequestCode.REGISTER_TOPIC_IN_NAMESRV:
return this.registerTopicToNamesrv(ctx, request);
BROKER_HEARTBEAT
请求是broker会给namesrv按频率发送的心跳请求,namesrv可以根据此来维护一个brokerLiveTable
public RemotingCommand brokerHeartbeat(ChannelHandlerContext ctx,
RemotingCommand request) throws RemotingCommandException {
final RemotingCommand response = RemotingCommand.createResponseCommand(null);
final BrokerHeartbeatRequestHeader requestHeader =
(BrokerHeartbeatRequestHeader) request.decodeCommandCustomHeader(BrokerHeartbeatRequestHeader.class);
// RouteInfoManager中的brokerLiveTable维护了
// broker的存活状态,这里更新brokerLiveTable中的broker信息
this.namesrvController.getRouteInfoManager().updateBrokerInfoUpdateTimestamp(requestHeader.getClusterName(), requestHeader.getBrokerAddr());
response.setCode(ResponseCode.SUCCESS);
response.setRemark(null);
return response;
}
// RouteInfoManager,实际上就是设置最后更新的时间戳
public void updateBrokerInfoUpdateTimestamp(final String clusterName, final String brokerAddr) {
BrokerAddrInfo addrInfo = new BrokerAddrInfo(clusterName, brokerAddr);
BrokerLiveInfo prev = this.brokerLiveTable.get(addrInfo);
if (prev != null) {
prev.setLastUpdateTimestamp(System.currentTimeMillis());
}
}
REGIST_TOPIC_TO_NAMESRV
是broker向mq注册自己的topic的请求,和heartbeat差不多,都是委托namesrvController
的RouteInfoManager
去维护实际内容,所以直接进入RouteInfoManager
,其中维护了一个topicQueueTable
保存了如下二级映射,Topic名→{brokername→queuedata}。用人话说就是每一个Topic下有一些队列,但是这些队列可能来自不同的broker,所以是一个broker到queuedata的映射。
// 该方法拿到的是topic名和所有要注册到其名下的数据
public void registerTopic(final String topic, List<QueueData> queueDatas) {
if (queueDatas == null || queueDatas.isEmpty()) {
return;
}
//
try {
// 上写锁
this.lock.writeLock().lockInterruptibly();
// 若已经有该topic的数据了
if (this.topicQueueTable.containsKey(topic)) {
Map<String, QueueData> queueDataMap = this.topicQueueTable.get(topic);
for (QueueData queueData : queueDatas) {
// 如果该broker已经注册过
if (!this.brokerAddrTable.containsKey(queueData.getBrokerName())) {
log.warn("Register topic contains illegal broker, {}, {}", topic, queueData);
return;
}
// 向queueDataMap中插入queueData
queueDataMap.put(queueData.getBrokerName(), queueData);
}
log.info("Topic route already exist.{}, {}", topic, this.topicQueueTable.get(topic));
} else {
// 构造queueDataMap
Map<String, QueueData> queueDataMap = new HashMap<>();
for (QueueData queueData : queueDatas) {
if (!this.brokerAddrTable.containsKey(queueData.getBrokerName())) {
log.warn("Register topic contains illegal broker, {}, {}", topic, queueData);
return;
}
queueDataMap.put(queueData.getBrokerName(), queueData);
}
// 向topicQueueTable中新增
this.topicQueueTable.put(topic, queueDataMap);
log.info("Register topic route:{}, {}", topic, queueDatas);
}
} catch (Exception e) {
log.error("registerTopic Exception", e);
} finally {
this.lock.writeLock().unlock();
}
}
GET_ALL_TOPIC_LIST_FROM_NAMESERVER
是客户端向namesrv发起的请求,用于获取topic列表(也是只看RouteInfoManager
的:
**public TopicList getAllTopicList() {
TopicList topicList = new TopicList();
try {
this.lock.readLock().lockInterruptibly();
topicList.getTopicList().addAll(this.topicQueueTable.keySet());
} catch (Exception e) {
log.error("getAllTopicList Exception", e);
} finally {
this.lock.readLock().unlock();
}
return topicList;
}**
从代码中可以看出,rocketmq维护了几张表来做这些工作:
- brokerAddrTable:broker的地址到broker的一些元信息的映射
- brokerLiveTable:broker存活信息
- topicQueueTable:主题到队列数据的映射
还没有看如何断定一个broker非活跃呢,现在只是把这些数据注册了进去。在最开始的时候,NamesrvController
里注册了三个定时任务,其中的第一个是:
private void startScheduleService() {
// 扫描非活跃broker的定时任务
this.scanExecutorService.scheduleAtFixedRate(NamesrvController.this.routeInfoManager::scanNotActiveBroker,
5, this.namesrvConfig.getScanNotActiveBrokerInterval(), TimeUnit.MILLISECONDS);
}
该任务会周期性扫描brokerLiveTable
,然后判断它上一次heartbeat时间与当前时间的差是否大于设置好的间隔时间,如果是,就destroy它的channel:
public void scanNotActiveBroker() {
try {
log.info("start scanNotActiveBroker");
for (Entry<BrokerAddrInfo, BrokerLiveInfo> next : this.brokerLiveTable.entrySet()) {
long last = next.getValue().getLastUpdateTimestamp();
long timeoutMillis = next.getValue().getHeartbeatTimeoutMillis();
if ((last + timeoutMillis) < System.currentTimeMillis()) {
RemotingHelper.closeChannel(next.getValue().getChannel());
log.warn("The broker channel expired, {} {}ms", next.getKey(), timeoutMillis);
this.onChannelDestroy(next.getKey());
}
}
} catch (Exception e) {
log.error("scanNotActiveBroker exception", e);
}
}
onChannelDestroy
会给BatchUnregistrationService
提交一个UnRegisterBroker请求,这个类管理所有的这种请求,包括broker主动的unregist。该类维护了一个阻塞队列,然后用一个线程不断的从阻塞队列中拿Unregist请求,调用RouteInfoManager
进行解注册
@Override
public void run() {
while (!this.isStopped()) {
try {
final UnRegisterBrokerRequestHeader request = unregistrationQueue.take();
Set<UnRegisterBrokerRequestHeader> unregistrationRequests = new HashSet<>();
unregistrationQueue.drainTo(unregistrationRequests);
// Add polled request
unregistrationRequests.add(request);
this.routeInfoManager.unRegisterBroker(unregistrationRequests);
} catch (Throwable e) {
log.error("Handle unregister broker request failed", e);
}
}
关于为啥这里非要一个阻塞队列,我想该类的注释中的这句话就是原因:BatchUnregistrationServer provides a mechanism to unregister brokers in batch manner, which speeds up broker-offline process.
在RouteInfoManager
中,解注册的代码有点复杂,但总体来说就是将它维护的那几张表里关于该broker的信息移除掉,所以我就不继续分析了。
gracefully close
通过给runtime添加shutdownhook,实现优雅关闭
public static NamesrvController start(final NamesrvController controller) throws Exception {
// 省略...
Runtime.getRuntime().addShutdownHook(new ShutdownHookThread(log, (Callable<Void>) () -> {
controller.shutdown();
return null;
}));
controller.start();
return controller;
}
小结
- broker启动时会向namesrv发送一个registBroker消息,携带了它的topic信息,namesrv会向brokerAddrTable、brokerLiveTable、topicQueueTable等表中注册broker的信息
- broker每隔一段时间会向namesrv发送一个heartbeat消息,namesrv会更新brokerLiveTable(最近更新时间戳)
- namesrv中有一个定时任务(10秒一次),扫描brokerLiveTable中的数据,发现没有及时heartbeat的broker,unregist它
- producer每隔一段时间会向namesrv发送getAllTopic请求获取所有topic列表
Producer篇
你希望Producer如何发送消息?
-
消息发送可能因为如下原因失败,我希望无论如何我的消息都不会丢失,不会被发送多次:
- 选择的broker死了
- 发送时网络中的某一环节出了问题
- broker已经接到消息,但响应时网络出了问题,producer没接到成功响应,可能会重复发送消息,造成重复消费风险
对于ab理想情况下,producer应该有重试机制,确保当ab发生时消息也可以被发送。c怎么解决?
如果MQ是单体的,只有一个broker,那就只需要给消息加一个唯一id即可,broker发现这条消息已经接受过了,就会知道这是上一次我给producer返回的“发送成功”没被接到,然后再给producer返回一个“发送成功”并忽略这条冗余消息即可(
回到了网络课),但在集群环境中,producer第一次给一个broker发送消息,无响应,它重试时几乎不会再选择该broker发送,它会投递到其他broker,对于其它broker来说,这个id是新的,这样一个消息就在mq集群中重复了。所以指望MQ来解决这个问题几乎没啥可能,如果需要一个精确消费的语义,一般都是在业务上做。 -
有时我希望消息的实时性,所以我想立即发送消息
-
有时我希望消息的吞吐量,所以我想批量发送消息
可以看一点Producer的代码了
producer家族的结构如下:
MQProducer有如下行为:
MQAdmin有如下行为:
所以,大体上来看,Producer:
- 有自己的生命周期(start、shutdown)
- 可以以多种形式发送消息(同步、异步、oneway、批量发送、指定mq发送)
- request好像是给rpc用的,我不理解是什么样的rpc
- 可以获取一个主题下的消息队列列表
- 具备创建主题、获取offset等mq管理功能
我们将主要精力放在
DefaultMQProducer
的同步消息发送上
MQProducer
并没有直接实现和broker通信的逻辑,而是委托DefaultMQImpl
,DefaultMQImpl
又分别委托MQClientAPI
来做发消息的工作,委托MQAdminImpl
来做MQ管理相关的工作
- MQClientAPIImpl:封装了到broker的rpc调用,它不关心如何选择broker,不关心失败重试等各种机制,只用来rpc调用
- DefaultMQProducerImpl:封装消息发送的主要逻辑,如失败重试
sendMessage的核心:RPC调用
MQClientAPIImpl
中有一个核心方法,所有发送消息的工作最终都会走到该方法,它封装了不同形式的消息对broker的rpc调用:
public SendResult sendMessage(
final String addr,
final String brokerName,
final Message msg,
final SendMessageRequestHeader requestHeader,
final long timeoutMillis,
final CommunicationMode communicationMode,
final SendCallback sendCallback,
final TopicPublishInfo topicPublishInfo,
final MQClientInstance instance,
final int retryTimesWhenSendFailed,
final SendMessageContext context,
final DefaultMQProducerImpl producer
) throws RemotingException, MQBrokerException, InterruptedException {
long beginStartTime = System.currentTimeMillis();
// 获取消息的一些属性
RemotingCommand request = null;
String msgType = msg.getProperty(MessageConst.PROPERTY_MESSAGE_TYPE);
boolean isReply = msgType != null && msgType.equals(MixAll.REPLY_MESSAGE_FLAG);
// 根据是否是回复消息、是否是批量消息、是否是智能消息,创建不同的request对象
if (isReply) {
if (sendSmartMsg) {
SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader);
request = RemotingCommand.createRequestCommand(RequestCode.SEND_REPLY_MESSAGE_V2, requestHeaderV2);
} else {
request = RemotingCommand.createRequestCommand(RequestCode.SEND_REPLY_MESSAGE, requestHeader);
}
} else {
if (sendSmartMsg || msg instanceof MessageBatch) {
SendMessageRequestHeaderV2 requestHeaderV2 = SendMessageRequestHeaderV2.createSendMessageRequestHeaderV2(requestHeader);
request = RemotingCommand.createRequestCommand(msg instanceof MessageBatch ? RequestCode.SEND_BATCH_MESSAGE : RequestCode.SEND_MESSAGE_V2, requestHeaderV2);
} else {
request = RemotingCommand.createRequestCommand(RequestCode.SEND_MESSAGE, requestHeader);
}
}
request.setBody(msg.getBody());
// 根据通信模式,调用不同的接口
switch (communicationMode) {
case ONEWAY:
// 如果是oneway,就是发完后啥也不用管,直接rpc调用
this.remotingClient.invokeOneway(addr, request, timeoutMillis);
return null;
case ASYNC:
// 异步消息,先检测刚刚的初始化过程是否已经导致消息超时,如果超时,就抛出异常
final AtomicInteger times = new AtomicInteger();
long costTimeAsync = System.currentTimeMillis() - beginStartTime;
if (timeoutMillis < costTimeAsync) {
throw new RemotingTooMuchRequestException("sendMessage call timeout");
}
// 调用sendMessageAsync
this.sendMessageAsync(addr, brokerName, msg, timeoutMillis - costTimeAsync, request, sendCallback, topicPublishInfo, instance,
retryTimesWhenSendFailed, times, context, producer);
return null;
case SYNC:
// 同步消息,也先检测超时
long costTimeSync = System.currentTimeMillis() - beginStartTime;
if (timeoutMillis < costTimeSync) {
throw new RemotingTooMuchRequestException("sendMessage call timeout");
}
// 调用sendMessageSync
return this.sendMessageSync(addr, brokerName, msg, timeoutMillis - costTimeSync, request);
default:
assert false;
break;
}
return null;
}
oneway消息不关心结果,所以返回null;异步消息通过SendCallback
返回结果,同步消息返回sendMessageSync
调用的结果,同步消息的代码如下:
private SendResult sendMessageSync(
final String addr,
final String brokerName,
final Message msg,
final long timeoutMillis,
final RemotingCommand request
) throws RemotingException, MQBrokerException, InterruptedException {
// 到broker的rpc调用
RemotingCommand response = this.remotingClient.invokeSync(addr, request, timeoutMillis);
// 响应必须不为null
assert response != null;
// 对响应进行处理,并返回给调用者
return this.processSendResponse(brokerName, msg, response, addr);
}
重试机制
DefaultMQProducerImpl
中封装了消息发送的重试机制:
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;
// 同步消息 timesTotal=1+重试次数次
// 其它消息 timesTotal=1
// 这不代表异步消息没有重试,它只是不在这个循环里重试
int timesTotal = communicationMode == CommunicationMode.SYNC ? 1 + this.defaultMQProducer.getRetryTimesWhenSendFailed() : 1;
int times = 0;
// 每次尝试发送的broker列表
String[] brokersSent = new String[timesTotal];
for (; times < timesTotal; times++) {
// 上次发送的broker
String lastBrokerName = null == mq ? null : mq.getBrokerName();
// 选择一个消息队列(传入上一次的broker名,因为上一次发送失败这次大概率还是失败,避免选择到它)
MessageQueue mqSelected = this.selectOneMessageQueue(topicPublishInfo, lastBrokerName);
if (mqSelected != null) {
mq = mqSelected;
// 填充broker列表
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();
// 更新FaultItem
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 | MQClientException e) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
log.warn("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq, e);
if (log.isDebugEnabled()) {
log.debug(msg.toString());
}
exception = e;
continue;
} catch (MQBrokerException e) {
endTimestamp = System.currentTimeMillis();
this.updateFaultItem(mq.getBrokerName(), endTimestamp - beginTimestampPrev, true);
log.warn("sendKernelImpl exception, resend at once, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq, e);
if (log.isDebugEnabled()) {
log.debug(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("sendKernelImpl exception, throw exception, InvokeID: %s, RT: %sms, Broker: %s", invokeID, endTimestamp - beginTimestampPrev, mq, e);
if (log.isDebugEnabled()) {
log.debug(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);
// 同步发送异常,根据exception封装异常代码
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
怎么选择要发送的消息队列?
下面是selectOneMessageQueue
的部分代码,我们分两个阶段看,第一个阶段是先从所有该topic的消息队列中轮询选择一个,并且尽量避免选择lastBrokerName
(因为它已经失败了):
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
// 如果开启了失败延迟规避机制
if (this.sendLatencyFaultEnable) {
try {
// 阶段1:
// 先从tpInfoGetMessageQueueList轮询选择
// 不选择上一次选过的broker(因为它失败了),并且选择一个latencyFaultTolerance认为可用的broker
int index = tpInfo.getSendWhichQueue().incrementAndGet();
for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
int pos = index++ % tpInfo.getMessageQueueList().size();
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
if (!StringUtils.equals(lastBrokerName, mq.getBrokerName()) && latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
return mq;
}
}
// 省略第二阶段...
} catch (Exception e) {
log.error("Error occurred when selecting message queue", e);
}
return tpInfo.selectOneMessageQueue();
}
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
比较有趣的是,这个代码在rocketmq的早期版本(至少在4.7.0)不是这样的,而是这样的:
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);
if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
return mq;
}
}
这段轮询的最终目的就是判断上一次失败的broker是否已经available,如果是,就选中它,否则直到循环结束,进入下一阶段。认同早期写法的人们的观点是应该尽量保证各个broker的loadbalance,即如果上次失败的broker已经available,就优先选择它。
但不论如何,首先最内层的if
判断被认为是冗余代码而被删除掉了,最后逻辑也改成了尽量不选上次失败的broker,而不管它是否已经可用。所以这一段代码先后被打上冗余代码和bug的标签,即使从最开始的视角来看好像逻辑也确实没什么问题(也没什么必要)。这些争论在rocketmq官方github repo下有很多issue,这里提供其中一个。
回到主线,第一个阶段以选择一个所在broker可用的mq并规避lastBrokerName为目标,但它最终有可能以选不出mq告终,这就来到了第二个阶段。第二个阶段的代码有些看不懂,需要先来了解下LatencyFaultTolerance
的工作模式。
LatencyFaultTolerance
的默认实现中维护了这样一个Map,它是brokerName到FaultItem
的映射,而FaultItem
中包含当前延迟以及开始时间戳两个字段:
private final ConcurrentHashMap<String, FaultItem> faultItemTable = new ConcurrentHashMap<>(16);
class FaultItem implements Comparable<FaultItem> {
private final String name;
private volatile long currentLatency;
private volatile long startTimestamp;
// 当前时间-开始时间戳 >= 0 --> available
public boolean isAvailable() {
return (System.currentTimeMillis() - startTimestamp) >= 0;
}
}
根据上面的return公式,大概可以断定开始时间戳的定义是开始认为broker已经可用的时间,所以当当前时间超过这个时间就认为可用,updateFaultItem
中的逻辑也大致可以看出来,其参数notAvailableDuration
是一个外部传入的计算值,即预估多久后broker会available。
// 会在每次消息投递时,根据投递情况被调用,用于根据当前投递延迟更新一个预估的broker可用时间
@Override
public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
FaultItem old = this.faultItemTable.get(name);
if (null == old) {
final FaultItem faultItem = new FaultItem(name);
faultItem.setCurrentLatency(currentLatency);
faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
old = this.faultItemTable.putIfAbsent(name, faultItem);
if (old != null) {
old.setCurrentLatency(currentLatency);
old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
}
} else {
old.setCurrentLatency(currentLatency);
old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
}
}
先不管外面是咋计算的,只能说isAvailable()
真了,broker也不一定为真,所以第一阶段老版本那个写法还挺抽象的,它大概率会引发bug(即总向一个不可用的broker重复投递)。
回到重复投递时选择消息队列的第二阶段:
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
// 如果开启了失败延迟规避机制
if (this.sendLatencyFaultEnable) {
try{
// 省略第一阶段...
// 第二阶段
// 先让latencytFaultTolerance根据手中的各种延迟情况以及预估的可用情况
// 选出一个broker,latencyFaultTolerance的逻辑是在比较优秀的前一半里
// 随机选一个出来,这里不深入。所以这个变量叫notBestBroker,它不一定是最好的
final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
// 如果写队列数大于0
int writeQueueNums = tpInfo.getWriteQueueNumsByBroker(notBestBroker);
if (writeQueueNums > 0) {
// 使用这个notBestBroker中的一个writeQueue发送
// 这段代码写的很奇怪,让tpInfo选一个mq,然后把这个mq实体改写成notBestBroker
// 中的一个mq
// 可能是MessageQueue没有一个合适的创建方式吧,用这种方法创建一个?那原来的不就被改变了吗??
final MessageQueue mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().incrementAndGet() % writeQueueNums);
}
return mq;
} else {
// 写队列数小于等于0,在latencyFaultTolerance中移除它
latencyFaultTolerance.remove(notBestBroker);
}
} catch (Exception e) {
log.error("Error occurred when selecting message queue", e);
}
// 如果第二阶段还没选出来,就tpInfo选一个
return tpInfo.selectOneMessageQueue();
}
// 如果没开失败延迟规避,直接选一个,但规避上一次失败的
return tpInfo.selectOneMessageQueue(lastBrokerName);
}
小结
RocketMQ通过在Producer端引入重试机制来避免消息投递过程中发生问题导致投递失败的情况
批量发送消息
同样是使用消息队列,有时希望获得的是实时性,即消息从发出到被消费者消费时间越短越好,这需要发送者立即发送消息,而有时希望获得的是吞吐量,即单位时间内发送消息的数量,这可能需要发送者批量发送消息,然后将消息用一个请求送出。
批量能带来更高的吞吐量这件事,在发送小消息时尤为明显,比如10字节的消息:
- 协议栈中的各个层次都要给消息加上头,消息本身都没那些头大
- 每次请求网络都需要进行系统调用
- 对于压缩算法来说,如果你的消息只有10字节,那么他们中可压缩的数据量可能很小,如果你的消息有10KB,可能有大量的数据可以压缩(不敢确定正确,没研究过压缩算法)
- 所有因为通信不得不引入的(与发送字节数无关的)额外开销,批量都可以只耗费一次
RocketMQ支持收集消息进行批量发送以获得更高的吞吐量,它给用户的接口就是传入Message
的列表,Producer会调用batch
将这些消息组合成一个MessageBatch
:
@Override
public SendResult send(
Collection<Message> msgs) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
return this.defaultMQProducerImpl.send(batch(msgs));
}
batch
方法就是使用列表生成MessageBatch
,并对其中的每个子Message
进行校验,设置唯一ID和topic:
private MessageBatch batch(Collection<Message> msgs) throws MQClientException {
MessageBatch msgBatch;
try {
msgBatch = MessageBatch.generateFromList(msgs);
for (Message message : msgBatch) {
Validators.checkMessage(message, this);
MessageClientIDSetter.setUniqID(message);
message.setTopic(withNamespace(message.getTopic()));
}
MessageClientIDSetter.setUniqID(msgBatch);
msgBatch.setBody(msgBatch.encode());
} catch (Exception e) {
throw new MQClientException("Failed to initiate the MessageBatch", e);
}
msgBatch.setTopic(withNamespace(msgBatch.getTopic()));
return msgBatch;
}
自动batch
RocketMQ提供了autoBatch
选项,一旦开启,对于单条消息的发送,也会尝试先缓存,然后再批量发送:
public SendResult send(
Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
msg.setTopic(withNamespace(msg.getTopic()));
// 如果开启了autoBatch,并且该消息不是MessageBatch
// 如果已经是聚合消息了直接发送
if (this.getAutoBatch() && !(msg instanceof MessageBatch)) {
return sendByAccumulator(msg, null, null);
} else {
return sendDirect(msg, null, null);
}
}
如果开启了authBatch,进入的消息会走sendByAccumulator
,这里会先判断消息能不能batch,如果不能就直接发送,否则交给produceAccumulator
来发送:
public SendResult sendByAccumulator(Message msg, MessageQueue mq,
SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
// check whether it can batch
if (!canBatch(msg)) {
return sendDirect(msg, mq, sendCallback);
} else {
Validators.checkMessage(msg, this);
MessageClientIDSetter.setUniqID(msg);
if (sendCallback == null) {
return this.produceAccumulator.send(msg, mq, this);
} else {
this.produceAccumulator.send(msg, mq, sendCallback, this);
return null;
}
}
}
private boolean canBatch(Message msg) {
// 如果produceAccumulator满了,produceAccumulator有最大限制
// - private long totalHoldSize = 32 * 1024 * 1024;
// 大消息不会走批量
if (!produceAccumulator.tryAddMessage(msg)) {
return false;
}
// 延时消息不支持批处理
if (msg.getDelayTimeLevel() > 0) {
return false;
}
// retry消息不支持批处理
if (msg.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
return false;
}
// 被分配到producer group的不支持批处理
if (msg.getProperties().containsKey(MessageConst.PROPERTY_PRODUCER_GROUP)) {
return false;
}
return true;
}
produceAccumulator
的send
会根据msg和mq创建一个聚合key,根据聚合key创建或获取一个MessageAccumulation
,也就是批量消息的存储对象,并把消息添加到这个存储对象中:
SendResult send(Message msg, MessageQueue mq,
DefaultMQProducer defaultMQProducer) throws InterruptedException, MQBrokerException, RemotingException, MQClientException {
AggregateKey partitionKey = new AggregateKey(msg, mq);
while (true) {
MessageAccumulation batch = getOrCreateSyncSendBatch(partitionKey, defaultMQProducer);
int index = batch.add(msg);
if (index == -1) {
syncSendBatchs.remove(partitionKey, batch);
} else {
return batch.sendResults[index];
}
}
}
存储好的批量消息总要在某一时刻发送出去,在ProduceAccumulator
启动时,会启动两个守护线程,用于同步和异步批量消息的发送:
void start() {
guardThreadForSyncSend.start();
guardThreadForAsyncSend.start();
}
这个守护线程的目的就是定期wakeup每一个MessageAccumulation
:
private class GuardForSyncSendService extends ServiceThread {
private final String serviceName;
public GuardForSyncSendService(String clientInstanceName) {
serviceName = String.format("Client_%s_GuardForSyncSend", clientInstanceName);
}
@Override public String getServiceName() {
return serviceName;
}
@Override public void run() {
log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
this.doWork();
} catch (Exception e) {
log.warn(this.getServiceName() + " service has exception. ", e);
}
}
log.info(this.getServiceName() + " service end");
}
private void doWork() throws InterruptedException {
Collection<MessageAccumulation> values = syncSendBatchs.values();
final int sleepTime = Math.max(1, holdMs / 2);
// 唤起每一个MessageAccumulation
for (MessageAccumulation v : values) {
v.wakeup();
synchronized (v) {
synchronized (v.closed) {
if (v.messagesSize.get() == 0) {
v.closed.set(true);
syncSendBatchs.remove(v.aggregateKey, v);
} else {
v.notify();
}
}
}
}
// 睡眠一会儿
Thread.sleep(sleepTime);
}
}
而MessageAccumulation
的wakeup
方法中只会调用this.notify()
:
public synchronized void wakeup() {
if (this.closed.get()) {
return;
}
this.notify();
}
这就要看看在哪wait
了,在MessageAccumulation.add
函数中会调用wait
:
public int add(
Message msg) throws InterruptedException, MQBrokerException, RemotingException, MQClientException {
int ret = -1;
synchronized (this.closed) {
if (this.closed.get()) {
return ret;
}
ret = this.count++;
this.messages.add(msg);
messagesSize.addAndGet(msg.getBody().length);
String msgKeys = msg.getKeys();
if (msgKeys != null) {
this.keys.addAll(Arrays.asList(msgKeys.split(MessageConst.KEY_SEPARATOR)));
}
}
synchronized (this) {
while (!this.closed.get()) {
if (readyToSend()) {
this.send();
break;
} else {
this.wait();
}
}
return ret;
}
}
也就是说,自动批量发送的线程发送消息时会先挂起,直到收集到了足够量的消息,或者到达最大等待时间,才会真正发送并返回。
Broker篇
Broker要做下面的事:
- 接受Producer的消息
- 持久化消息
- 维护Topic、MQ等数据结构,使得对于消息的所有操作尽量快
- 推送消息给Consumer
本节的重心是研究RocketMQ的Broker如何持久化消息,而对于其它环节可能会弱化
如果让你来设计broker,你会如何存储消息
当接到producer的消息之后,肯定需要将其持久化到磁盘,然后再通知producer已经投递成功了,并且我会有如下期待:
- 每次持久化时的磁盘写入尽量是顺序的,以避免写入速度受影响导致broker整体接受消息缓慢
- 尽量减少每次都要通过系统调用发起磁盘写入带来的额外数据拷贝
- 我希望磁盘数据结构可以让消息的消费尽量快(RocketMQ还有一个难点是支持按tag检索消息)
- 我肯定希望有某种缓存,不让我每次都去磁盘找数据
看起来1和3好像是矛盾的,如果你希望持久化时的磁盘写入是顺序的,你就要将broker上所有消息(不论哪个topic、哪个queue)都顺序写到一起,但这貌似对于消息消费是不利的,当消费者获取一个topic下的某个消息时,总得过滤掉点儿别的
RocketMQ的文件结构
RocketMQ中主要有三种文件来支撑broker的消息持久化:
- CommitLog文件
- ConsumeQueue文件
- Index文件
.
├── commitlog
│ └── 00000000000000000000
├── consumequeue
│ └── test-topic
│ ├── 1
│ │ └── 00000000000000000000
│ └── 7
│ └── 00000000000000000000
├── index
│ └── 20230815104806838
CommitLog
由于希望Producer生产的消息都被顺序的存储起来,以避免随机磁盘IO影响性能,所以RocketMQ中所有消息都在一个大文件中,这个文件就是CommitLog
,RocketMQ中每一条消息被紧凑的保存在该文件中,如下是一个CommitLog
文件的内容:
CommitLog
中是紧凑的消息列表,每一个消息紧密的排布在文件中,没有空隙,下面是该文件中的消息格式,可以对应该表尝试手动解析消息或编写消息解析器:
PS:QueueId和QueueOffset中间有一个4字节的flag,这个表里没有
比如上面,第一条消息的长度是10B
(也就是267字节),我们可以看到在00000104
那一行,往后数到0000010B
的位置上,又出现了和文件开头类似的结构,这就是该文件中的第二条消息(如果你有兴趣甚至可以解析出消息的body)。
这里我通过编写工具,发现了这个文件中一共有4条消息,它们都属于test-topic
,两个在1号队列,两个在7号队列,并且可以看到queueOffset
的变化(同一个队列中第二条消息的offset是1),physicOffset
则记录了消息在该文件中的物理偏移量,同时我们解析出了消息内容:
CommitLog文件以1G为最大大小,当文件满1G,会切换到另一个文件中,它的文件名就是该文件中第一条消息在全局(所有文件中)的物理offset。
ConsumeQueue
上面的格式对于消息投递性能是绝佳的,但对于消费则不是,如果该文件中有多个topic(虽然目前只有一个),考虑消费者只想获取test-topic
的消息,broker需要为它过滤掉很多其它topic的消息。
ConsumeQueue文件用于解决这一问题,这些文件以topic、queue来编组,当前有一个topic,两个队列:
consumequeue
└── test-topic
├── 1
│ └── 00000000000000000000
└── 7
└── 00000000000000000000
test-topic/1
下的文件中保存了test-topic
下队列1中的消息,对于test-topic/7
也是一样的道理。
ConsumeQueue中不会保存消息的完整内容,因为没必要,对于每一个消息,它保存20字节的条目,分别为8字节的commitlog偏移量,4字节的消息长度,8字节的tag哈希码:
所以,上面的文件中,第一条消息位于commitlog
的0处,长度为10B,第二条消息位于0x0321
处,长度为0x11D
。在下图中,队列1的第一条消息在物理地址0处,而队列1的第二条消息在物理地址801处,也就是16进制的0x321
。
将该文件中的消息条目设计成固定长度的,就可以按照数组的方式索引消息条目了,只需要给出你想要第几条消息,然后x20即可。
index文件
时间原因先不考虑
那这不还是随机写入嘛
如果写一条消息必须写入ConsumeQueue的话,那CommitLog带来的优势全无,因为ConsumeQueue又让磁盘IO随机了。
但实际上我们只需要同步写入CommitLog就好了,ConsumeQueue异步写入即可,甚至先缓存然后批量写入都行,它丢了也没事,反正CommitLog中已经又了所有数据,使用CommitLog中的数据恢复就行了。ConsumeQueue更像是原始数据的索引!
道理我都懂但是我还是不想和磁盘对话啊!!!
顺序写入是从磁盘物理结构上来加速写入速度,我们还可以从另一个方面加速,即使用内存映射文件。
在Java中可以使用nio包下的FileChannel.map
来使用内存映射文件,在jdk8的solaris实现中,FileChannel.map
最终是使用mmap64
系统调用实现的:
JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
jint prot, jlong off, jlong len)
{
void *mapAddress = 0;
jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
jint fd = fdval(env, fdo);
int protections = 0;
int flags = 0;
if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
protections = PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_SHARED;
} else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
protections = PROT_WRITE | PROT_READ;
flags = MAP_PRIVATE;
}
mapAddress = mmap64(
0, /* Let OS decide location */
len, /* Number of bytes to map */
protections, /* File permissions */
flags, /* Changes are shared */
fd, /* File descriptor of mapped file */
off); /* Offset into file */
if (mapAddress == MAP_FAILED) {
if (errno == ENOMEM) {
JNU_ThrowOutOfMemoryError(env, "Map failed");
return IOS_THROWN;
}
return handle(env, -1, "Map failed");
}
return ((jlong) (unsigned long) mapAddress);
}
在虚拟内存页表的加持下,
mmap
可以把文件的一部分映射到内存中(并且是lazy的),然后用户可以通过读写内存来访问文件内容,这样就不用每次都发起磁盘IO了。但这也会造成内存和磁盘数据不一致的问题,需要根据使用场景考虑向内存写入数据时是否同步刷盘。
可以开始读点数据存储的代码了
DefaultMessageStore
是rocketmq做数据存储的核心类,下面是它的一些属性:
private final CommitLog commitLog;
private final ConsumeQueueStore consumeQueueStore;
private final FlushConsumeQueueService flushConsumeQueueService;
private final CleanCommitLogService cleanCommitLogService;
private final CleanConsumeQueueService cleanConsumeQueueService;
private final CorrectLogicOffsetService correctLogicOffsetService;
private final IndexService indexService;
private final AllocateMappedFileService allocateMappedFileService;
private ReputMessageService reputMessageService;
private HAService haService;
private CompactionStore compactionStore;
private CompactionService compactionService;
private final StoreStatsService storeStatsService;
// ...
看到都已经懵逼了,但是还是能挑出一些关键的东西的:
commitLog
:肯定对应CommitLog文件相关的操作或数据结构comsumeQueueStore
:肯定和ConsumeQueue的存储相关flushConsumeQueueService
:和刷新ConsumeQueue存储相关的服务cleanCommitLogService
:清理CommitLog的服务(可能是清除那些已经消费掉的消息?)correctLogicOffsetService
:矫正逻辑offset的服务indexService
:索引相关服务allocateMappedFileService
:分配内存映射文件的服务reputMessageService
:貌似是根据commitLog文件异步构建ConsumeQueue和Index的服务haService
:某种高可用服务?compactionStore
:压缩存储??
而该类有一些行为:
大体上能做到见名知意,这里我们主要关注putMessage
和getMessage
,它们应该对应着producer发布消息和consumer获取消息。
asyncPutMessage
由于同步putmessage也是复用了asyncPutMessage
然后wait结果,所以我们分析异步的。
由于是异步代码,所以显得很复杂,但细看只是语法上复杂,逻辑还是很简单:
@Override
public CompletableFuture<PutMessageResult> asyncPutMessage(MessageExtBrokerInner msg) {
// 调用putMessageHook,这里应该是一些扩展点,可以看出只要某个hook处理了这个消息
// 就直接返回结果,不会继续执行了。我们不关注这个扩展点
for (PutMessageHook putMessageHook : putMessageHookList) {
PutMessageResult handleResult = putMessageHook.executeBeforePutMessage(msg);
if (handleResult != null) {
return CompletableFuture.completedFuture(handleResult);
}
}
// 校验正确性
if (msg.getProperties().containsKey(MessageConst.PROPERTY_INNER_NUM)
&& !MessageSysFlag.check(msg.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)) {
LOGGER.warn("[BUG]The message had property {} but is not an inner batch", MessageConst.PROPERTY_INNER_NUM);
return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null));
}
// 校验正确性
if (MessageSysFlag.check(msg.getSysFlag(), MessageSysFlag.INNER_BATCH_FLAG)) {
Optional<TopicConfig> topicConfig = this.getTopicConfig(msg.getTopic());
if (!QueueTypeUtils.isBatchCq(topicConfig)) {
LOGGER.error("[BUG]The message is an inner batch but cq type is not batch cq");
return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null));
}
}
// 调用commitLog.asyncPutMessage,将消息加到commitLog
long beginTime = this.getSystemClock().now();
CompletableFuture<PutMessageResult> putResultFuture = this.commitLog.asyncPutMessage(msg);
// 更新storeStats,这里我们也不关注
putResultFuture.thenAccept(result -> {
long elapsedTime = this.getSystemClock().now() - beginTime;
if (elapsedTime > 500) {
LOGGER.warn("DefaultMessageStore#putMessage: CommitLog#putMessage cost {}ms, topic={}, bodyLength={}",
elapsedTime, msg.getTopic(), msg.getBody().length);
}
this.storeStatsService.setPutMessageEntireTimeMax(elapsedTime);
if (null == result || !result.isOk()) {
this.storeStatsService.getPutMessageFailedTimes().add(1);
}
});
return putResultFuture;
}
所以实际上这么长的一段代码,我们只关注其中的一行,也就是this.commitLog.asyncPutMessage(msg)
。所以说看代码的时候过滤掉那些和自己无关的path是很重要的,后面我会直接删掉不关注的代码。
下面是CommitLog.asyncPutMessages
:
public CompletableFuture<PutMessageResult> asyncPutMessages(final MessageExtBatch messageExtBatch) {
// 根据message获取bornSocketAddress和storeSocketAddress
InetSocketAddress bornSocketAddress = (InetSocketAddress) messageExtBatch.getBornHost();
if (bornSocketAddress.getAddress() instanceof Inet6Address) {
messageExtBatch.setBornHostV6Flag();
}
InetSocketAddress storeSocketAddress = (InetSocketAddress) messageExtBatch.getStoreHost();
if (storeSocketAddress.getAddress() instanceof Inet6Address) {
messageExtBatch.setStoreHostAddressV6Flag();
}
// 获取CommitLog的内存映射文件
MappedFile unlockMappedFile = null;
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
// 当前offset
long currOffset;
if (mappedFile == null) {
currOffset = 0;
} else {
currOffset = mappedFile.getFileFromOffset() + mappedFile.getWrotePosition();
}
messageExtBatch.setVersion(MessageVersion.MESSAGE_VERSION_V1);
boolean autoMessageVersionOnTopicLen =
this.defaultMessageStore.getMessageStoreConfig().isAutoMessageVersionOnTopicLen();
if (autoMessageVersionOnTopicLen && messageExtBatch.getTopic().length() > Byte.MAX_VALUE) {
messageExtBatch.setVersion(MessageVersion.MESSAGE_VERSION_V2);
}
// 这几行都是和锁相关的,不必过分理解,这里使用和topicQueue相关的细粒度锁
PutMessageThreadLocal pmThreadLocal = this.putMessageThreadLocal.get();
updateMaxMessageSize(pmThreadLocal);
MessageExtEncoder batchEncoder = pmThreadLocal.getEncoder();
String topicQueueKey = generateKey(pmThreadLocal.getKeyBuilder(), messageExtBatch);
PutMessageContext putMessageContext = new PutMessageContext(topicQueueKey);
messageExtBatch.setEncodedBuff(batchEncoder.encode(messageExtBatch, putMessageContext));
// 锁定
topicQueueLock.lock(topicQueueKey);
try {
defaultMessageStore.assignOffset(messageExtBatch);
putMessageLock.lock();
try {
long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
this.beginTimeInLock = beginLockTimestamp;
// storetimestamp
messageExtBatch.setStoreTimestamp(beginLockTimestamp);
// 如果没有mappedFile,或者满了,创建新的(getLastMappedFile会默认创建新的)
if (null == mappedFile || mappedFile.isFull()) {
mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
// 和预读相关的吧,先不管
if (isCloseReadAhead()) {
setFileReadMode(mappedFile, LibC.MADV_RANDOM);
}
}
// 如果到这mappedFile还没有,就返回错误
if (null == mappedFile) {
log.error("Create mapped file1 error, topic: {} clientAddr: {}", messageExtBatch.getTopic(), messageExtBatch.getBornHostString());
beginTimeInLock = 0;
return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.CREATE_MAPPED_FILE_FAILED, null));
}
// 添加message
result = mappedFile.appendMessages(messageExtBatch, this.appendMessageCallback, putMessageContext);
switch (result.getStatus()) {
// 成功
case PUT_OK:
break;
// 如果到达EOF,再走一遍上面的流程
case END_OF_FILE:
unlockMappedFile = mappedFile;
// Create a new file, re-write the message
mappedFile = this.mappedFileQueue.getLastMappedFile(0);
if (null == mappedFile) {
// XXX: warn and notify me
log.error("Create mapped file2 error, topic: {} clientAddr: {}", messageExtBatch.getTopic(), messageExtBatch.getBornHostString());
beginTimeInLock = 0;
return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.CREATE_MAPPED_FILE_FAILED, result));
}
if (isCloseReadAhead()) {
setFileReadMode(mappedFile, LibC.MADV_RANDOM);
}
result = mappedFile.appendMessages(messageExtBatch, this.appendMessageCallback, putMessageContext);
break;
// 出错
case MESSAGE_SIZE_EXCEEDED:
case PROPERTIES_SIZE_EXCEEDED:
beginTimeInLock = 0;
return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result));
case UNKNOWN_ERROR:
default:
beginTimeInLock = 0;
return CompletableFuture.completedFuture(new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result));
}
elapsedTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
beginTimeInLock = 0;
} finally {
putMessageLock.unlock();
}
// Increase queue offset when messages are successfully written
if (AppendMessageStatus.PUT_OK.equals(result.getStatus())) {
this.defaultMessageStore.increaseOffset(messageExtBatch, (short) putMessageContext.getBatchSize());
}
} finally {
topicQueueLock.unlock(topicQueueKey);
}
if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
}
PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);
// Statistics
storeStatsService.getSinglePutMessageTopicTimesTotal(messageExtBatch.getTopic()).add(result.getMsgNum());
storeStatsService.getSinglePutMessageTopicSizeTotal(messageExtBatch.getTopic()).add(result.getWroteBytes());
// 处理刷盘和高可用相关
return handleDiskFlushAndHA(putMessageResult, messageExtBatch, needAckNums, needHandleHA);
}
上面的代码里有一个MappedFileQueue
,它是一个内存文件映射队列,每一个MappedFile
都是一个内存映射文件。根据上面的代码逻辑,很容易映射到我们有多个CommitLog文件的概念。
每一个CommitLog文件都有1G大小,不可能把整个文件映射到内存中,只是映射实际文件的一部分,实际上这个大小由MappedFileQueue.mappedFileSize
指定。
reput commitlog
ReputMessageService
每隔1ms会调用一次doReput
@Override
public void run() {
DefaultMessageStore.LOGGER.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
try {
TimeUnit.MILLISECONDS.sleep(1);
this.doReput();
} catch (Exception e) {
DefaultMessageStore.LOGGER.warn(this.getServiceName() + " service has exception. ", e);
}
}
DefaultMessageStore.LOGGER.info(this.getServiceName() + " service end");
}
public void doReput() {
// 如果当前reput开始偏移量量小于commitlog的最小偏移量,貌似consumequeue和index已经被落下太远了
if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
LOGGER.warn("The reputFromOffset={} is smaller than minPyOffset={}, this usually indicate that the dispatch behind too much and the commitlog has expired.",
this.reputFromOffset, DefaultMessageStore.this.commitLog.getMinOffset());
// 让reputFromOffset追上
this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
}
for (boolean doNext = true; this.isCommitLogAvailable() && doNext; ) {
// 获取commitLog数据
SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
if (result == null) {
break;
}
try {
this.reputFromOffset = result.getStartOffset();
for (int readSize = 0; readSize < result.getSize() && reputFromOffset < DefaultMessageStore.this.getConfirmOffset() && doNext; ) {
// 对于每条消息,生成dispatchRequest对象,也就是分派请求对象
DispatchRequest dispatchRequest =
DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false, false);
int size = dispatchRequest.getBufferSize() == -1 ? dispatchRequest.getMsgSize() : dispatchRequest.getBufferSize();
if (reputFromOffset + size > DefaultMessageStore.this.getConfirmOffset()) {
doNext = false;
break;
}
if (dispatchRequest.isSuccess()) {
if (size > 0) {
// doDispath(分派该请求)
DefaultMessageStore.this.doDispatch(dispatchRequest);
}
}
}
} finally {
result.release();
}
}
}
上面的代码从CommitLog中获取要Reput的消息,然后每一条消息构建一个DispatchRequest,最后调用DefaultMessageStore.this.doDispatch
分派该请求,doDispatch
则是对于所有dispatcher都调用各自的dispatch:
public void doDispatch(DispatchRequest req) {
for (CommitLogDispatcher dispatcher : this.dispatcherList) {
dispatcher.dispatch(req);
}
}
这里可以看到ConsumeQueue
和Index
都有各自的Dispatcher,它们将分别接受到对于该消息的分派请求:
刷盘
在CommitLog.handleDiskFlushAndHA
里,最终调用到CommitLog.handleDiskFlush
,这里分同步刷盘和异步刷盘,累了看不动了......