RocketMQ中的消息存储文件解析

RocketMQ中写入的消息会存储到commitlog文件下,然后再异步转存到consumequeue以及index文件,它们的关系如下:

文件和底层交互

CommitLog

commitlog中存放很多的mappedFile文件,当前Broker中的所有消息都是落盘到这些mappedFile文件。mappedFile文件默认最大为1G,文件名是20位10进制数构成,表示当前文件的第一条消息起始偏移量。每一个mappedFile文件物理上是被拆分的,但是逻辑上是连续的。

消息单元

一个Broker中只会有一个commitlog目录,Broker接收到的所以消息都会顺序存放进mappedFile,不管你是什么Topic消息,也就是说Broker中存放消息时是没有按照Topic进行分类存放的,那是如何区分这些消息是哪个Queue,哪个Topic的呢

consumequeue

刚刚讲到mappedFile是用于存放消息的各种信息,从这些信息可以知道当前消息属于那个Queue那个Topic,那么如果通过遍历mappedFile去获取消息是相当麻烦的事情

为了提高效率,RocketMQ在为每个Topic在~/store/consumequeue中创建一个目录,目录名称就是Topic的名称,在该Topic下会为每个Queue创建独立的目录,目录名为QueueId,每个目录中存放着若干的consumequeue文件,consumequeue是commitlog的索引文件,可以通过consumequeue定位到commitlog的具体消息。

indexFile

除了通过正常的指定Topic进行消息消费外,RocketMQ还提供了一种根据key进行消息查询的功能,该查询通过store目录中的index子目录中的indexFile进行索引实现查询的,当Broker收到包含key的消息时这个消息索引就会被写入indexFile,如果消息没key不会写入。

MappedFile

CommitLog与ConsumeQueue都有成员变量MappedFileQueue,MappedFileQueue的定义如下:

public class MappedFileQueue {

    /**
     * 批量删除文件上限
     */
    private static final int DELETE_FILES_BATCH_MAX = 10;
    /**
     * 目录
     */
    private final String storePath;
    /**
     * 每个映射文件大小
     */
    private final int mappedFileSize;
    /**
     * 映射文件数组
     */
    private final CopyOnWriteArrayList<MappedFile> mappedFiles = new CopyOnWriteArrayList<>();
    /**
     * TODO
     */
    private final AllocateMappedFileService allocateMappedFileService;
    /**
     * 最后flush到的位置offset
     */
    private long flushedWhere = 0;
    /**
     * 最后commit到的位置offset,这个变量是针对所有MappedFile的,committedWhere/mappedFileSize = 写入的文件的序号,从0开始
     */
    private long committedWhere = 0;
    /**
     * 最后store时间戳
     */
    private volatile long storeTimestamp = 0;

MappedFileQueue含有变量mappedFiles,它是一个MappedFile的数组,MappedFile是实际保存消息的地方。MappedFile的定义如下:

public class MappedFile extends ReferenceResource {

    /**
     * 文件系统缓存里内存页的最小分配单元,4K
     */
    public static final int OS_PAGE_SIZE = 1024 * 4;
    /**
     * 映射虚拟内存总字节数
     */
    private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);
    /**
     * 映射文件总数
     */
    private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);
    /**
     * 当前追加到MappedByteBuffer的位置
     */
    protected final AtomicInteger wrotePosition = new AtomicInteger(0);
    /**
     * ADD BY ChenYang
     * 当前commit位置
     */
    protected final AtomicInteger committedPosition = new AtomicInteger(0);
    /**
     * 当前flush位置
     */
    private final AtomicInteger flushedPosition = new AtomicInteger(0);
    /**
     * 文件大小
     */
    protected int fileSize;
    /**
     * fileChannel
     * {@link #file}的channel = new RandomAccessFile(this.file, "rw").getChannel()
     */
    protected FileChannel fileChannel;
    /**
     * Message will put to here first, and then reput to FileChannel if writeBuffer is not null.
     * 写入缓冲
     */
    protected ByteBuffer writeBuffer = null;
    /**
     * writeBuffer缓存池
     */
    protected TransientStorePool transientStorePool = null;
    /**
     * 文件名
     */
    private String fileName;
    /**
     * 文件开始的offset。
     * 目前文件名即offset
     */
    private long fileFromOffset;
    /**
     * 文件
     */
    private File file;
    /**
     * 文件映射Buffer
     */
    private MappedByteBuffer mappedByteBuffer;
    /**
     * 最后插入数据时间。即{@link #mappedByteBuffer}变更时间
     */
    private volatile long storeTimestamp = 0;
    /**
     * 是否最先创建在队列
     * {@link MappedFileQueue#getLastMappedFile(long, boolean)}
     */
    private boolean firstCreateInQueue = false;

MappedFile初始化

MappedFile先将数据存储到堆外内存,然后通过commit线程将数据提交到内存映射Buffer中,再通过Flush线程将内存映射Buffer中的数据持久化到磁盘。MappedFile的初始化操作如下:

