rocket mq 底層儲存原始碼分析(2)-業務訊息持久化

weixin_34402408發表於2017-12-04

本章主要詳細分析Rocket mq 訊息持久化底層原始碼實現。

先講解幾個核心的業務抽象類

MappedFile, 該類為一個儲存檔案的直接記憶體對映業務抽象類,通過操作該類,可以把訊息位元組寫入pagecache快取區(commit),或者原子性的訊息刷盤(flush)

public class MappedFile{

  protected final AtomicInteger wrotePosition;

  protected final AtomicInteger committedPosition;

  private final AtomicInteger flushedPosition;

   protected ByteBuffer writeBuffer;

   protected TransientStorePool transientStorePool;
    
   //檔案的起始實體地址位移
   private String fileName;
   
   //檔案的起始實體地址位移
   private long fileFromOffset;

   private MappedByteBuffer mappedByteBuffer;


   public void init(final String fileName, final int fileSize, final TransientStorePool transientStorePool) throws IOException {
        init(fileName, fileSize);
        this.writeBuffer = transientStorePool.borrowBuffer();
        this.transientStorePool = transientStorePool;
    }


   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;
        }
        ...
    }

}

rmq實現持久化方式有兩種:
MappedByteBuffer
rmq的初始化方式:
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize)。

READ_WRITE(讀/寫):
對得到的緩衝區的更改最終將傳播到檔案;該更改對對映到同一檔案的其他程式不一定是可見的,並且是對整個檔案作出對映(0, fileSize)。

通過wrotePosition屬性來維護當前檔案所對映到的訊息寫入pagecache的位置,flushedPosition來維持刷盤的最新位置。

例如,wrotePosition和flushedPosition的初始化值為0,一條1k大小的訊息送達,當訊息commit也就是寫入pagecache以後,wrotePosition的值為1024 * 1024;如果訊息刷盤以後,則flushedPosition也是1024 * 1024;另外一條1k大小的訊息送達,當訊息commit時,wrotePosition的值為1024 * 1024 + 1024 * 1024,同樣,訊息刷盤後,flushedPosition的值為1024 * 1024 + 1024 * 1024。

FileChannel
當broker設定刷盤方式為FlushDiskType.ASYNC_FLUSH非同步刷盤,角色為MASTER,並且屬性transientStorePoolEnable設定為true時, 才通過FileChannel進行訊息的持久化實現。這裡,fileChannel一樣通過直接記憶體來操作pagecache,只不過,在系統初始化時通過時,通過TransientStorePool來初始化DirectBuffer池,具體程式碼:

   public void init() {
     for (int i = 0; i < poolSize; i++) {
           ByteBuffer byteBuffer = ByteBuffer.allocateDirect(fileSize);

           final long address = ((DirectBuffer) byteBuffer).address();
           Pointer pointer = new Pointer(address);

           //過mlock可以將程式使用的部分或者全部的地址空間鎖定在實體記憶體中,防止其被交換到swap空間。
           //對時間敏感的應用會希望全部使用實體記憶體,提高資料訪問和操作的效率。
           LibC.INSTANCE.mlock(pointer, new NativeLong(fileSize));

           availableBuffers.offer(byteBuffer);
       }
   }

程式碼中,直接通過jna來鎖定實體記憶體頁,最後放入availableBuffers-> ConcurrentLinkedDeque裡面。
這裡使用 堆外記憶體 池化的組合方式,來對生命週期較短,但涉及到I/O操作的物件
進行堆外記憶體的在使用(Netty中就使用了該方式)。

當我們需要使用DirectBuffer時,transientStorePool.borrowBuffer(),直接從ConcurrentLinkedDeque獲取一個即可。


訊息持久化的業務抽象類為CommitLog,以下為該類的幾個關鍵屬性:

