浅析RocketMQ-MappedFileQueue和MappedFile

RocketMQ的Commit,Comsumequeue,Index文件的代码实现都是MappedFile,而MappedFileQueue则持有了多个MappedFile,可以理解为对应的文件夹。本文主要分析下其重要的方法。

一.创建MappedFile

RocketMQ要向MappedFile中写入数据时,会调用getLastMappedFile获取最新的写入文件

1.getLastMappedFile
 public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
        long createOffset = -1;
        // 获取MappedFile集合中末尾的文件,对应的MappedFileQueue的mappedFiles
        MappedFile mappedFileLast = getLastMappedFile();
		// 1.MappedFile不存在,计算新文件初始偏移量,注意这里不为0
        if (mappedFileLast == null) {
            createOffset = startOffset - (startOffset % this.mappedFileSize);
        }
		// MappedFile存在但是无法写入了,计算新文件初始偏移量
        if (mappedFileLast != null && mappedFileLast.isFull()) {
            createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize;
        }
		// 创建新文件
        if (createOffset != -1 && needCreate) {
            return tryCreateMappedFile(createOffset);
        }

        return mappedFileLast;
    }

注释1处,计算新文件偏移量为啥直接不为0呢?
因为MappedFile文件可能被删除了,故需要重新计算

    protected MappedFile tryCreateMappedFile(long createOffset) {
    	// 拼接文件路径,这里获取了下个文件路径和下下的文件路径
        String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
        String nextNextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset
                + this.mappedFileSize);
        return doCreateMappedFile(nextFilePath, nextNextFilePath);
    }
    protected MappedFile doCreateMappedFile(String nextFilePath, String nextNextFilePath) {
        MappedFile mappedFile = null;
		// 正常是allocateMappedFileService在初始化时会创建
        if (this.allocateMappedFileService != null) {
            mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
                    nextNextFilePath, this.mappedFileSize);
        } else {
            try {
            	// 否则直接构建文件
                mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);
            } catch (IOException e) {}
        }
		
        if (mappedFile != null) {
        	// 设置首次创建标识
            if (this.mappedFiles.isEmpty()) {
                mappedFile.setFirstCreateInQueue(true);
            }
            // 持有新的mappedFile
            this.mappedFiles.add(mappedFile);
        }

        return mappedFile;
    }
2.putRequestAndReturnMappedFile

这里有个transientStorePoolEnable参数,等于true时,开启堆外内存配置,表示消息存储时会先存在堆外内存,然后通过Commit线程将数据提交到内存映射的Buffer中,最后通过Flush线程将数据持久化到磁盘中。

putRequestAndReturnMappedFile会生成下个文件和下下个文件。但提前生成的下下个文件不会返回,留到下次调用直接返回。这样做到每次快人一步,提升了运行效率。

    public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) {
        int canSubmitRequests = 2;
        // 开启了transientStorePoolEnable,默认为false
        if (this.messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
        	// 开启了fastFailIfNoBufferInStorePool配置,默认为false
            if (this.messageStore.getMessageStoreConfig().isFastFailIfNoBufferInStorePool()
            	// 要求是主节点
                && BrokerRole.SLAVE != this.messageStore.getMessageStoreConfig().getBrokerRole()) {
                // 分配的堆外空间是有限数量的
                canSubmitRequests = this.messageStore.getTransientStorePool().availableBufferNums() - this.requestQueue.size();
            }
        }
		// 生成下个文件,包装一下成一个特定请求
        AllocateRequest nextReq = new AllocateRequest(nextFilePath, fileSize);
        boolean nextPutOK = this.requestTable.putIfAbsent(nextFilePath, nextReq) == null;
		
        if (nextPutOK) {
            if (canSubmitRequests <= 0) {
                this.requestTable.remove(nextFilePath);
                return null;
            }
            // requestQueue是一个堵塞队列 
            // 初始化时AllocateMappedFileService会调用一个线程,不断读取requestQueue数据,生成具体的文件
            boolean offerOK = this.requestQueue.offer(nextReq);
            canSubmitRequests--;
        }
		// 生成下下个文件,跟生成生成下个文件逻辑一致
        AllocateRequest nextNextReq = new AllocateRequest(nextNextFilePath, fileSize);
        boolean nextNextPutOK = this.requestTable.putIfAbsent(nextNextFilePath, nextNextReq) == null;
        if (nextNextPutOK) {
            if (canSubmitRequests <= 0) {
                this.requestTable.remove(nextNextFilePath);
            } else {
                boolean offerOK = this.requestQueue.offer(nextNextReq);
            }
        }
		// 具体生成文件过程有误,这里拦截方法返回
        if (hasException) {
            return null;
        }
		// 只获取生成的下个文件结果,下下个文件让线程慢慢运行
        AllocateRequest result = this.requestTable.get(nextFilePath);
        try {
            if (result != null) {
                boolean waitOK = result.getCountDownLatch().await(waitTimeOut, TimeUnit.MILLISECONDS);
                if (!waitOK) {
                    return null;
                } else {
                    this.requestTable.remove(nextFilePath);
                    return result.getMappedFile();
                }
            }
        } catch (InterruptedException e) {}

        return null;
    }

AllocateMappedFileService 调度运行的线程,执行mmapOperation进行创建文件

