RocketMQ:存储架构

一、阅读目标

        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);
}

引用

https://blog.csdn.net/hxyascx/article/details/83341606
https://www.jianshu.com/p/d06e9bc6c463
https://my.oschina.net/mingxungu/blog/3083961

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值