public class CommitLog {

private final MappedFileQueue mappedFileQueue;

private final FlushCommitLogService flushCommitLogService;

//If TransientStorePool enabled, we must flush message to FileChannel at fixed periods

private final FlushCommitLogService commitLogService;

private final AppendMessageCallback appendMessageCallback;

//指定(topic-queueId)的邏輯offset 按順序有0->n 遞增,每producer 傳送訊息成功,即append一條訊息,加1

private HashMaptopic QueueTable=new HashMap(1024);


//true: Can lock, false : in lock.
private Atomic Boolean putMessageSpinLock=new AtomicBoolean(true);

解析-》
MappedFileQueue,是對連續物理儲存的抽象類,業務使用方可以通過訊息儲存的物理偏移量位置快速定位該offset所在的MappedFile(具體物理儲存位置的抽象)、建立、刪除MappedFile等操作。

   
    public PutMessageResult putMessage(final MessageExtBrokerInner msg) {

        // 設定訊息的儲存時間
        msg.setStoreTimestamp(System.currentTimeMillis());
        // 設定訊息的內容校驗碼
        msg.setBodyCRC(UtilAll.crc32(msg.getBody()));
        // Back to Results
        AppendMessageResult result = null;

        StoreStatsService storeStatsService = this.defaultMessageStore.getStoreStatsService();

        ...

        long eclipseTimeInLock = 0;
        MappedFile unlockMappedFile = null;

        //獲取CopyOnWriteArrayList<MappedFile>的最後一個MappedFile
        MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();

        //step1->預設使用自旋鎖,可以使用重入鎖,這裡表明一個broker,寫入緩衝區是執行緒安全的
        lockForPutMessage(); //spin...
        try {
            long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
            //該值用於isOSPageCacheBusy() 判斷
            this.beginTimeInLock = beginLockTimestamp;

            // Here settings are stored timestamp, in order to ensure an orderly
            // global
            msg.setStoreTimestamp(beginLockTimestamp);


            //step2->獲取最後一個MappedFile
            if (null == mappedFile || mappedFile.isFull()) {
                //程式碼走到這裡,說明broker的  mappedFileQueue 為初始建立或者最後一個mappedFile已滿
                //因此會重新建立一個新的mappedFile
                mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
            }
            if (null == mappedFile) {
                log.error("create maped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
                beginTimeInLock = 0;
                return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
            }

            //step3->把訊息寫入緩衝區
            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 = this.mappedFileQueue.getLastMappedFile(0);
                    if (null == mappedFile) {
                        // XXX: warn and notify me
                        log.error("create maped 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;
                    
                    ...
            }

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

        if (eclipseTimeInLock > 500) {
            log.warn("[NOTIFYME]putMessage in lock cost time(ms)={}, bodyLength={} AppendMessageResult={}", eclipseTimeInLock, msg.getBody().length, result);
        }

        if (null != unlockMappedFile && this.defaultMessageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
            this.defaultMessageStore.unlockMappedFile(unlockMappedFile);
        }

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

        // Statistics,指定topic 下已經put 訊息總條數以及總大小
        storeStatsService.getSinglePutMessageTopicTimesTotal(msg.getTopic()).incrementAndGet();
        storeStatsService.getSinglePutMessageTopicSizeTotal(topic).addAndGet(result.getWroteBytes());

        GroupCommitRequest request = null;

        //step4-> Synchronization flush 同步刷盤
        if (FlushDiskType.SYNC_FLUSH == this.defaultMessageStore.getMessageStoreConfig().getFlushDiskType()) {
            //flushCommitLogService->service 該service是處理local flush,刷盤
            final GroupCommitService service = (GroupCommitService) this.flushCommitLogService;
            //預設為true
            if (msg.isWaitStoreMsgOK()) {
                //result.getWroteOffset() = fileFromOffset + byteBuffer.position()
                request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
                //put Request 的時候喚醒GroupCommitService 執行doCommit(),遍歷  List<GroupCommitRequest> requestsWrite,執行flush
                service.putRequest(request);

                //這裡會一直等待 ,直到對應的GroupCommitRequest執行完flush 以後在喚醒,或等待超時(預設5秒)
                boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
                if (!flushOK) {
                    log.error("do groupcommit, wait for flush failed, topic: " + msg.getTopic() + " tags: " + msg.getTags()
                        + " client address: " + msg.getBornHostString());
                    putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_DISK_TIMEOUT);
                }
            } else {
                service.wakeup();
            }
        }
        // Asynchronous flush
        else {
            if (!this.defaultMessageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
                flushCommitLogService.wakeup();
            } else {
                commitLogService.wakeup();
            }
        }

       
        //step5->高可用,同步訊息 Synchronous write double
        if (BrokerRole.SYNC_MASTER == this.defaultMessageStore.getMessageStoreConfig().getBrokerRole()) {
            //該service 是委託groupTransferService  處理master  slave  同步訊息的處理類
            HAService service = this.defaultMessageStore.getHaService();
            if (msg.isWaitStoreMsgOK()) {
                // Determine whether to wait
                //result.getWroteOffset() + result.getWroteBytes()  :  這條訊息開始寫的位置 + 已寫入的位元組數
                if (service.isSlaveOK(result.getWroteOffset() + result.getWroteBytes())) {
                    if (null == request) {
                        request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());
                    }
                    //喚醒groupTransferService 呼叫doWaitTransfer(),該方法核心只是判斷HAService.this.push2SlaveMaxOffset >= req.getNextOffset() 有沒有成立;
                    //而正在的master slave 同步是委託HAConnection(master端) 與 HAClient(slave端)非同步處理的
                    service.putRequest(request);
                    //喚醒所有的HaConnection 處理訊息的傳輸
                    service.getWaitNotifyObject().wakeupAll();

                    boolean flushOK =
                        // TODO
                        // 預設5秒
                        request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());
                    if (!flushOK) {
                        log.error("do sync transfer other node, wait return, but failed, topic: " + msg.getTopic() + " tags: "
                            + msg.getTags() + " client address: " + msg.getBornHostString());
                        putMessageResult.setPutMessageStatus(PutMessageStatus.FLUSH_SLAVE_TIMEOUT);
                    }
                }
                // Slave problem
                else {
                    // Tell the producer, slave not available
                    //程式碼走到這裡,說明出現了兩種可能的情況,第一種就是producer 傳送訊息太快,導致
                    //叢集中的slave落後於master太多,或者是master同步業務訊息位元組給slave太慢。
                    //因此返回狀態結果PutMessageStatus.SLAVE_NOT_AVAILABLE,也就是希望producer
                    //客戶端針對該傳送結果做出相對應的處理。例如放慢傳送結果等。
                    putMessageResult.setPutMessageStatus(PutMessageStatus.SLAVE_NOT_AVAILABLE);
                }
            }
        }
        return putMessageResult;
    }

我們從5個步驟逐段分析putMessage(final MessageExtBrokerInner msg)是如果實現訊息儲存的。

step1,寫訊息前加鎖,使其序列化