private boolean mmapOperation() {
        boolean isSuccess = false;
        AllocateRequest req = null;
        try {
        	// 获取创建文件的请求,获取不到则堵塞住
            req = this.requestQueue.take();
            AllocateRequest expectedRequest = this.requestTable.get(req.getFilePath());
            // 请求对象发生变动,直接返回
            if (null == expectedRequest) {
                return true;
            }
            if (expectedRequest != req) {
                return true;
            }
			// 这里不为null说明,已经提前生成了,对应了生成下下个文件的逻辑
            if (req.getMappedFile() == null) {
                long beginTime = System.currentTimeMillis();

                MappedFile mappedFile;
                // 开启了堆外内存
                if (messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
                    try {
                        mappedFile = ServiceLoader.load(MappedFile.class).iterator().next();
                        // 这里比直接构造,多了一个将堆外内存分配给writeBuffer的操作
                        mappedFile.init(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
                    } catch (RuntimeException e) {
                        mappedFile = new MappedFile(req.getFilePath(), req.getFileSize(), messageStore.getTransientStorePool());
                    }
                } else {
                	// 直接构建对象
                    mappedFile = new MappedFile(req.getFilePath(), req.getFileSize());
                }


                // 文件预热操作
                if (mappedFile.getFileSize() >= this.messageStore.getMessageStoreConfig()
                    .getMappedFileSizeCommitLog()
                    &&
                    this.messageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
                    // getFlushDiskType为异步刷盘,getFlushLeastPagesWhenWarmMapedFile为4k
                    mappedFile.warmMappedFile(this.messageStore.getMessageStoreConfig().getFlushDiskType(),
                        this.messageStore.getMessageStoreConfig().getFlushLeastPagesWhenWarmMapedFile());
                }

                req.setMappedFile(mappedFile);
                this.hasException = false;
                isSuccess = true;
            }
        } 
        ... // 省略异常捕获处理,主要设置hasException=true
        } finally {
        	// 对应putRequestAndReturnMappedFile的getCountDownLatch.await操作
            if (req != null && isSuccess)
                req.getCountDownLatch().countDown();
        }
        return true;
    }
3. warmMappedFile

为啥要进行文件预热?
我们知道RocketMQ使用了内存映射技术mmap,它将文件在磁盘位置的地址和的虚拟地址通过映射对应起来。但是操作系统并没有加载到物理内存中。文件预热可以理解为将数据加载到物理内存的操作。

   public void warmMappedFile(FlushDiskType type, int pages) {
   		// 创建一个共享的缓存区
        ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
        int flush = 0;
        // MappedFile.OS_PAGE_SIZE= 4k
        for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
        	// 1.每隔4k,向缓存区写入一个字节
            byteBuffer.put(i, (byte) 0);
            // 同步刷盘
            if (type == FlushDiskType.SYNC_FLUSH) {
            	// 每隔16K,强制刷盘一次,pages=4k,OS_PAGE_SIZE为=4k
                if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
                    flush = i;
                    mappedByteBuffer.force();
                }
            }

            // 这里是让出CPU,避免长时间占用
            if (j % 1000 == 0) {
                try {
                    Thread.sleep(0);
                } catch (InterruptedException e) {}
            }
        }

        // 强制刷盘
        if (type == FlushDiskType.SYNC_FLUSH) {
            mappedByteBuffer.force();
        }
        // 将进程使用的部分或全部的地址空间锁定在物理内存中,防止其被交换到swap空间
        this.mlock();
    }

注释1 为啥每次写入是4k?
这跟Page Cache 页缓存有关,每一页大小约为4k。系统每次读写数据会先到Page Cache,再到硬盘,如果请求过来,在页缓存上没对应的数据,则会发生缺页中断,磁盘重新加载数据到内存。每隔4k写入,用于保证不会发生缺页。

二.查找MappedFile

findMappedFileByOffset 根据偏移量查找文件

    public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) {
        try {
        	// 获取首个文件
            MappedFile firstMappedFile = this.getFirstMappedFile();
            //获取最后一个文件
            MappedFile lastMappedFile = this.getLastMappedFile();
            if (firstMappedFile != null && lastMappedFile != null) {
            	// 偏移量小于现有最小的或者大于现有最大的,直接返回
                if (offset < firstMappedFile.getFileFromOffset() || offset >= lastMappedFile.getFileFromOffset() + this.mappedFileSize) {
                } else {
                	//计算文件索引,这里也是考虑到首个文件偏移量不为0的情况
                    int index = (int) ((offset / this.mappedFileSize) - (firstMappedFile.getFileFromOffset() / this.mappedFileSize));
                    MappedFile targetFile = null;
                    try {
                        targetFile = this.mappedFiles.get(index);
                    } catch (Exception ignored) {
                    }
					// 正好存在
                    if (targetFile != null && offset >= targetFile.getFileFromOffset()
                        && offset < targetFile.getFileFromOffset() + this.mappedFileSize) {
                        return targetFile;
                    }

                    for (MappedFile tmpMappedFile : this.mappedFiles) {
                        if (offset >= tmpMappedFile.getFileFromOffset()
                            && offset < tmpMappedFile.getFileFromOffset() + this.mappedFileSize) {
                            return tmpMappedFile;
                        }
                    }
                }

                if (returnFirstOnNotFound) {
                    return firstMappedFile;
                }
            }
        } catch (Exception e) { }

        return null;
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值