6. Broker-消息存储文件

1. CommitLog文件

commitlog的文件组织如下所示,每个文件固定大小,默认1G,一个文件写满,创建另一个文件。文件名为文件中第一条消息的全局偏移量。

commitlog使用MappedFile和MappedFileQueue来组织消息文件。MappedFileQueue对应一个文件夹,MappedFile对应一个commitlog文件

接着上一节,进入CommitLog.putMessage方法

 public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
        。。。
        //获取最后一个mappedfile
        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);

            //如果不存在,则创建一个mappedfile
            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);
            }

            //调用mappedfile的appendMessage方法存储消息
            result = mappedFile.appendMessage(msg, this.appendMessageCallback);
            switch (result.getStatus()) {
                case PUT_OK:
                    break;

                //文件已经满了
                case END_OF_FILE:
                    unlockMappedFile = mappedFile;
                    // Create a new file, re-write the message
                    //重新创建一个mappedfile文件
                    mappedFile = this.mappedFileQueue.getLastMappedFile(0);
                    if (null == mappedFile) {
                        // XXX: warn and notify me
                        log.error("create mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
                        beginTimeInLock = 0;
                        return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
                    }
                    result = mappedFile.appendMessage(msg, this.appendMessageCallback);
                    break;
                case MESSAGE_SIZE_EXCEEDED:
                case PROPERTIES_SIZE_EXCEEDED:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result);
                case UNKNOWN_ERROR:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
                default:
                    beginTimeInLock = 0;
                    return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
            }

            eclipseTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
            beginTimeInLock = 0;
        } finally {
            putMessageLock.unlock();
        }

        。。。

        PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);

        。。。

        handleDiskFlush(result, putMessageResult, msg);
        handleHA(result, putMessageResult, msg);

        return putMessageResult;
    }

首先调用mappedFileQueue.getLastMappedFile()方法获取最后一个mappedfile对象,MappedFileQueue对象结构如下

storePath:存储路径

mappedFileSize:每个commitlog文件大小,默认1G

allocateMappedFileService:mappedFile创建服务,默认没有用到。

flushedWhere:flush偏移位置

committedWhere:commit偏移位置

mappedFiles:维护的mappedfile列表

再看下mappedFileQueue.getLastMappedFile()方法

    public MappedFile getLastMappedFile() {
        MappedFile mappedFileLast = null;

        while (!this.mappedFiles.isEmpty()) {
            try {
                mappedFileLast = this.mappedFiles.get(this.mappedFiles.size() - 1);
                break;
            } catch (IndexOutOfBoundsException e) {
                //continue;
            } catch (Exception e) {
                log.error("getLastMappedFile has exception.", e);
                break;
            }
        }

        return mappedFileLast;
    }

该方法就是从mappedFiles列表中取出最后一个返回,如果列表为空则返回null,再由外部去创建新的文件。


MappedFile类结构如下

OS_PAGE_SIZE:页大小,文件commit和flush都是以页大小为单位的,大小为4K

fileSize:文件大小,默认1G

fileChannel:文件对应的通道

writeBuffer:文件写入buffer,transientStorePool不为null时有用

fileName:文件名

fileFromOffset:文件第一个消息的偏移

file:打开的文件

mappedByteBuffer:文件的内存映射

wrotePosition、committedPosition、flushedPosition:写偏移,commit偏移,flush偏移

先解释下writeBuffer和transientStorePool,mappedfile有两种初始化,一种是直接创建内存映射,一种在外面再包一层堆外内存即transientStorePool,目的是使用了一种内存锁定技术,避免内存被交换。但是默认好像是关闭的。

再解释下commit和flush,commit是将内存提交到filechannel中(此时未落盘处于oscache中),flush是将内容刷新到磁盘上。

 

接着看CommitLog.putMessage()方法,调用mappedFile.appendMessage()方法,即调用mappedFile去具体存储消息。


    public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
        assert messageExt != null;
        assert cb != null;

        int currentPos = this.wrotePosition.get();

        if (currentPos < this.fileSize) {
            ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
            byteBuffer.position(currentPos);
            AppendMessageResult result = null;
            if (messageExt instanceof MessageExtBrokerInner) {
                result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
            } else if (messageExt instanceof MessageExtBatch) {
                result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
            } else {
                return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
            }
            this.wrotePosition.addAndGet(result.getWroteBytes());
            this.storeTimestamp = result.getStoreTimestamp();
            return result;
        }
        log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);
        return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
    }

该方法首先获取当前写偏移currentPos,然后调用ByteBuffer的slice方法创建一个共享内存对象byteBuffer ,更新byteBuffer 的写位置,调用CommitLog.doAppend将消息写入byteBuffer ,最后更新写偏移wrotePosition

 

2. Consumequeue文件

由上面commitlog的存储消息可以看到,所有topic的消息都是写到同一个文件中的,这样不同的消费者根据topic检索消息效率肯定非常低下,所以引入了consumequeue文件

consumequeue文件夹下一级目录是各个topic

topic下是各个queue文件夹

 

各个queue下存储的当前queue下的文件,组织方式和comitlog类似。

 

consumerqueue文件中并没有存储全量的消息内容,存储的只是消息再commitlog中的偏移,每个item存储格式如下:

comitlog offset: 8字节

size:4字节

tag hashcode: 8字节

consumequeue可以看做一个item数组,如果给定一个startIndex位置,获取item,则offset=startIndex*20 为该消息在consumequeue中的物理偏移量,如果offset小于minLogicOffset则表示该消息已经被删除了。否则获取从offset开始后的20个字节即为要获取的消息item,在根据item中的comitlog offset即可在commitlog中获取消息。

consumequeue是由异步线程从commitlog中拉取的。

具体服务为DefaultMessageStore中的reputMessageService服务,最终调用CommitLogDispatcherBuildConsumeQueue的dispatch方法

 

3. Index文件

rocketmq还维护一个index索引文件,格式如下

可以根据消息的unikey(topic+producer发送消息是创建的UniqID)快速定位消息,unikey格式为:

其中,

IndexHeader头部,包含40个字节,记录文件的统计信息,包括最小存储时间,最大存储时间,最小物理偏移,最大物理偏移,hashslot个数,index条目列表已经使用的个数

hash槽,一个index文件默认包含500万个hash槽,每个hash槽存储的是index的索引

index条目,默认一个index文件包含2000万个条目,每个index条目结构为
hashcode:key的hashcode

phyoffset:消息对应的物理偏移量

timedif:消息时间与第一条消息的时间戳差值,小于0表示该消息无效

preindexNo:当hash冲突时 ,构建的链表结构。

index文件和consumequeue文件一样也是由异步线程同步的,具体位置位于CommitLogDispatcherBuildIndex的dispatch方法。

4.过期文件删除

rocketmq文件采用内存映射机制,如果文件不删除,将一直占用大量内存,所以引入一种过期文件删除机制。

当前激活文件(正在写入的文件)之外的文件,如果一段时间没有更新(默认72h),rocketmq并不关心消息文件是否被消费。

过期文件删除位于

    private void cleanFilesPeriodically() {
        this.cleanCommitLogService.run();
        this.cleanConsumeQueueService.run();
    }

commitlog和consumequeue文件分别由两个服务去定期检测。

过期的文件并不是立即删除,删除条件包括:

1.达到设置的时间点,默认是凌晨4点

2.磁盘不足,默认是75%

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值