    private void lockForPutMessage() {
        if (this.defaultMessageStore.getMessageStoreConfig().isUseReentrantLockWhenPutMessage()) {
            putMessageNormalLock.lock();
        } else {
            boolean flag;
            do {
                flag = this.putMessageSpinLock.compareAndSet(true, false);
            }
            while (!flag);
        }
    }

從程式碼中,我們可以看出,rmq使用鎖的方式有兩種,預設使用自旋;如果broker顯式配置了useReentrantLockWhenPutMessage系統屬性為true,
則使用ReentrantLock重入鎖,並且是非公平的,效能會比公平鎖要好,因為公平鎖要維持一條等待佇列。


step2,獲取最後一個MappedFile

從MappedFileQueue連續記憶體對映檔案抽象獲取最後一個即將要儲存的記憶體對映檔案MappedFile,如果該記憶體對映檔案為空或者儲存已滿,就建立一個新的MappedFile,並加入到MappedFileQueue的尾部。

      if (null == mappedFile || mappedFile.isFull()) {
                //程式碼走到這裡,說明broker的  mappedFileQueue  為空或者最後一個mappedFile已滿
                //因此會重新建立一個新的mappedFile
                mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
            }
    public MappedFile getLastMappedFile(final long startOffset) {
        return getLastMappedFile(startOffset, true);
    }
    public MappedFile getLastMappedFile(final long startOffset, boolean needCreate) {
        long createOffset = -1;
        MappedFile mappedFileLast = getLastMappedFile();

        //mappedFileSize  預設為1G

        if (mappedFileLast == null) {
            createOffset = startOffset - (startOffset % this.mappedFileSize);
        }

        if (mappedFileLast != null && mappedFileLast.isFull()) {
            createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize;
        }

        if (createOffset != -1 && needCreate) {
            // UtilAll.offset2FileName(createOffset)  這裡表示左填充0指20位為止  例如, createoffset = 123 ,UtilAll.offset2FileName(createOffset)=00000000000000000123
            String nextFilePath = this.storePath + File.separator + UtilAll.offset2FileName(createOffset);
            String nextNextFilePath = this.storePath + File.separator
                + UtilAll.offset2FileName(createOffset + this.mappedFileSize);
            MappedFile mappedFile = null;

            //預設使用非同步建立mmap,並嘗試預熱,同步等待
            if (this.allocateMappedFileService != null) {
                //this.mappedFileSize  預設為1G = 1024 * 1024 * 1024 = 1,073,741,824
                //同步連續建立兩個MappedFile
                mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath,
                    nextNextFilePath, this.mappedFileSize);
            } else {
                try {
                    mappedFile = new MappedFile(nextFilePath, this.mappedFileSize);
                } catch (IOException e) {
                    log.error("create mappedFile exception", e);
                }
            }

            if (mappedFile != null) {
                if (this.mappedFiles.isEmpty()) {
                    mappedFile.setFirstCreateInQueue(true);
                }
                this.mappedFiles.add(mappedFile);
            }

            return mappedFile;
        }

        return mappedFileLast;
    }

createOffset = mappedFileLast.getFileFromOffset() + this.mappedFileSize看出,新建的MappedFile的檔名是根據上一個MappedFilefileFromOffset加上檔案大小向左填充0,直到20位構成的,從而達到了物理位移上的連續性。

例如,
*
* 第一個MappedFile :
* fileSize = 1G (1,073,741,824)
* fileName = 00000000000000000000(0,000,000,000,0,000,000,000);
* fileFromOffset = 0->(0,1073741824)
*
*
* 第二個MappedFile :
* fileSize = 1G (1,073,741,824)
* fileName = 00000000001073741824(0,000,000,000,1,073,741,824);
* fileFromOffset = 1073741824->(1073741824,2147483648)
*
* 第三個MappedFile :
* fileSize = 1G (1,073,741,824)
* fileName = 00000000002147483648(0,000,000,000,2,147,483,648);
* fileFromOffset = 2147483648->(2147483648,3221225472)

例如訊息按序裝滿第一個MappedFile時,就會建立 第二個MappedFile,繼續存放訊息,以此類推。

