rocketMQ消息存儲源碼

MapedFile

  • public static final int OS_PAGE_SIZE= 1024 * 4; —操作系統每頁大小,默認4K
  • private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY= new AtomicLong(0); —當前JVM中已經map的虛擬內存
  • private static final AtomicInteger TOTAL_MAPPED_FILES= new AtomicInteger(0);--當前JVM中已經map的MappedFIle對象個數
  • protected final AtomicInteger wrotePosition= new AtomicInteger(0);
    //ADD BY ChenYang --當前文件的寫指針(writeBuffer)
  • protected final AtomicInteger committedPosition = new AtomicInteger(0);--當前文件的提交指針
  • private final AtomicInteger flushedPosition = new AtomicInteger(0); -刷寫道磁盤的指針
  • protected int fileSize;--文件大小
  • protected FileChannel fileChannel; --文件通道
    //**/
    / * Message will put to here first, and then rebut to FileChannel if writeBuffer is not null./
    / *//
  • protected ByteBuffer writeBuffer = null; --堆內存byte buffer,如果不爲空(transientStorePoolEnable=true),則提交的數據先緩存在這裏,再提交到mappedFIle對應的內存映射文件buffer
  • protected TransientStorePool transientStorePool= null; --堆內存池,ransientStorePoolEnable=true時啓用
  • private String fileName;--文件名
  • private long fileFromOffset;--文件初始偏移量,也就是文件名
  • private File file;--物理文件
  • private MappedByteBuffer mappedByteBuffer; --物理文件對應的內存映射buffer
  • private volatile long storeTimestamp = 0; --文件最後一次寫入時間
  • private boolean firstCreateInQueue = false; --是否時mappedFileQUe隊列中第一個

transientStorePoolEnable=true,多了一個commit流程
數據流向:數據——》writeBuffer——》fileChannel——》force到磁盤
transientStorePoolEnable=false
數據流向:數據-》mappedFileBuffer-》force到磁盤

init

  1. transientStorePoolEnable不爲true時,通過radomFileAccess創建文件通道
this.fileChannel = new RandomAccessFile(this.file, “rw”).getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode./READ_WRITE/, 0, fileSize);
  1. transientStorePoolEnable=true時先初始化writeBuffer
this.writeBuffer = transientStorePool.borrowBuffer();
this.transientStorePool = transientStorePool;

提交(從write buffer->FileChannel)

commit方法

ByteBuffer byteBuffer = writeBuffer.slice();
byteBuffer.position(lastCommittedPosition);
byteBuffer.limit(writePos);
this.fileChannel.position(lastCommittedPosition);
this.fileChannel.write(byteBuffer);
this.committedPosition.set(writePos);

  • slice方法創建一個共享緩存區,position爲上一次提交位置,limit爲上一次寫入位置,把這區間的數據寫入fileChannel

刷盤 flush

if (writeBuffer != null || this.fileChannel.position() != 0) {
    this.fileChannel.force(false);
} else {
    this.mappedByteBuffer.force();
}

調用fileChannel或mappedByteBuffer的force方法寫入磁盤。
當前最大讀指針,如果writeBuffer爲空則是當前的寫指針,否則爲提交指針,認爲只有提交到mappedByteBuffer或fileChannel的數據纔是安全的數據

//**/
/ */*@return*/The max position which have valid data/
/ *//
public int getReadPosition() {
    return this.writeBuffer == null ? this.wrotePosition.get() : this.committedPosition.get();
}

獲取selectMappedBuffer

ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
byteBuffer.position(pos);
ByteBuffer byteBufferNew = byteBuffer.slice();
byteBufferNew.limit(size);
return new SelectMappedBufferResult(this.fileFromOffset + pos, byteBufferNew, size, this);

銷燬

TransientStorePool

用來存儲臨時數據,數據先寫入臨時內存映射文件中,再由commit線程定時從這個內存複製到與物理文件對應的內存映射中。主要原因是提供一種內存鎖定,將當前堆外內存一隻鎖定在內存中,避免被進程將內存交換到磁盤。

//**/
/ * It’s a heavy init method./
/ *//
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);
        LibC./INSTANCE/.mlock(pointer, new NativeLong(fileSize));//將這批內存鎖定,避免置換到交換區,提高存儲性能

        availableBuffers.offer(byteBuffer);
    }
}

引入兩種內存刷入機制原因:
java - FileChannel 和 MappedByteBuffer 實現上有什麼不同?爲什麼性能差這麼多? - SegmentFault 思否

commitLog

getMessage

public SelectMappedBufferResult getMessage(final long offset, final int size) {
    int mappedFileSize = this.defaultMessageStore.getMessageStoreConfig().getMapedFileSizeCommitLog();
    MappedFile mappedFile = this.mappedFileQueue.findMappedFileByOffset(offset, offset == 0);
    if (mappedFile != null) {
        int pos = (int) (offset % mappedFileSize);
        return mappedFile.selectMappedBuffer(pos, size);
    }
    return null;
}

CommitLog,MappedFileQueue,MappedFile關係

commitLog對應commitLog文件,文件下有多個最大長度相等的消息文件,這些文件每個對應一個mappedFIle,mappedFIleQueue負責管理這些mappedFile

ConsumeQueue

相當於消息消費的索引文件,由於commitLog中不同topic不規則存放,利用這個consumequeue可以清晰得到消費者訂閱的topic下的消息隊列。

每個consumeQueue的條目只保存commitLogOffset(邏輯偏移量),size和tagHashSet,共20字節。一個consumeQueue可以看作多個comsumeQueue條目的數組。

