一 认识mappedByteBuffer
mappedByteBuffer在rocketmq中的初始化
// 初始化
private void init(final String fileName, final int fileSize) throws IOException {
try {
this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
} finally {
if (!ok && this.fileChannel != null) {
this.fileChannel.close();
}
}
}
初始化过程通过fileChannel通道在堆外分配
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;
// ...
result = cb.doAppend(this.getFileFromOffset(), byteBuffer, this.fileSize - currentPos, (MessageExtBrokerInner) messageExt);
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);
}
二 池化管理 TransientStorePool 短暂的存储池
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);
}
}
rocketmq单独创建一个MappedByteBuffer内存缓存池用来临时存储数据。数据先写入该内存映射中然后由commit线程定时将数据从该内存复制到与目的物理文件对应的内存映射中。主要的作用是提供一个内存锁定将当前堆外内存一直锁定在内存中避免因为内存不足下将内存交换到磁盘。
一二两种内存buffer的解释图如下,需要注意的是在MappedFile初始化过程中(即mmap)并未实际的提供对应的物理内存,必须通过缺页中断读取数据建立管理也就是可以理解为后面的预热过程的目的。
三 mmap
mmap是一种将文件映射到虚拟内存的技术,可以将文件在磁盘位置的地址和在虚拟内存中的虚拟地址通过映射对应起来,之后就可以在内存这块区域进行读写数据,而不必调用系统级别的read,wirte这些函数,从而提升IO操作性能,另外一点就是mmap后的虚拟内存大小必须是内存页大小(通常是4K)的倍数,之所以这么做是为了匹配内存操作。
AllocateMappedFileService
private boolean mmapOperation() {
try {
// 拿取MappedFile的时候开始分配
req = this.requestQueue.take();
if (req.getMappedFile() == null) {
long beginTime = System.currentTimeMillis();
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
if (mappedFile.getFileSize() >= this.messageStore.getMessageStoreConfig()
.getMapedFileSizeCommitLog()
&&
this.messageStore.getMessageStoreConfig().isWarmMapedFileEnable()) {
// 预热,这里预热的主要目的是在mmap之后无物理内存建立map关联关系。减少缺页
mappedFile.warmMappedFile(this.messageStore.getMessageStoreConfig().getFlushDiskType(),
this.messageStore.getMessageStoreConfig().getFlushLeastPagesWhenWarmMapedFile());
}
req.setMappedFile(mappedFile);
this.hasException = false;
isSuccess = true;
}
} finally {
if (req != null && isSuccess)
req.getCountDownLatch().countDown();
}
return true;
}
在正常的系统调用过程中,内核调用磁盘数据建立Buffer再传输至用户空间user buffer最后由内核的socket buffer发送出去。这一过程经历了三次buffer,而使用mmap创建位于用户空间指针DirectByteBuffer可以直接进行传输从而跳过了内核。