我們接著看看 mappedFile = this.allocateMappedFileService.putRequestAndReturnMappedFile(nextFilePath, nextNextFilePath, this.mappedFileSize)
先說明一下,該方法是通過非同步轉同步的方式來正真建立MappedFile,繼續跟進該方法:

  public MappedFile putRequestAndReturnMappedFile(String nextFilePath, String nextNextFilePath, int fileSize) {
        int canSubmitRequests = 2;

        //transientStorePoolEnable default value :false;
        //canSubmitRequests 代表建立MappedFile的個數,如果系統使用了訊息非同步持久化的池化技術,並且
        //系統配置fastFailIfNoBufferInStorePool為true,則canSubmitRequests的數值為TransientStorePool中DirectBuffer剩餘數量與請求建立的數量差值
        //這裡也算是一種持久化控流的方式。
        if (this.messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
            if (this.messageStore.getMessageStoreConfig().isFastFailIfNoBufferInStorePool()
                && BrokerRole.SLAVE != this.messageStore.getMessageStoreConfig().getBrokerRole()) { //if broker is slave, don't fast fail even no buffer in pool
                canSubmitRequests = this.messageStore.getTransientStorePool().remainBufferNumbs() - this.requestQueue.size();
            }
        }

        //AllocateRequest代表一個建立MappedFile的請求抽象,通過CountDownLatch非同步轉同步
        AllocateRequest nextReq = new AllocateRequest(nextFilePath, fileSize);
        boolean nextPutOK = this.requestTable.putIfAbsent(nextFilePath, nextReq) == null;

        ...

        //建立第二個MappedFile,key為nextNextFilePath
        AllocateRequest nextNextReq = new AllocateRequest(nextNextFilePath, fileSize);
        boolean nextNextPutOK = this.requestTable.putIfAbsent(nextNextFilePath, nextNextReq) == null;
        ...

        
        AllocateRequest result = this.requestTable.get(nextFilePath);
        try {
            if (result != null) {
                //waitTimeOut default = 5秒,這裡會一直等待,
                //直到AllocateMappedFileService.run()-》mmapOperation()從requestQueue取出
                //AllocateRequest,建立MappedFile完畢後,才返回。
                boolean waitOK = result.getCountDownLatch().await(waitTimeOut, TimeUnit.MILLISECONDS);
                if (!waitOK) {
                    log.warn("create mmap timeout " + result.getFilePath() + " " + result.getFileSize());
                    return null;
                } else {
                    this.requestTable.remove(nextFilePath);
                    return result.getMappedFile();
                }
            } else {
                log.error("find preallocate mmap failed, this never happen");
            }
        } catch (InterruptedException e) {
            log.warn(this.getServiceName() + " service has exception. ", e);
        }

        return null;
    }

這裡有一個非常巧妙的設計,當我們需要建立一個MappedFile 時,會預先多建立一個MappedFile,也就是兩個MappedFile,前者以nextFilePath為key,後者以nextNextFilePath為key,一同放入requestTable 快取中,這樣的好處就是,如果我們下一次再建立MappedFile,直接通過下一次的nextFilePath(等於這次的nextNextFilePath),從requestTable 快取中獲取預先建立的MappedFile ,這樣一來,就不用等待MappedFile建立所帶來的延時。

我們接著看mmapOperation()

  private boolean mmapOperation() {
        boolean isSuccess = false;
        AllocateRequest req = null;
        try {
            //從請求佇列中獲取建立請求。
            req = this.requestQueue.take();

            ...

            if (req.getMappedFile() == null) {
                ...
                MappedFile mappedFile;

                //估計收費版的rmq有多種儲存引擎的實現,所以,如果預設配置了池化,則使用spi的方式
                //載入儲存引擎實現類,但開源版的rmq只有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());
                }
                ...

                // pre write mappedFile ,預熱mappedFile ,預先建立頁面對映,並且
                //鎖住pagecache,防止記憶體頁排程到交換空間(swap space),即使該程式已有一段時間沒有訪問
                //這段空間;加快寫pagecache速度
                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) {
           ...
        } finally {
            if (req != null && isSuccess)
                //喚醒同步等待的建立請求操作
                req.getCountDownLatch().countDown();
        }
        return true;
    }

mmapOperation()總結一下就是先從佇列中獲取建立MappedFile請求,接著就new MappedFile(...)真正建立例項,如果允許預熱 mappedFile.warmMappedFile(...),則進行MappedFlie預熱,最後再喚醒同步等待的建立請求操作;

重點說一下rmq是如何預熱的,mappedFile.warmMappedFile(...)跟入:

   public void warmMappedFile(FlushDiskType type, int pages) {
        long beginTime = System.currentTimeMillis();
        ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
        int flush = 0;
        long time = System.currentTimeMillis();
        //①如果系統設定為同步刷盤,則預寫MappedFile檔案
        for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
            byteBuffer.put(i, (byte) 0);
            // force flush when flush disk type is sync
            if (type == FlushDiskType.SYNC_FLUSH) {
                if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
                    flush = i;
                    mappedByteBuffer.force();
                }
            }

            ...
        }

        // 強制刷盤,當預載入完成後
        if (type == FlushDiskType.SYNC_FLUSH) {
  
                this.getFileName(), System.currentTimeMillis() - beginTime);
            mappedByteBuffer.force();
        }

        //②本地呼叫鎖頁操作。
        this.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);
        }

        {
            //當使用者態應用使用MADV_WILLNEED命令執行madvise()系統呼叫時,它會通知核心,某個檔案記憶體對映區域中的給定範圍的檔案頁不久將要被訪問。
            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);
        }
    }

①中為什麼要先預寫MappedFile檔案,然後刷盤這種操作呢?

先看看系統呼叫mlock

