1. CommitLog文件
commitlog的文件组织如下所示,每个文件固定大小,默认1G,一个文件写满,创建另一个文件。文件名为文件中第一条消息的全局偏移量。
commitlog使用MappedFile和MappedFileQueue来组织消息文件。MappedFileQueue对应一个文件夹,MappedFile对应一个commitlog文件
接着上一节,进入CommitLog.putMessage方法
public PutMessageResult putMessage(final MessageExtBrokerInner msg) {
。。。
//获取最后一个mappedfile
MappedFile mappedFile = this.mappedFileQueue.getLastMappedFile();
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);
//如果不存在,则创建一个mappedfile
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);
}
//调用mappedfile的appendMessage方法存储消息
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文件
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);
}
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();
}
。。。
PutMessageResult putMessageResult = new PutMessageResult(PutMessageStatus.PUT_OK, result);
。。。
handleDiskFlush(result, putMessageResult, msg);
handleHA(result, putMessageResult, msg);
return putMessageResult;
}
首先调用mappedFileQueue.getLastMappedFile()方法获取最后一个mappedfile对象,MappedFileQueue对象结构如下
storePath:存储路径
mappedFileSize:每个commitlog文件大小,默认1G
allocateMappedFileService:mappedFile创建服务,默认没有用到。
flushedWhere:flush偏移位置
committedWhere:commit偏移位置
mappedFiles:维护的mappedfile列表
再看下mappedFileQueue.getLastMappedFile()方法
public MappedFile getLastMappedFile() {
MappedFile mappedFileLast = null;
while (!this.mappedFiles.isEmpty()) {
try {
mappedFileLast = this.mappedFiles.get(this.mappedFiles.size() - 1);
break;
} catch (IndexOutOfBoundsException e) {
//continue;
} catch (Exception e) {
log.error("getLastMappedFile has exception.", e);
break;
}
}
return mappedFileLast;
}
该方法就是从mappedFiles列表中取出最后一个返回,如果列表为空则返回null,再由外部去创建新的文件。
MappedFile类结构如下
OS_PAGE_SIZE:页大小,文件commit和flush都是以页大小为单位的,大小为4K
fileSize:文件大小,默认1G
fileChannel:文件对应的通道
writeBuffer:文件写入buffer,transientStorePool不为null时有用
fileName:文件名
fileFromOffset:文件第一个消息的偏移
file:打开的文件
mappedByteBuffer:文件的内存映射
wrotePosition、committedPosition、flushedPosition:写偏移,commit偏移,flush偏移
先解释下writeBuffer和transientStorePool,mappedfile有两种初始化,一种是直接创建内存映射,一种在外面再包一层堆外内存即transientStorePool,目的是使用了一种内存锁定技术,避免内存被交换。但是默认好像是关闭的。
再解释下commit和flush,commit是将内存提交到filechannel中(此时未落盘处于oscache中),flush是将内容刷新到磁盘上。
接着看CommitLog.putMessage()方法,调用mappedFile.appendMessage()方法,即调用mappedFile去具体存储消息。
public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
assert messageExt != null;
assert cb != null;
int currentPos = this.wrotePosition.get();
if (currentPos < this.fileSize) {
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);
}
该方法首先获取当前写偏移currentPos,然后调用ByteBuffer的slice方法创建一个共享内存对象byteBuffer ,更新byteBuffer 的写位置,调用CommitLog.doAppend将消息写入byteBuffer ,最后更新写偏移wrotePosition
2. Consumequeue文件
由上面commitlog的存储消息可以看到,所有topic的消息都是写到同一个文件中的,这样不同的消费者根据topic检索消息效率肯定非常低下,所以引入了consumequeue文件
consumequeue文件夹下一级目录是各个topic
topic下是各个queue文件夹
各个queue下存储的当前queue下的文件,组织方式和comitlog类似。
consumerqueue文件中并没有存储全量的消息内容,存储的只是消息再commitlog中的偏移,每个item存储格式如下:
comitlog offset: 8字节
size:4字节
tag hashcode: 8字节
consumequeue可以看做一个item数组,如果给定一个startIndex位置,获取item,则offset=startIndex*20 为该消息在consumequeue中的物理偏移量,如果offset小于minLogicOffset则表示该消息已经被删除了。否则获取从offset开始后的20个字节即为要获取的消息item,在根据item中的comitlog offset即可在commitlog中获取消息。
consumequeue是由异步线程从commitlog中拉取的。
具体服务为DefaultMessageStore中的reputMessageService服务,最终调用CommitLogDispatcherBuildConsumeQueue的dispatch方法
3. Index文件
rocketmq还维护一个index索引文件,格式如下
可以根据消息的unikey(topic+producer发送消息是创建的UniqID)快速定位消息,unikey格式为:
其中,
IndexHeader头部,包含40个字节,记录文件的统计信息,包括最小存储时间,最大存储时间,最小物理偏移,最大物理偏移,hashslot个数,index条目列表已经使用的个数
hash槽,一个index文件默认包含500万个hash槽,每个hash槽存储的是index的索引
index条目,默认一个index文件包含2000万个条目,每个index条目结构为
hashcode:key的hashcode
phyoffset:消息对应的物理偏移量
timedif:消息时间与第一条消息的时间戳差值,小于0表示该消息无效
preindexNo:当hash冲突时 ,构建的链表结构。
index文件和consumequeue文件一样也是由异步线程同步的,具体位置位于CommitLogDispatcherBuildIndex的dispatch方法。
4.过期文件删除
rocketmq文件采用内存映射机制,如果文件不删除,将一直占用大量内存,所以引入一种过期文件删除机制。
当前激活文件(正在写入的文件)之外的文件,如果一段时间没有更新(默认72h),rocketmq并不关心消息文件是否被消费。
过期文件删除位于
private void cleanFilesPeriodically() {
this.cleanCommitLogService.run();
this.cleanConsumeQueueService.run();
}
commitlog和consumequeue文件分别由两个服务去定期检测。
过期的文件并不是立即删除,删除条件包括:
1.达到设置的时间点,默认是凌晨4点
2.磁盘不足,默认是75%