一、阅读目标
RocketMQ 的具体消息存储结构是怎么样的呢?
如何尽量保证顺序写的呢?
二、设计理念
RocketMQ 将所有 Topic 的消息存储在同一个文件中,确保消息发送时顺序写入文件,尽最大的鞥努力确保消息发送的高性能与高吞吐量。但由于消息中间件一般是基于 Topic 的订阅机制,这样便给按照 Topic 检索消息带来了极大的不便。
为了提高消息消费的效率,RocketMQ 引入了 ConsumerQueue 消息队列文件,每个 Topic 包含多个消息消费队列,每一个消息队列有一个消息文件。IndexFile 索引文件,其主要设计理念就是为了加速消息的检索性能,根据消息的属性快速从 CommitLog 文件中检索消息。
原理图如下:
Producer、Consumer 与 CommitLog 和 ConsumerQueue的交互逻辑如下:
发送时,Producer 不直接与 ConsumerQueue 打交道。上文提到过,RocketMQ 把所有 Topic 的消息存储在一个 CommitLog 文件中,为了使消息存储不发生混乱,对 CommitLog 文件进行写之前就上锁。
消息持久被锁串行化后,对 CommitLog 文件就是顺序写,也就是常说的 Append 操作。
Broker 端的后台服务线程-ReputMessageService 会不停地分发请求并异步构建 ConsumerQueue 和 IndexFile 数据,不停的轮询,将当前的 ConsumerQueue 中的 offset 和 CommitLog 中的 offset 进行对比,将多出来的 offset 进行解析,然后放入 ConsumerQueue 中的 MappedFile 中。
消费时,Cunsumer 不直接与 CommitLog 打交道,而是从 ConsumerQueue 中拉取数据,拉取的顺序从旧到新,在 ConsumerQueue 拉取到消息的 offset后,再根据 offset 到 CommitLog 中拿到消息主体。
三、实现分析
1、RocketMQ文件存储模型层次结构
根据类别和作用从概念模型上大致划分为5层,如图所示:
1) RocketMQ业务处理器层
Broker端对消息进行读取和写入的业务逻辑入口,这一层主要包含了业务逻辑相关处理操作(根据解析RemotingCommand中的RequestCode来区分具体的业务操作类型,进而执行不同的业务处理流程),比如前置的检查和校验步骤、构造MessageExtBrokerInner对象、decode反序列化、构造Response返回对象等;
2) RocketMQ数据存储组件层
该层主要是RocketMQ的存储核心类—DefaultMessageStore,其为RocketMQ消息数据文件的访问入口,通过该类的“putMessage()”和“getMessage()”方法完成对CommitLog消息存储的日志数据文件进行读写操作(具体的读写访问操作还是依赖下一层中CommitLog对象模型提供的方法);另外,在该组件初始化时候,还会启动很多存储相关的后台服务线程,包括AllocateMappedFileService(MappedFile预分配服务线程)、ReputMessageService(回放存储消息服务线程)、HAService(Broker主从同步高可用服务线程)、StoreStatsService(消息存储统计服务线程)、IndexService(索引文件服务线程)等;
3) RocketMQ存储逻辑对象层
该层主要包含了RocketMQ数据文件存储直接相关的三个模型类IndexFile、ConsumerQueue和CommitLog。IndexFile为索引数据文件提供访问服务,ConsumerQueue为逻辑消息队列提供访问服务,CommitLog则为消息存储的日志数据文件提供访问服务。这三个模型类也是构成了RocketMQ存储层的整体结构(对于这三个模型类的深入分析将放在后续篇幅中);
4) 封装的文件内存映射层
RocketMQ主要采用JDK NIO中的MappedByteBuffer和FileChannel两种方式完成数据文件的读写。其中,采用MappedByteBuffer这种内存映射磁盘文件的方式完成对大文件的读写,在RocketMQ中将该类封装成MappedFile类。这里限制的问题在上面已经讲过;对于每类大文件(IndexFile/ConsumerQueue/CommitLog),在存储时分隔成多个固定大小的文件(单个IndexFile文件大小约为400M、单个ConsumerQueue文件大小约5.72M、单个CommitLog文件大小为1G),其中每个分隔文件的文件名为前面所有文件的字节大小数+1,即为文件的起始偏移量,从而实现了整个大文件的串联。这里,每一种类的单个文件均由MappedFile类提供读写操作服务(其中,MappedFile类提供了顺序写/随机读、内存数据刷盘、内存清理等和文件相关的服务);
5) 磁盘存储层
主要指的是部署RocketMQ服务器所用的磁盘。这里,需要考虑不同磁盘类型(如SSD或者普通的HDD)特性以及磁盘的性能参数(如IOPS、吞吐量和访问时延等指标)对顺序写/随机读操作带来的影响;
2、源码
1) Produer 发送消息
org.apache.rocketmq.broker.processor.SendMessageProcessor代码片段
public RemotingCommand processRequest(ChannelHandlerContext ctx, RemotingCommand request) throws RemotingCommandException {
SendMessageContext mqtraceContext;
switch (request.getCode()) {
case RequestCode.CONSUMER_SEND_MSG_BACK:
return this.consumerSendMsgBack(ctx, request);
default:
SendMessageRequestHeader requestHeader = parseRequestHeader(request);
if (requestHeader == null) {
return null;
}
mqtraceContext = buildMsgContext(ctx, requestHeader);
this.executeSendMessageHookBefore(ctx, request, mqtraceContext);
RemotingCommand response;
if (requestHeader.isBatch()) {
response = this.sendBatchMessage(ctx, request, mqtraceContext, requestHeader);
} else {
response = this.sendMessage(ctx, request, mqtraceContext, requestHeader);
}
this.executeSendMessageHookAfter(response, mqtraceContext);
return response;
}
}
org.apache.rocketmq.store.DefaultMessageStore#putMessage代码片段
public PutMessageResult putMessage(MessageExtBrokerInner msg) {
//broker已停止工作,拒绝消息写入
if (this.shutdown) {
log.warn("message store has shutdown, so putMessage is forbidden");
return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
}
//broker为slave时,拒绝消息写入
if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
long value = this.printTimes.getAndIncrement();
if ((value % 50000) == 0) {
log.warn("message store is slave mode, so putMessage is forbidden ");
}
return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
}
//rocket不支持写入,拒绝消息写入
if (!this.runningFlags.isWriteable()) {
long value = this.printTimes.getAndIncrement();
if ((value % 50000) == 0) {
log.warn("message store is not writeable, so putMessage is forbidden " + this.runningFlags.getFlagBits());
}
return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
} else {
this.printTimes.set(0);
}
//消息topic长度超过256个字符,拒绝消息写入
if (msg.getTopic().length() > Byte.MAX_VALUE) {
log.warn("putMessage message topic length too long " + msg.getTopic().length());
return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null);
}
//消息属性长度超过65536个字符,拒绝消息写入
if (msg.getPropertiesString() != null && msg.getPropertiesString().length() > Short.MAX_VALUE) {
log.warn("putMessage message properties length too long " + msg.getPropertiesString().length());
return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null);
}
//系统缓存页繁忙,拒绝消息写入
if (this.isOSPageCacheBusy()) {
return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null);
}
long beginTime = this.getSystemClock().now();
PutMessageResult result = this.commitLog.putMessage(msg); //存储消息
long elapsedTime = this.getSystemClock().now() - beginTime;
if (elapsedTime > 500) {
log.warn("putMessage not in lock elapsed time(ms)={}, bodyLength={}", elapsedTime, msg.getBody().length);
}
this.storeStatsService.setPutMessageEntireTimeMax(elapsedTime);
if (null == result || !result.isOk()) {
this.storeStatsService.getPutMessageFailedTimes().incrementAndGet();
}
return result;
}
org.apache.rocketmq.store.CommitLog#putMessage代码片段
public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
//省略部分代码...
//获取commitlog文件存储的最新存储文件
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
try {
long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
this.beginTimeInLock = beginLockTimestamp;
// Here settings are stored timestamp, in order to ensure an orderly
// global
msg.setStoreTimestamp(beginLockTimestamp);
if (null == mappedFile || mappedFile.isFull()) {
mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
}
if (null == mappedFile) {
log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
beginTimeInLock = 0;
return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
}
//追加消息,存储在文件中
result = mappedFile.appendMessage(msg, this.appendMessageCallback);
//省略部分代码...
handleDiskFlush(result, putMessageResult, msg); //消息磁盘存储
handleHA(result, putMessageResult, msg); //主从消息传输
return putMessageResult;
}
org.apache.rocketmq.store.DefaultMessageStore.ReputMessageService#doReput代码片段
private void doReput() {
//省略部分代码,只抽取主体
if (this.reputFromOffset < DefaultMessageStore.this.commitLog.getMinOffset()) {
this.reputFromOffset = DefaultMessageStore.this.commitLog.getMinOffset();
}
SelectMappedBufferResult result = DefaultMessageStore.this.commitLog.getData(reputFromOffset);
DispatchRequest dispatchRequest =
DefaultMessageStore.this.commitLog.checkMessageAndReturnSize(result.getByteBuffer(), false, false);
DefaultMessageStore.this.doDispatch(dispatchRequest);
}
org.apache.rocketmq.store.DefaultMessageStore#doDispatch代码片段
public void doDispatch(DispatchRequest req) {
for (CommitLogDispatcher dispatcher : this.dispatcherList) {
dispatcher.dispatch(req);
}
}
2) Consumer 消费消息
org.apache.rocketmq.broker.processor.PullMessageProcessor#processRequest代码片段
private RemotingCommand processRequest(final Channel channel, RemotingCommand request, boolean brokerAllowSuspend)
throws RemotingCommandException {
//省略部分代码...
//处理客户端拉取消息请求,获取存储消息
final GetMessageResult getMessageResult =
this.brokerController.getMessageStore().getMessage(requestHeader.getConsumerGroup(), requestHeader.getTopic(),
requestHeader.getQueueId(), requestHeader.getQueueOffset(), requestHeader.getMaxMsgNums(), messageFilter);
}
org.apache.rocketmq.store.DefaultMessageStore#getMessage代码片段
public GetMessageResult getMessage(final String group, final String topic, final int queueId, final long offset,
final int maxMsgNums,
final MessageFilter messageFilter) {
//省略部分代码...
ConsumeQueue consumeQueue = findConsumeQueue(topic, queueId);
SelectMappedBufferResult bufferConsumeQueue = consumeQueue.getIndexBuffer(offset);
SelectMappedBufferResult selectResult = this.commitLog.getMessage(offsetPy, sizePy);
getResult.addMessage(selectResult);
}