該系統呼叫允許程式在實體記憶體上鎖住它的部分或全部地址空間。這將阻止作業系統 將這個記憶體頁排程到交換空間(swap space),即使該程式已有一段時間沒有訪問這段空間。鎖定一個記憶體區間只需簡單將指向區間開始的指標及區間長度作為引數呼叫 mlock,被指定的區間涉及到的每個記憶體頁都將被鎖定。但,僅分配記憶體並呼叫mlock並不會為呼叫程式鎖定這些記憶體,因為對應的分頁可能是寫時複製(copy-on-write)的。因此,你應該在每個頁面中寫入一個假的值,因此,才有①中的預寫操作。

Copy-on-write 寫時複製意味著僅當程式在記憶體區間的任意位置寫入內容時,Linux 系統才會為程式建立該區記憶體的私有副本。

這裡順便舉一個Copy on write 的原理:
Copy On Write(寫時複製)使用了“引用計數”,會有一個變數用於儲存引用的數量。當第一個類構造時,string的建構函式會根據傳入的引數從堆上分配記憶體,當有其它類需要這塊記憶體時,這個計數為自動累加,當有類析構時,這個計數會減一,直到最後一個類析構時,此時的引用計數為1或是0,此時,程式才會真正的Free這塊從堆上分配的記憶體。
引用計數就是string類中寫時才拷貝的原理!

什麼情況下觸發Copy On Write(寫時複製)?
很顯然,當然是在共享同一塊記憶體的類發生內容改變時,才會發生Copy On Write(寫時複製)。比如string類的[]、=、+=、+等,還有一些string類中諸如insert、replace、append等成員函式等,包括類的析構時。

最後在說一下系統呼叫madvise
呼叫mmap()時核心只是建立了邏輯地址到實體地址的對映表,並沒有對映任何資料到記憶體。

在你要訪問資料時核心會檢查資料所在分頁是否在記憶體,如果不在,則發出一次缺頁中斷,linux預設分頁為4K,可以想象讀一個1G的訊息儲存檔案要發生多少次中斷。

解決辦法:
madvise()mmap()搭配起來使用,在使用資料前告訴核心這一段資料我要用,將其一次讀入記憶體。madvise()這個函式可以對對映的記憶體提出使用建議,從而提高記憶體。

使用過mmap對映檔案發現一個問題,search程式訪問對應的記憶體對映時,處理query的時間會有latecny會陡升,究其原因是因為mmap只是建立了一個邏輯地址,linux的記憶體分配測試都是採用延遲分配的形式,也就是隻有你真正去訪問時採用分配實體記憶體頁,並與邏輯地址建立對映,這也就是我們常說的缺頁中斷。

缺頁中斷分為兩類,一種是記憶體缺頁中斷,這種的代表是malloc,利用malloc分配的記憶體只有在程式訪問到得時候,記憶體才會分配;另外就是硬碟缺頁中斷,這種中斷的代表就是mmap,利用mmap對映後的只是邏輯地址,當我們的程式訪問時,核心會將硬碟中的檔案內容讀進實體記憶體頁中,這裡我們就會明白為什麼mmap之後,訪問記憶體中的資料延時會陡增。

出現問題解決問題,上述情況出現的原因本質上是mmap對映檔案之後,實際並沒有載入到記憶體中,要解決這個檔案,需要我們進行索引的預載入,這裡就會引出本文講到的另一函式madvise,這個函式會傳入一個地址指標,已經一個區間長度,madvise會向核心提供一個針對於於地址區間的I/O的建議,核心可能會採納這個建議,會做一些預讀的操作。

rmq中的使用int ret = LibC.INSTANCE.madvise(pointer, new NativeLong(this.fileSize), LibC.MADV_WILLNEED);

例如,當使用者態應用使用MADV_WILLNEED命令執行madvise()系統呼叫時,它會通知核心,某個檔案記憶體對映區域中的給定範圍的檔案頁不久將要被訪問。

上述,rmq為了建立一個MappedFile,做出了這麼多系統級別的優化。


step3,把訊息寫入緩衝區

