rocketmq文件索引之我见

本文探讨RocketMQ中CommitLog和ConsumeQueue的文件结构,分析消息写入流程及索引逻辑。当发送9条900byte消息时,观察到consumequeue已预分配空间,commitLog文件的offset与消息数量对应。每条commitLog的第一个消息offset与其文件名offset相同,文件满后用0填充剩余空间。接着,分析了MappedFile的创建和消息写入流程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

rocketmq:4.3

消息写入概要写入流出,先写入CommitLog,然后会有异步线程写入ConsumeQueue和IndexFile文件。这个写入是顺序写。

下面来看下各个文件里面的索引/offset都是什么含义。

先看现象,容易理解。首先配置mapedFileSizeCommitLog(即commitLog的文件大小)为10K,然后创建topic:commitLogTopic,然后发送长度为900byte的字符串9次。commitLog和consumequeue的结构如下。rocketmq在创建mappedfile的时候会进行预分配。所以还会有一个以10240结尾的文件,虽然占了10K空间,但是里面是空的。

[-bash-3.2$ ls -lh commitlog/
total 24
-rw-r--r--  1 xiao  staff    10K  3 29 10:00 00000000000000000000
-rw-r--r--  1 xiao  staff    10K  3 29 10:00 00000000000000010240

然后观察consumequeue的文件,也是已经分配好了5.7M的空间

[-bash-3.2$ ls -lh consumequeue/commitLogTopic/0/
total 11720
-rw-r--r--  1 xiao  staff   5.7M  3 29 10:00 00000000000000000000

读取一下consumequeue的内容。第一列是消息数量,第二列是tag的hashcode(我们没设置tag),第三列是消息的长度,第四列是消息的offset(物理的offset)。观察最后一个消息的offset是8464,是说第9条消息在第一个commitLog(即00000000000000000000里面的开始写入的位置),但是实际上消息已经写到了(8464+1058)这个位置,注意是写到了。

1: 0 1058 0
2: 0 1058 1058
3: 0 1058 2116
4: 0 1058 3174
5: 0 1058 4232
6: 0 1058 5290
7: 0 1058 6348
8: 0 1058 7406
9: 0 1058 8464

那么第一个commitLog剩余可写的容量是10240-9522(8464+1058)=718,即不足以存储下一条消息(我们发送的消息)。

接下来我们发送第10条消息,发现00000000000000010240被写入了一条消息,(同时又生成了第三个commitLog文件:00000000000000020480)我们再次读一下consumequeue,第10条消息的内容如下:

10: 0 1058 10240

即每个commitLog里面第一条消息的offset跟当前文件名称的offset一致。既然第10条消息写如了第2个commitLog文件,那第一个commitLog后面的字节变成了什么?我们把第一个commitLog读到byte[]里面,然后倒叙输出,发现后718个字节是0,即被0补齐了。


OK,那接下来咱们看下代码里面的一些逻辑。

首先,消息的各种配置的类是:

org.apache.rocketmq.store.config.MessageStoreConfig

里面定义的CommitLog默认大小1G,然后进入CommitLog类的构造方法,分配了mappedFileQueue,commitLogService等等变量,我们这里关注mappedFileQueue

public CommitLog(final DefaultMessageStore defaultMessageStore) {
    this.mappedFileQueue = new MappedFileQueue(defaultMessageStore.getMessageStoreConfig().getStorePathCommitLog(),
        defaultMessageStore.getMessageStoreConfig().getMapedFileSizeCommitLog(), defaultMessageStore.getAllocateMappedFileService());
    this.defaultMessageStore = defaultMessageStore;

然后何时会创建MappedFile呢?

方法在AllocateMappedFileService.mmapOperation()方法里面,它是从一个queue取出AllocateReq,然后进行构造。

private boolean mmapOperation() {
        boolean isSuccess = false;
        AllocateRequest req = null;
        try {
            req = this.requestQueue.take();
            AllocateRequest expectedRequest = this.requestTable.get(req.getFilePath());
            if (null == expectedRequest) {
                log.warn("this mmap request expired, maybe cause timeout " + req.getFilePath() + " "
                    + req.getFileSize());
                return true;
            }
            if (expectedRequest != req) {
                log.warn("never expected here,  maybe cause timeout " + req.getFilePath() + " "
                    + req.getFileSize() + ", req:" + req + ", expectedRequest:" + expectedRequest);
                return true;
            }

            if (req.getMappedFile() == null) {
                long beginTime = System.currentTimeMillis();

                MappedFile mappedFile;
                if (messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
                    try {
                        mappedFile = ServiceLoader.load(MappedFile.class).iterator().next();
// 这里创建
                        mappedFile.init(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
                    } catch (RuntimeException e) {
                        log.warn("Use default implementation.");
                        mappedFile = new MappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
                    }
                } else {
// 或者这里创建
                    mappedFile = new MappedFile(req.getFilePath(), req.getFileSize());
                }
// 省略其他部分
}

那接下来的问题就是这个queue里面的参数是在什么地方放进去的呢?我们搜一下这个requestQueue,那么发现是这个类里面的putRequestAndReturnMappedFile方法。代码不贴了,具体有是谁调用了它,通过IDEA的find usage即可找到。

分析完MappedFile的创建过程,我们开始看下消息写入的流程。

直接进入CommitLog查看的putMessage方法,然后进入这行代码里面继续查看

result = mappedFile.appendMessage(msg, this.appendMessageCallback);

然后进入到MappedFile.appendMessageInner方法,

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) {
                // 进入这里查看逻辑,this.getFileFromOffset()即文件的名字,this.fileSize - currentPos表示当前文件剩余可写入的空间
                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);
    }

注意观察标记类注释的那行,再次跳回到CommitLog类

public AppendMessageResult doAppend(final long fileFromOffset, final ByteBuffer byteBuffer, final int maxBlank,
    final MessageExtBrokerInner msgInner) {
    // STORETIMESTAMP + STOREHOSTADDRESS + OFFSET <br>

    // PHY OFFSET
    long wroteOffset = fileFromOffset + byteBuffer.position();

这里看到wroteOffset即是文件的偏移量+当前已写入字节数的位置

 

后续继续更新。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值