private void init(final String fileName, final int fileSize) throws IOException {
        this.fileName = fileName;
        this.fileSize = fileSize;
        this.file = new File(fileName);
        this.fileFromOffset = Long.parseLong(this.file.getName());
        boolean ok = false;

        ensureDirOK(this.file.getParent());

        try {
            this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
            this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
            TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
            TOTAL_MAPPED_FILES.incrementAndGet();
            ok = true;
        } catch (FileNotFoundException e) {
            log.error("create file channel " + this.fileName + " Failed. ", e);
            throw e;
        } catch (IOException e) {
            log.error("map file " + this.fileName + " Failed. ", e);
            throw e;
        } finally {
            if (!ok && this.fileChannel != null) {
                this.fileChannel.close();
            }
        }
    }

MappedFile提交

MappedFile先将数据写入到内存ByteBuffer writeBuffer中,然后每隔一段时间将writeBuffer中的数据提交到内存映射MappedByteBuffer mappedByteBuffer。在CommitLog中有个线程CommitRealTimeService会实时检查MappedFile是否满足commit的条件:Commit需要缓冲区内至少含有4页数据,也就是16KB,或者是最近200毫秒内没有消息Commit。

MappedFile的commit流程

public int commit(final int commitLeastPages) {
        if (writeBuffer == null) {
            //no need to commit data to file channel, so just regard wrotePosition as committedPosition.
            return this.wrotePosition.get();
        }
        //判断是否满足提交条件
        if (this.isAbleToCommit(commitLeastPages)) {
            if (this.hold()) {
                commit0(commitLeastPages);
                this.release();
            } else {
                log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
            }
        }

        // 所以数据提交后,清空缓存
        if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
            this.transientStorePool.returnBuffer(writeBuffer);
            this.writeBuffer = null;
        }

        return this.committedPosition.get();
    }

MappedFile#isAbleToCommit 

 /**
     * 是否能够commit。满足如下条件任意条件:
     * 1. 映射文件已经写满
     * 2. commitLeastPages > 0 && 未commit部分超过commitLeastPages
     * 3. commitLeastPages = 0 && 有新写入部分
     *
     * @param commitLeastPages commit最小分页
     * @return 是否能够写入
     */
    protected boolean isAbleToCommit(final int commitLeastPages) {
        int commit = this.committedPosition.get();
        int write = this.wrotePosition.get();

        if (this.isFull()) {
            return true;
        }

        if (commitLeastPages > 0) {
            return ((write / OS_PAGE_SIZE) - (commit / OS_PAGE_SIZE)) >= commitLeastPages;
        }

        return write > commit;
    }

判断是否执行commit操作:

     1)如果文件已满返回true;

     2)如果commitLeastPages 大于0,且脏数据页大于commitLeastPages则返回true;

     3)如果commitLeastPages 小于等于0,则只要有脏数据及提交

MappedFile#commit0

/**
     * commit实现,将writeBuffer写入fileChannel,更新committedPosition = wrotePosition
     *
     * @param commitLeastPages commit最小页数。用不上该参数
     */
    protected void commit0(final int commitLeastPages) {
        int writePos = this.wrotePosition.get();
        int lastCommittedPosition = this.committedPosition.get();

        if (writePos - this.committedPosition.get() > 0) {
            try {
                // 设置需要写入的byteBuffer
                ByteBuffer byteBuffer = writeBuffer.slice();
                byteBuffer.position(lastCommittedPosition);
                byteBuffer.limit(writePos);
                // 写入fileChannel
                this.fileChannel.position(lastCommittedPosition);
                this.fileChannel.write(byteBuffer);
                // 设置position
                this.committedPosition.set(writePos);
            } catch (Throwable e) {
                log.error("Error occurred when commit data to FileChannel.", e);
            }
        }
    }

具体提交的实现,首先创建writeBuffer的共享存储区,然后将position回退到上一次提交的位置lastCommittedPosition,设置limit为wrotePosition(当前最大有效数据位置),然后把wrotePosition到lastCommittedPosition之间的数据写入到fileChannel中。总之,commit的作用就是将MappedFile中的writeBuffer中的数据提交到文件通道FileChannel中。

MappedFile刷写磁盘

刷写磁盘,就是调用MappedByteBuffer或者FileChannel中的force方法将内存中的数据写入到磁盘中。流程如下:

 

/**
     * 如果满足flush条件,返回这次flush后的位置
     * 如果不满住flush条件,返回上次flush的位置
     *
     * @param flushLeastPages flush最小页数
     * @return The current flushed position
     */
    public int flush(final int flushLeastPages) {
        //是否达到刷盘条件
        if (this.isAbleToFlush(flushLeastPages)) {
             //加锁,同步刷盘
            if (this.hold()) {
                //获得读指针
                int value = getReadPosition();

                try {
                     //数据从writeBuffer提交到fileChannel再刷盘
                    if (writeBuffer != null || this.fileChannel.position() != 0) {
                        this.fileChannel.force(false);
                    } else {
                        //从mmap刷新数据到磁盘
                        this.mappedByteBuffer.force();
                    }
                } catch (Throwable e) {
                    log.error("Error occurred when force data to disk.", e);
                }

                this.flushedPosition.set(value);
                this.release();
            } else {
                log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
                this.flushedPosition.set(getReadPosition());
            }
        }
        return this.getFlushedPosition();
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值