mappedFile.appendMessage(msg, this.appendMessageCallback)

    public AppendMessageResult appendMessage(final MessageExtBrokerInner msg, final AppendMessageCallback cb) {
        assert msg != null;
        assert cb != null;

        //wrotePosition 有MappedFile 自行維護當前的寫位置
        int currentPos = this.wrotePosition.get();


        if (currentPos < this.fileSize) {
            //寫入臨時緩衝區
            ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
            byteBuffer.position(currentPos);
            AppendMessageResult result =
                cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, msg);
            //新增寫位置,大小為寫入訊息的大小
            this.wrotePosition.addAndGet(result.getWroteBytes());
            this.storeTimestamp = result.getStoreTimestamp();
            return result;
        }

        log.error("MappedFile.appendMessage return null, wrotePosition: " + currentPos + " fileSize: "
            + this.fileSize);
        return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
    }

     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();

            //hostHolder.length = 8
            this.resetByteBuffer(hostHolder, 8);

            //構造 記憶體中的msgId  =  addr(high) + offset(low)
            //addr(8 bytes) = socket ip(4 bytes) + port (4 bytes)
            //msgId(16 bytes) = addr(8 bytes)(high) + wroteOffset(8 bytes)(low)
            String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(hostHolder), wroteOffset);


            // Record ConsumeQueue information
            keyBuilder.setLength(0);
            keyBuilder.append(msgInner.getTopic());
            keyBuilder.append('-');
            keyBuilder.append(msgInner.getQueueId());
            String key = keyBuilder.toString();

            //指定(topic-queueId)的邏輯offset 按順序有0->n 遞增,每producer 傳送訊息成功,即append一條訊息,加1
            Long queueOffset = CommitLog.this.topicQueueTable.get(key);
            if (null == queueOffset) {
                queueOffset = 0L;
                CommitLog.this.topicQueueTable.put(key, queueOffset);
            }

            // Transaction messages that require special handling
            final int tranType = MessageSysFlag.getTransactionValue(msgInner.getSysFlag());
            switch (tranType) {
                // Prepared and Rollback message is not consumed, will not enter the
                // consumer queuec
                case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
                case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
                    queueOffset = 0L;
                    break;
                case MessageSysFlag.TRANSACTION_NOT_TYPE:
                case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
                default:
                    break;
            }

            /**
             * Serialize message
             */

            //獲取擴充套件欄位位元組
            final byte[] propertiesData =
                msgInner.getPropertiesString() == null ? null : msgInner.getPropertiesString().getBytes(MessageDecoder.CHARSET_UTF8);

            final int propertiesLength = propertiesData == null ? 0 : propertiesData.length;

            if (propertiesLength > Short.MAX_VALUE) {
                log.warn("putMessage message properties length too long. length={}", propertiesData.length);
                return new AppendMessageResult(AppendMessageStatus.PROPERTIES_SIZE_EXCEEDED);
            }

            //計算topic位元組
            final byte[] topicData = msgInner.getTopic().getBytes(MessageDecoder.CHARSET_UTF8);
            final int topicLength = topicData.length;

            //獲取訊息體內容位元組
            final int bodyLength = msgInner.getBody() == null ? 0 : msgInner.getBody().length;

            //計算訊息總長度
            final int msgLen = calMsgLength(bodyLength, topicLength, propertiesLength);

            // Exceeds the maximum message
            if (msgLen > this.maxMessageSize) {
                CommitLog.log.warn("message size exceeded, msg total size: " + msgLen + ", msg body size: " + bodyLength
                    + ", maxMessageSize: " + this.maxMessageSize);
                return new AppendMessageResult(AppendMessageStatus.MESSAGE_SIZE_EXCEEDED);
            }

            //這裡保證每個檔案都會儲存完整的一條訊息,如果改檔案的所剩空間不足存放這條訊息則向外層
            //呼叫返回END_OF_FILE,然後在由外層呼叫建立新的MappedFile,存放該條訊息
            // Determines whether there is sufficient free space
            if ((msgLen + END_FILE_MIN_BLANK_LENGTH) > maxBlank) {
                this.resetByteBuffer(this.msgStoreItemMemory, maxBlank);
                // 1 TOTALSIZE
                this.msgStoreItemMemory.putInt(maxBlank);
                // 2 MAGICCODE
                this.msgStoreItemMemory.putInt(CommitLog.BLANK_MAGIC_CODE);
                // 3 The remaining space may be any value
                //

                // Here the length of the specially set maxBlank
                final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
                byteBuffer.put(this.msgStoreItemMemory.array(), 0, maxBlank);
                return new AppendMessageResult(AppendMessageStatus.END_OF_FILE, wroteOffset, maxBlank, msgId, msgInner.getStoreTimestamp(),
                    queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);
            }

            // Initialization of storage space
            this.resetByteBuffer(msgStoreItemMemory, msgLen);
            // 1 TOTALSIZE
            this.msgStoreItemMemory.putInt(msgLen);
            // 2 MAGICCODE
            this.msgStoreItemMemory.putInt(CommitLog.MESSAGE_MAGIC_CODE);
            // 3 BODYCRC
            this.msgStoreItemMemory.putInt(msgInner.getBodyCRC());
            // 4 QUEUEID
            this.msgStoreItemMemory.putInt(msgInner.getQueueId());
            // 5 FLAG
            this.msgStoreItemMemory.putInt(msgInner.getFlag());
            // 6 QUEUEOFFSET
            this.msgStoreItemMemory.putLong(queueOffset);
            // 7 PHYSICALOFFSET :當前訊息的開始寫入位置
            this.msgStoreItemMemory.putLong(fileFromOffset + byteBuffer.position());
            // 8 SYSFLAG
            this.msgStoreItemMemory.putInt(msgInner.getSysFlag());
            // 9 BORNTIMESTAMP
            this.msgStoreItemMemory.putLong(msgInner.getBornTimestamp());
            // 10 BORNHOST
            this.resetByteBuffer(hostHolder, 8);
            this.msgStoreItemMemory.put(msgInner.getBornHostBytes(hostHolder));
            // 11 STORETIMESTAMP
            this.msgStoreItemMemory.putLong(msgInner.getStoreTimestamp());
            // 12 STOREHOSTADDRESS
            this.resetByteBuffer(hostHolder, 8);
            this.msgStoreItemMemory.put(msgInner.getStoreHostBytes(hostHolder));
            //this.msgStoreItemMemory.put(msgInner.getStoreHostBytes());
            // 13 RECONSUMETIMES
            this.msgStoreItemMemory.putInt(msgInner.getReconsumeTimes());
            // 14 Prepared Transaction Offset
            this.msgStoreItemMemory.putLong(msgInner.getPreparedTransactionOffset());
            // 15 BODY
            this.msgStoreItemMemory.putInt(bodyLength);
            if (bodyLength > 0)
                this.msgStoreItemMemory.put(msgInner.getBody());
            // 16 TOPIC
            this.msgStoreItemMemory.put((byte) topicLength);
            this.msgStoreItemMemory.put(topicData);
            // 17 PROPERTIES
            this.msgStoreItemMemory.putShort((short) propertiesLength);
            if (propertiesLength > 0)
                this.msgStoreItemMemory.put(propertiesData);

            final long beginTimeMills = CommitLog.this.defaultMessageStore.now();
            // Write messages to the queue buffer
            byteBuffer.put(this.msgStoreItemMemory.array(), 0, msgLen);

            AppendMessageResult result = new AppendMessageResult(AppendMessageStatus.PUT_OK, wroteOffset, msgLen, msgId,
                msgInner.getStoreTimestamp(), queueOffset, CommitLog.this.defaultMessageStore.now() - beginTimeMills);

            switch (tranType) {
                case MessageSysFlag.TRANSACTION_PREPARED_TYPE:
                case MessageSysFlag.TRANSACTION_ROLLBACK_TYPE:
                    break;
                case MessageSysFlag.TRANSACTION_NOT_TYPE:
                case MessageSysFlag.TRANSACTION_COMMIT_TYPE:
                    // The next update ConsumeQueue information
                    CommitLog.this.topicQueueTable.put(key, ++queueOffset);
                    break;
                default:
                    break;
            }
            return result;
        }

