RocketMQ MappedFile 預熱原理解析

創建 MappedFile 文件

創建 MappedFile 文件實現如下:

private boolean mmapOperation() {
    boolean isSuccess = false;
    AllocateRequest req = null;
    try {
        // 從 requestQueue 阻塞隊列中獲取 AllocateRequest  任務。
        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;
            // 判斷是否開啓 isTransientStorePoolEnable ,如果開啓則使用直接內存進行寫入數據,最後從直接內存中 commit 到 FileChannel 中。
            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 {
                // 使用 mmap 方式創建 MappedFile
                mappedFile = new MappedFile(req.getFilePath(), req.getFileSize());
            }

            long eclipseTime = UtilAll.computeEclipseTimeMilliseconds(beginTime);
            if (eclipseTime > 10) {
                int queueSize = this.requestQueue.size();
                log.warn("create mappedFile spent time(ms) " + eclipseTime + " queue size " + queueSize
                    + " " + req.getFilePath() + " " + req.getFileSize());
            }
            // 判斷 mappedFile 大小,只有 CommitLog 才進行文件預熱
            // 預寫入數據。按照系統的 pagesize 進行每個pagesize 寫入一個字節數據。
            //爲了把mmap 方式映射的文件都加載到內存中。
            if (mappedFile.getFileSize() >= this.messageStore.getMessageStoreConfig()
                .getMapedFileSizeCommitLog()
                &&
                this.messageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
                mappedFile.warmMappedFile(this.messageStore.getMessageStoreConfig().getFlushDiskType(),
                    this.messageStore.getMessageStoreConfig().getFlushLeastPagesWhenWarmMapedFile());
            }

            req.setMappedFile(mappedFile);
            this.hasException = false;
            isSuccess = true;
        }
    } catch (InterruptedException e) {
        log.warn(this.getServiceName() + " interrupted, possibly by shutdown.");
        this.hasException = true;
        return false;
    } catch (IOException e) {
        log.warn(this.getServiceName() + " service has exception. ", e);
        this.hasException = true;
        if (null != req) {
            requestQueue.offer(req);
            try {
                Thread.sleep(1);
            } catch (InterruptedException ignored) {
            }
        }
    } finally {
        if (req != null && isSuccess)
            req.getCountDownLatch().countDown();
    }
    return true;
}

從代碼中可以看出,只有 MappedFile 的大小等於或大於 CommitLog 的大小並且開啓文件預熱功能纔會預加載文件。
CommitLog 文件的大小默認爲 1 G。

文件預熱

文件預熱的時候需要了解的知識點 操作系統的 Page Cache 和 內存映射技術 mmap 。

Page Cache

Page Cache 叫做頁緩存,而每一頁的大小通常是4K,在Linux系統中寫入數據的時候並不會直接寫到硬盤上,而是會先寫到Page Cache中,並打上dirty標識,由內核線程flusher定期將被打上dirty的頁發送給IO調度層,最後由IO調度決定何時落地到磁盤中,而Linux一般會把還沒有使用的內存全拿來給Page Cache使用。而讀的過程也是類似,會先到Page Cache中尋找是否有數據,有的話直接返回,如果沒有纔會到磁盤中去讀取並寫入Page Cache然後再次讀取Page Cache並返回。而且讀的這個過程中操作系統也會有一個預讀的操作,你的每一次讀取操作系統都會幫你預讀出後面一部分數據,而且當你一直在使用預讀數據的時候,系統會幫你預讀出更多的數據(最大到128K)。

mmap

mmap是一種將文件映射到虛擬內存的技術,可以將文件在磁盤位置的地址和在虛擬內存中的虛擬地址通過映射對應起來,之後就可以在內存這塊區域進行讀寫數據,而不必調用系統級別的read,wirte這些函數,從而提升IO操作性能,另外一點就是mmap後的虛擬內存大小必須是內存頁大小(通常是4K)的倍數,之所以這麼做是爲了匹配內存操作。

預熱代碼

public void warmMappedFile(FlushDiskType type, int pages) {
        long beginTime = System.currentTimeMillis();
        ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
        int flush = 0;
        long time = System.currentTimeMillis();
        for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
            byteBuffer.put(i, (byte) 0);
            // 如果是同步寫盤操作,則進行強行刷盤操作
            if (type == FlushDiskType.SYNC_FLUSH) {
                if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
                    flush = i;
                    mappedByteBuffer.force();
                }
            }

            // prevent gc  (有什麼用?)
            if (j % 1000 == 0) {
                log.info("j={}, costTime={}", j, System.currentTimeMillis() - time);
                time = System.currentTimeMillis();
                try {
                    Thread.sleep(0);
                } catch (InterruptedException e) {
                    log.error("Interrupted", e);
                }
            }
        }

        // 把剩餘的數據強制刷新到磁盤中
        if (type == FlushDiskType.SYNC_FLUSH) {
            log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}",
                this.getFileName(), System.currentTimeMillis() - beginTime);
            mappedByteBuffer.force();
        }
        log.info("mapped file warm-up done. mappedFile={}, costTime={}", this.getFileName(),
            System.currentTimeMillis() - beginTime);

        this.mlock();
    }

這裏 MappedFile 已經創建,對應的 Buffer 爲 mappedByteBuffer。
mappedByteBuffer 已經通過 mmap 映射,此時操作系統中只是記錄了該文件和該 Buffer 的映射關係,而沒有映射到物理內存中。這裏就通過對該 MappedFile 的每個 Page Cache 進行寫入一個字節,通過讀寫操作把 mmap 映射全部加載到物理內存中。

鎖定內存 mlock()

    public void mlock() {
        final long beginTime = System.currentTimeMillis();
        final long address = ((DirectBuffer) (this.mappedByteBuffer)).address();
        Pointer pointer = new Pointer(address);
        {
            int ret = LibC.INSTANCE.mlock(pointer, new NativeLong(this.fileSize));
            log.info("mlock {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime);
        }

        {
            int ret = LibC.INSTANCE.madvise(pointer, new NativeLong(this.fileSize), LibC.MADV_WILLNEED);
            log.info("madvise {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime);
        }
    }

該方法主要是實現文件預熱後,防止把預熱過的文件被操作系統調到swap空間中。當程序在次讀取交換出去的數據的時候會產生缺頁異常。

LibC.INSTANCE.mlock 和 LibC.INSTANCE.madvise 都是調用的 Native 方法。

  • LibC.INSTANCE.mlock 方法
    實現是將鎖住指定的內存區域避免被操作系統調到swap空間中。
  • LibC.INSTANCE.madvise 方法
    實現是一次性先將一段數據讀入到映射內存區域,這樣就減少了缺頁異常的產生。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章