consume queue文件結構爲:一個consume queue下面有多個topic目錄,每個topic目錄下多個消息隊列文件夾,每個消息隊列文件夾對應一個消息隊列,消息隊列目錄對應一個MappedFileQueue,每個文件對應MappedFileQueue。每個文件30w個條目,單個文件大小爲30w*20字節

消息到達commitLog之後,有專門的線程產生消息轉發任務,構建consumeQueue和index文件

索引文件

索引文件結構爲:

  • 40字節的IndexHeader,包含該索引文件的全局信息
  • 500w個hash槽,每個hash槽4個字節,存儲的是hashcode落在該hash槽的最新的index索引條目的下標
  • 2000w個 index條目列表,包含key的hash值,commit log的物理偏移量等

寫入 putKey()

public boolean putKey(final String key, final long phyOffset, final long storeTimestamp) {
    if (this.indexHeader.getIndexCount() < this.indexNum) {
        int keyHash = indexKeyHashMethod(key);
        int slotPos = keyHash % this.hashSlotNum;
        int absSlotPos = IndexHeader./INDEX_HEADER_SIZE/+ slotPos * /hashSlotSize/;

        FileLock fileLock = null;

        try {

            // fileLock = this.fileChannel.lock(absSlotPos, hashSlotSize,
            // false);
            int slotValue = this.mappedByteBuffer.getInt(absSlotPos); //slotValue 是當前hash槽中的值,也是最後一個hashIndex的下標
            if (slotValue <= /invalidIndex/|| slotValue > this.indexHeader.getIndexCount()) {
                slotValue = /invalidIndex/;
            }

            long timeDiff = storeTimestamp - this.indexHeader.getBeginTimestamp();

            timeDiff = timeDiff / 1000;

            if (this.indexHeader.getBeginTimestamp() <= 0) {
                timeDiff = 0;
            } else if (timeDiff > Integer./MAX_VALUE/) {
                timeDiff = Integer./MAX_VALUE/;
            } else if (timeDiff < 0) {
                timeDiff = 0;
            }

            int absIndexPos =
                IndexHeader./INDEX_HEADER_SIZE/+ this.hashSlotNum * /hashSlotSize/
+ this.indexHeader.getIndexCount() * /indexSize/;//absIndexPos是index條目的物理偏移量

            this.mappedByteBuffer.putInt(absIndexPos, keyHash);//存儲的是key的hash而不是key的值,可以將索引條目設計爲定長結構
            this.mappedByteBuffer.putLong(absIndexPos + 4, phyOffset);
            this.mappedByteBuffer.putInt(absIndexPos + 4 + 8, (int) timeDiff);
            this.mappedByteBuffer.putInt(absIndexPos + 4 + 8 + 4, slotValue);

            this.mappedByteBuffer.putInt(absSlotPos, this.indexHeader.getIndexCount());//設置hash槽中的值爲最新索引條目下標

            if (this.indexHeader.getIndexCount() <= 1) {
                this.indexHeader.setBeginPhyOffset(phyOffset);
                this.indexHeader.setBeginTimestamp(storeTimestamp);
            }

            this.indexHeader.incHashSlotCount();
            this.indexHeader.incIndexCount();
            this.indexHeader.setEndPhyOffset(phyOffset);
            this.indexHeader.setEndTimestamp(storeTimestamp);

            return true;
        }	
			。。。
    return false;
}

獲取-- selectPhyOffset()

根據傳入的key值獲取hashcode,定位hash槽,拿到index條目下標,根據index條目一次拿到上一個條目下標,每個index條目的物理偏移量存儲起來

checkpoint文件

記錄commitLog,indexFile,consumeQueue文件的刷盤時間

abort 文件

rocketMQ在啓動時創建abort文件,mq正常退出時會調用JVM鉤子函數清除這個文件,如果這個文件存在,說明異常退出,文件數據不同步,需要恢復數據

實時更新consumeQueue和indexFile文件

在DefaultMEssageStore啓動的時候開啓ReputMessageService線程。如果允許重複轉發,起始偏移量爲commitLog的提交指針,否則爲commitLog內存最大偏移量

ReputMessageService爲DefaultMessafeStore內部類。

文件刷盤機制

同步刷盤SYNC_FLUSH

消息追加到內存映射文件的內存中就立刻刷寫到磁盤
commitLog.handleDiskFlush(),實際執行爲GroupCommitService類。
同步等待waitForFlush(),刷盤成功之後awake。

異步刷盤ASYNC_FLUSH,默認爲異步刷盤

索引文件不是採用的定式異步刷盤,而是每更新一次索引文件就將上一次的更新刷到磁盤
根據transientStorePoolEnable不同刷盤機制有細微差別。

爲true時,
消息首先追加到writeBuffer,commitRealTimeService線程牧人每200ms將新追加的數據提交到mappedByteBuffer中
FlushRealTimeService線程默認每500ms醬MappedByteBuffer中新追加的內容調用force方法刷到磁盤

過期文件清除機制

commitLog,consumeQueue都是順序寫,如果非當前操作文件在一定時間內沒有被更新,則認爲是過期文件,可以被清除,不管這個文件上的消息是否全部被消費
默認過期時間是72小時,可以在Broker的fileReservedTime配置中修改。

private void addScheduleTask() {

    this.scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
        @Override
        public void run() {
            DefaultMessageStore.this.cleanFilesPeriodically();
        }
    }, 1000 * 60, this.messageStoreConfig.getCleanResourceInterval(), TimeUnit./MILLISECONDS/);

每隔10s調度一次cleanFilesPeriodically(),檢測是否需要清除過期文件

private void cleanFilesPeriodically() {
    this.cleanCommitLogService.run();
    this.cleanConsumeQueueService.run();
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章