這裡就按照 rocket mq 底層儲存原始碼分析(1) 中的訊息內容體逐個寫入pagecache,一般情況下會使用MappedByteBuffer,如果使用池化方式的話,則使用從池中借來DirectBuffer來實現寫入pagecache。當然,在寫入前,會判斷當前的MappedFile剩餘的空間是否足夠寫入當前訊息,如果不夠就會返回EOF。如果訊息成功寫入pagecache,則遞增加1訊息的邏輯位移。最後在更新當前MappedFilewrotePosition,更新的值為原值加上訊息寫入pagecache的總長度。


step4,同步刷盤

這裡我們只分析同步,非同步刷盤比同步刷盤要簡單,因此,讀者可以自行分析非同步刷盤的實現。

這裡先說一下,rmq使用生產消費模式,非同步轉同步的方式實現同步刷盤,GroupCommitService就是刷盤消費執行緒的抽象,該執行緒會從請求佇列獲取GroupCommitRequest刷盤請求,執行刷盤邏輯。

看看程式碼片段:

//①建立刷盤請求
request = new GroupCommitRequest(result.getWroteOffset() + result.getWroteBytes());

//②建立刷盤請求,put Request 的時候喚醒GroupCommitService 執行doCommit(),遍歷  List<GroupCommitRequest> requestsWrite,執行flush
service.putRequest(request);

//③這裡會一直等待 ,直到對應的GroupCommitRequest執行完flush 以後在喚醒,或等待超時(預設5秒)
boolean flushOK = request.waitForFlush(this.defaultMessageStore.getMessageStoreConfig().getSyncFlushTimeout());

