一、存儲總體結構
從上面的圖中可以看出,Broker都是通過DefaultMessageStore實現(xiàn)數(shù)據(jù)的存儲和讀取。消息的存儲主要是通過調用DefaultMessageStore.putMessage(),實現(xiàn)消息的存儲。
二、消息存儲過程
本節(jié)將以消息發(fā)送存儲為突破點,一點一點揭開RocketMQ 存儲設計的神秘面紗。消息存儲入口: org.apache.rocketmq.store.DefaultMessageStore#putMessage 。
public PutMessageResult putMessage(MessageExtBrokerInner msg) {
if (this.shutdown) {
log.warn("message store has shutdown, so putMessage is forbidden");
return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
}
// 如果當前節(jié)點是從節(jié)點,拒絕寫
if (BrokerRole.SLAVE == this.messageStoreConfig.getBrokerRole()) {
long value = this.printTimes.getAndIncrement();
if ((value % 50000) == 0) {
log.warn("message store is slave mode, so putMessage is forbidden ");
}
return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
}
// 當前broker狀態(tài),是否可寫
if (!this.runningFlags.isWriteable()) {
long value = this.printTimes.getAndIncrement();
if ((value % 50000) == 0) {
log.warn("message store is not writeable, so putMessage is forbidden " + this.runningFlags.getFlagBits());
}
return new PutMessageResult(PutMessageStatus.SERVICE_NOT_AVAILABLE, null);
} else {
this.printTimes.set(0);
}
// 消息主題長度限制
if (msg.getTopic().length() > Byte.MAX_VALUE) {
log.warn("putMessage message topic length too long " + msg.getTopic().length());
return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, null);
}
// 消息屬性限制
if (msg.getPropertiesString() != null && msg.getPropertiesString().length() > Short.MAX_VALUE) {
log.warn("putMessage message properties length too long " + msg.getPropertiesString().length());
return new PutMessageResult(PutMessageStatus.PROPERTIES_SIZE_EXCEEDED, null);
}
// 是否os cache刷新繁忙
if (this.isOSPageCacheBusy()) {
return new PutMessageResult(PutMessageStatus.OS_PAGECACHE_BUSY, null);
}
// 寫數(shù)據(jù)
long beginTime = this.getSystemClock().now();
PutMessageResult result = this.commitLog.putMessage(msg);
long eclipseTime = this.getSystemClock().now() - beginTime;
if (eclipseTime > 500) {
log.warn("putMessage not in lock eclipse time(ms)={}, bodyLength={}", eclipseTime, msg.getBody().length);
}
this.storeStatsService.setPutMessageEntireTimeMax(eclipseTime);
if (null == result || !result.isOk()) {
this.storeStatsService.getPutMessageFailedTimes().incrementAndGet();
}
return result;
}
獲取到對應的MappedFile寫數(shù)據(jù)
MappedFile unlockMappedFile = null;
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
在寫入CommitLog 之前,先申請putMessageLock,也就是將消息存儲到CornrnitLog 文件中是串行的。
putMessageLock.lock(); //spin or ReentrantLock ,depending on store config
try {
long beginLockTimestamp = this.defaultMessageStore.getSystemClock().now();
this.beginTimeInLock = beginLockTimestamp;
// Here settings are stored timestamp, in order to ensure an orderly
// global
msg.setStoreTimestamp(beginLockTimestamp);
if (null == mappedFile || mappedFile.isFull()) {
mappedFile = this.mappedFileQueue.getLastMappedFile(0); // Mark: NewFile may be cause noise
}
if (null == mappedFile) {
log.error("create mapped file1 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
beginTimeInLock = 0;
return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, null);
}
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 mapped file2 error, topic: " + msg.getTopic() + " clientAddr: " + msg.getBornHostString());
beginTimeInLock = 0;
return new PutMessageResult(PutMessageStatus.CREATE_MAPEDFILE_FAILED, result);
}
// 寫數(shù)據(jù)
result = mappedFile.appendMessage(msg, this.appendMessageCallback);
break;
case MESSAGE_SIZE_EXCEEDED:
case PROPERTIES_SIZE_EXCEEDED:
beginTimeInLock = 0;
return new PutMessageResult(PutMessageStatus.MESSAGE_ILLEGAL, result);
case UNKNOWN_ERROR:
beginTimeInLock = 0;
return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
default:
beginTimeInLock = 0;
return new PutMessageResult(PutMessageStatus.UNKNOWN_ERROR, result);
}
eclipseTimeInLock = this.defaultMessageStore.getSystemClock().now() - beginLockTimestamp;
beginTimeInLock = 0;
} finally {
putMessageLock.unlock();
}
調用appendMessagesInner寫數(shù)據(jù)
public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
assert messageExt != null;
assert cb != null;
int currentPos = this.wrotePosition.get();
if (currentPos < this.fileSize) {
// 調用slice共享緩沖區(qū)
ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
// 寫入位置
byteBuffer.position(currentPos);
AppendMessageResult result = null;
if (messageExt instanceof MessageExtBrokerInner) {
// 追加消息
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
} else if (messageExt instanceof MessageExtBatch) {
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBatch) messageExt);
} else {
return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}
// 更新寫入位置
this.wrotePosition.addAndGet(result.getWroteBytes());
this.storeTimestamp = result.getStoreTimestamp();
return result;
}
log.error("MappedFile.appendMessage return null, wrotePosition: {} fileSize: {}", currentPos, this.fileSize);
return new AppendMessageResult(AppendMessageStatus.UNKNOWN_ERROR);
}
創(chuàng)建全局唯一id,消息ID 有16 字節(jié),消息ID 組成如圖4-4 所示
// PHY OFFSET
long wroteOffset = fileFromOffset + byteBuffer.position();
this.resetByteBuffer(hostHolder, 8);
String msgId = MessageDecoder.createMessageId(this.msgIdMemory, msgInner.getStoreHostBytes(hostHolder), wroteOffset);
消息存儲格式
RocketMQ 消息存儲格式如下。
- TOTALSIZE : 該消息條目總長度,4 字節(jié)。
- MAGICCODE : 魔數(shù), 4 字節(jié)。固定值Oxdaa320a7 。
- BODYCRC : 消息體crc校驗碼, 4 字節(jié)。
- QUEUEID : 消息消費隊列ID , 4 字節(jié)。
- FLAG : 消息FLAG , RocketMQ 不做處理, 供應用程序使用,默認4 字節(jié)。
- QUEUEOFFSET :消息在消息消費隊列的偏移量, 8 字節(jié)。
- PHYSICALOFFSET : 消息在CommitLog 文件中的偏移量, 8 字節(jié)。
- SYSFLAG : 消息系統(tǒng)Flag ,例如是否壓縮、是否是事務消息等, 4 字節(jié)。
- BORNTIMESTAMP : 消息生產者調用消息發(fā)送API 的時間戳, 8 字節(jié)。
- BORNHOST :消息發(fā)送者IP 、端口號, 8 字節(jié)。
- STORETIMESTAMP : 消息存儲時間戳, 8 字節(jié)。
- STOREHOSTADDRESS: Broker 服務器IP+端口號, 8 字節(jié)。
- RECONSUMETIMES : 消息重試次數(shù), 4 字節(jié)。
- Prepared Transaction Offset : 事務消息物理偏移量, 8 字節(jié)。
- BodyLength :消息體長度, 4 字節(jié)。
- Body : 消息體內容,長度為bodyLen th 中存儲的值。
- TopieLength : 主題存儲長度, 1 字節(jié),表示主題名稱不能超過255 個字符。
- Topic : 主題,長度為TopieL e n g th 中存儲的值。
- PropertiesLength : 消息屬性長度, 2 字節(jié), 表示消息屬性長度不能超過6 553 6 個字符。
- Properties : 消息屬性,長度為PropertiesLength 中存儲的值。
上述表示CommitLog 條目是不定長的,每一個條目的長度存儲在前4 個字節(jié)中。
如果消息長度+END_FILE_MIN_BLANK_LENGTH 大于CommitLog 文件的空閑空間,則返回AppendMessageStatus.END_OF_FILE, Broker 會重新創(chuàng)建一個新的CommitLog 文件來存儲該消息。
從這里可以看出,每個CommitLog 文件最少會空閑8個字節(jié),高4 字節(jié)存儲當前文件剩余空間,低4 字節(jié)存儲魔數(shù): CommitLog.BLANK_MAGIC_CODE 。
// 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);
}
將消息內容存儲到ByteBuffer 中,然后創(chuàng)建AppendMessageResult 。這里只是將消息存儲在MappedFile 對應的內存映射Buffer 中,并沒有刷寫到磁盤。
DefaultAppendMessageCallback#doAppend 只是將消息追加在內存中, 需要根據(jù)是同步刷盤還是異步刷盤方式,將內存中的數(shù)據(jù)持久化到磁盤。
AppendMessageResult 的屬性:
AppendMessageStatus status :消息追加結果,取值PUT_OK : 追加成功; END_OF_FILE: 超過文件大小; MESSAGE_SIZE_EXCEEDED :消息長度超過最大允許長度:PROPERTIES_SIZE_EXCEEDED :消息、屬性超過最大允許長度; UNKNOWN_ERROR :未知異常。
long wroteOffset :消息的物理偏移量。
String msgld :消息ID 。
long storeTimestamp :消息存儲時間戳。
long logicsOffset :消息消費隊列邏輯偏移量,類似于數(shù)組下標。
long pagecacheRT = 0 :當前未使用。
int msgNum = 1 :消息條數(shù),批量消息發(fā)送時消息條數(shù)。