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
- transientStorePoolEnable不爲true時,通過radomFileAccess創建文件通道
this.fileChannel = new RandomAccessFile(this.file, “rw”).getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode./READ_WRITE/, 0, fileSize);
- 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();
}