程式碼片段中,①建立刷盤請求;②放入快取佇列中:

  //②放入快取佇列中:
  public void putRequest(final GroupCommitRequest request) {
        synchronized (this) {
            this.requestsWrite.add(request);
            if (hasNotified.compareAndSet(false, true)) {
                waitPoint.countDown(); // notify
            }
        }
    }

    public void run() {
        
        while (!this.isStopped()) {
            try {
                this.waitForRunning(10);
                this.doCommit();
            }
            ...
        }

    ...
    }

    protected void waitForRunning(long interval) {
        if (hasNotified.compareAndSet(true, false)) {
            this.onWaitEnd();
            return;
        }

        //entry to wait
        waitPoint.reset();

        try {
            waitPoint.await(interval, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            hasNotified.set(false);
            this.onWaitEnd();
        }
    }

    protected void onWaitEnd() {
        this.swapRequests();
    }

    private void swapRequests() {
        List<GroupCommitRequest> tmp = this.requestsWrite;
        this.requestsWrite = this.requestsRead;
        this.requestsRead = tmp;
    }

結合上述程式碼片段,我們分析刷盤生產者commitLog是如何同步等待GroupCommitService非同步刷盤消費者結果的。

commitLog,也就是刷盤生產者主體通過service.putRequest(...)把請求放入刷盤佇列後, request.waitForFlush(...)等待刷盤結果;而putRequest(...)後,會主動喚醒等待在waitPoint處的GroupCommitService,然後再swapRequests()交換讀寫佇列,消費刷盤請求佇列。

這裡,說一下rmq使用雙快取佇列來實現讀寫分離,這樣做的好處就是內部消費生產刷盤請求均不用加鎖。

我們看看GroupCommitService非同步刷盤消費者是如何commit的:

     private void doCommit() {
            if (!this.requestsRead.isEmpty()) {
                for (GroupCommitRequest req : this.requestsRead) {
                    // There may be a message in the next file, so a maximum of
                    // two times the flush
                    boolean flushOK = false;
                    for (int i = 0; i < 2 && !flushOK; i++) {
                        flushOK = CommitLog.this.mappedFileQueue.getFlushedWhere() >= req.getNextOffset();

                        if (!flushOK) {
                            CommitLog.this.mappedFileQueue.flush(0);
                        }
                    }

                    req.wakeupCustomer(flushOK);
                }

                ...
                this.requestsRead.clear();
        ...
        }

遍歷刷盤請求佇列,委託mappedFileQueue連續儲存抽象執行刷盤;每條訊息嘗試兩次刷盤,繼續跟入

CommitLog.this.mappedFileQueue.flush(0):

    public boolean flush(final int flushLeastPages) {
        boolean result = true;
        //通過flushedWhere 開始刷盤的位置找出相對應的mappedFile,flushedWhere為訊息與訊息的儲存分解線
        MappedFile mappedFile = this.findMappedFileByOffset(this.flushedWhere, false);
        if (mappedFile != null) {
            long tmpTimeStamp = mappedFile.getStoreTimestamp();
            //執行物理刷屏,並且返回最大的已刷盤的offset
            //flushLeastPages為0的話,說明只要this.wrotePosition.get()/this.committedPosition.get() > flushedPosition均可以刷盤,即持久化
            int offset = mappedFile.flush(flushLeastPages);
            long where = mappedFile.getFileFromOffset() + offset;
            result = where == this.flushedWhere;
            //持久化後,返回已刷盤的最大位置;即 (flushedWhere == committedPosition.get() == flushedPosition.get())
            this.flushedWhere = where;
            if (0 == flushLeastPages) {
                this.storeTimestamp = tmpTimeStamp;
            }
        }

        return result;
    }

這裡總結一下flush(...)流程,先通過當前的邏輯刷盤位置flushedWhere找出連續對映檔案抽象mappedFileQueue的具體MappedFile;接著委託MappedFile對映檔案執行mappedFile.flush(...)刷盤操作;最後在更新連續對映檔案抽象的flushedWhere,更新的值為原值加上刷盤訊息的大小。

我們看看findMappedFileByOffset(this.flushedWhere, false)如何通過flushedWhere找出具體MappedFile

  public MappedFile findMappedFileByOffset(final long offset, final boolean returnFirstOnNotFound) {
        try {
            MappedFile mappedFile = this.getFirstMappedFile();
            if (mappedFile != null) {
                //mappedFileSize  預設為1G
                int index = (int) ((offset / this.mappedFileSize) - (mappedFile.getFileFromOffset() / this.mappedFileSize));
                ...
                return this.mappedFiles.get(index);
            }
             ...
    }

這裡的核心演算法為:(int) ((offset / this.mappedFileSize) - (mappedFile.getFileFromOffset() / this.mappedFileSize));

簡單說一下該演算法,先獲取MappedFileQueued的首個MappedFile的fileFromOffset,也即是建立該MappedFile 的物理偏移量位置,而offset 為指定條訊息的開始物理偏移量位置,該值是由當前所在檔案的fileFromOffset + 前一條訊息的最後一個位元組所在的位置算出。

例如,第一個mappedFile的的fileFromOffset 為零 ,該mappedFile儲存訊息滿以後,會建立第二個mappedFile,第二個mappedFile的fileFromOffset 為1073741824,以此類推,第三個mappedFile的fileFromOffset 為2147483648;

假設該訊息為第三個mappedFile 的第二個訊息:
1st_Msg.offset = 3rd_mappedFile.fileFromOffset
2nd_Msg.offset = 3rd_mappedFile.fileFromOffset + 1st_Msg.size;

然後通過(int)[(offset - fileFromOffset)/ mappedFileSize]即可算出MappedFile 所在的index位置。

最後在看看 mappedFile.flush(flushLeastPages),執行底層刷盤:

    public int flush(final int flushLeastPages) {
        if (this.isAbleToFlush(flushLeastPages)) {
            if (this.hold()) {
                int value = getReadPosition();

                try {
                    //We only append data to fileChannel or mappedByteBuffer, never both.
                    if (writeBuffer != null || this.fileChannel.position() != 0) {
                        this.fileChannel.force(false);
                    } else {
                        this.mappedByteBuffer.force();
                    }
                } catch (Throwable e) {
                    log.error("Error occurred when force data to disk.", e);
                }
                //刷完盤以後使flushedPosition = commitPosition
                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();
    }

分析一下,同步刷盤的情況下this.isAbleToFlush(flushLeastPages)會一直返回true;只有在非同步刷盤時,訊息在寫滿pagecache時,才允許刷盤。

接著,在刷盤前,先增加引用this.hold(),上文分析到用引用計數器來執行MappedFile回收。

如果使用池化技術,則委託fileChannel呼叫底層的force刷盤操作,否則,通過mappedByteBuffer執行底層刷盤操作。

最後在釋放應用。

到此為止,訊息的刷盤已分析完畢。

step5,高可用,master同步slave

該小結會專門寫一篇來分析,詳細請看:

總結:

經過上述分析,我們是不是已經清楚rmq是如何進行訊息持久化的。

相關文章