源码分析之堆外内存
我们根据之前了解可以,一般情况下RocketMQ是通过MMAP内存映射,生产时消息写入内存映射文件,然后消费的时候再读。但是RocketMQ还提供了一种机制。我们来看下。
TransientStorePool,短暂的存储池(堆外内存)。RocketMQ单独创建一个ByteBuffer内存缓存池,用来临时存储数据,数据先写入该内存映射中,然后由commit线程定时将数据从该内存复制到与目标物理文件对应的内存映射中。
RocketMQ引入该机制主要的原因是提供一种内存锁定,将当前堆外内存一直锁定在内存中,避免被进程将内存交换到磁****盘。同时因为是堆外内存,这么设计可以避免频繁的GC
开启条件及限制
开启位置 broker中的配置文件
transientStorePoolEnable=true
在DefaultMessageStore.DefaultMessageStore()构造方法中,也可以看到还有其他限制
// org.apache.rocketmq.store.DefaultMessageStore#DefaultMessageStore
// todo 开启对外内存缓冲区
if (messageStoreConfig.isTransientStorePoolEnable()) {
this.transientStorePool.init();
}
// org.apache.rocketmq.store.TransientStorePool#init
/**
* Enable transient commitLog store pool only if transientStorePoolEnable is true and the FlushDiskType is ASYNC_FLUSH
*
* @return <tt>true</tt> or <tt>false</tt>
*/
public boolean isTransientStorePoolEnable() {
return transientStorePoolEnable && FlushDiskType.ASYNC_FLUSH == getFlushDiskType()
&& BrokerRole.SLAVE != getBrokerRole(); // 参数 && 异步刷盘 && 不是slave节点
}
/**
* 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);
}
}
TransientStorePool概要设计
public class TransientStorePool {
private static final InternalLogger log = InternalLoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
private final int poolSize; // 缓冲池的大小
private final int fileSize; // 每个ByteBuffer的大小
private final Deque<ByteBuffer> availableBuffers; // ByteBuffer容器, 双端队列
private final MessageStoreConfig storeConfig;
public void returnBuffer(ByteBuffer byteBuffer) {
byteBuffer.position(0);
byteBuffer.limit(fileSize);
this.availableBuffers.offerFirst(byteBuffer);
}
public ByteBuffer borrowBuffer() {
ByteBuffer buffer = availableBuffers.pollFirst();
if (availableBuffers.size() < poolSize * 0.4) {
log.warn("TransientStorePool only remain {} sheets.", availableBuffers.size());
}
return buffer;
}
}
这个地方的设计有点类似于连接池的设计,首先,构造方法中init方法用于构造堆外内存缓冲值**,默认构造5个**。
borrowBufferf()借用堆外内存池ByteBuffer
在创建MappedFile时就会进行设置。要注意,这里就会把堆外内存通过returnBuffer()赋给writeBuffer
// org.apache.rocketmq.store.MappedFile#init()
public void init(final String fileName, final int fileSize,
final TransientStorePool transientStorePool) throws IOException {
init(fileName, fileSize);
this.writeBuffer = transientStorePool.borrowBuffer();
this.transientStorePool = transientStorePool;
}
与消息发送流程串联
有了上面的知识,我们就可以确定,在MappedFile中,如果writeBuffer不为null,要么就一定开启了堆外内存缓冲!!!
再结合消息的发送流程。
数据到了存储存,最终会调用MappedFile的appendMessagesInner()进行消息的存储。
// org.apache.rocketmq.store.MappedFile#appendMessagesInner
public AppendMessageResult appendMessagesInner(final MessageExt messageExt, final AppendMessageCallback cb) {
assert messageExt != null;
assert cb != null;
//当前这个MaapedFile的写入位置
int currentPos = this.wrotePosition.get();
if (currentPos < this.fileSize) {
//异步输盘时还有两种刷盘模式可以选择
// TODO 如果writeBuffer不为null,要么就一定开启了堆外内存缓冲,否则使用MappedByteBuffer,也继承自ByteBuffer
ByteBuffer byteBuffer = writeBuffer != null ? writeBuffer.slice() : this.mappedByteBuffer.slice();
byteBuffer.position(currentPos);
AppendMessageResult result;
if (messageExt instanceof MessageExtBrokerInner) {
//todo 非批量处理
}
}
}
按照上图的流程,消息发送就有两条线
1、走传统的MMAP内存映射,数据写mappedByteBuffer,然后通过 flush刷盘
2、走堆外内存缓冲区,数据先写writeBuffer,再通过commit提交到FileChannel中,最后再flush刷盘
以上两种方式,处理的都是基于byteBuffer的实现,所以都通过 put方法可以写入内存。
所以对应前面讲的刷盘。
你会发现为什么异步刷盘线程有两个。一个是针对的MMAP刷盘,一个是针对的堆外内存缓冲的提交刷盘。
所以堆外内存缓冲区一定是要异步。 Commit的是针对堆外内存缓冲的提交,Flush的是针对MMAP的内存映射的处理。
在CommitRealTimeService中最后调用到MappedFile的 commit0方法写入:具体的如下:
// org.apache.rocketmq.store.CommitLog.CommitRealTimeService#run
@Override
public void run() { // 异步刷盘
CommitLog.log.info(this.getServiceName() + " service started");
while (!this.isStopped()) {
// 时间间隔默认200ms
int interval = CommitLog.this.defaultMessageStore.getMessageStoreConfig().getCommitIntervalCommitLog();
// 一次提交的最少页数
int commitDataLeastPages = CommitLog.this.defaultMessageStore.
getMessageStoreConfig().getCommitCommitLogLeastPages();
// 两次正式提交的最大间隔,默认200ms
int commitDataThoroughInterval = CommitLog.this.defaultMessageStore.
getMessageStoreConfig().getCommitCommitLogThoroughInterval();
// 上一次提交超过commitDataThoroughInterval, 则忽略提交commitDataThoroughInterval参数, 直接提交
long begin = System.currentTimeMillis();
if (begin >= (this.lastCommitTimestamp + commitDataThoroughInterval)) {
this.lastCommitTimestamp = begin;
commitDataLeastPages = 0;
}
// 执行提交操作, 将待提交数据提交到物理文件的内存映射区
try {
boolean result = CommitLog.this.mappedFileQueue.commit(commitDataLeastPages);
}
}
}
// org.apache.rocketmq.store.MappedFileQueue#commit
public boolean commit(final int commitLeastPages) {
boolean result = true;
MappedFile mappedFile = this.findMappedFileByOffset(this.committedWhere, this.committedWhere == 0);
if (mappedFile != null) {
int offset = mappedFile.commit(commitLeastPages); // here
long where = mappedFile.getFileFromOffset() + offset;
result = where == this.committedWhere;
this.committedWhere = where;
}
return result;
}
// org.apache.rocketmq.store.MappedFile#commit
public int commit(final int commitLeastPages) {
if (writeBuffer == null) { // 没有开启对外缓存就直接返回地址
//no need to commit data to file channel, so just regard wrotePosition as committedPosition.
return this.wrotePosition.get();
}
if (this.isAbleToCommit(commitLeastPages)) {
if (this.hold()) {
commit0(commitLeastPages); // here
this.release();
} else {
log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
}
}
// All dirty data has been committed to FileChannel.
if (writeBuffer != null && this.transientStorePool != null && this.fileSize == this.committedPosition.get()) {
this.transientStorePool.returnBuffer(writeBuffer);
this.writeBuffer = null;
}
return this.committedPosition.get();
}
protected void commit0(final int commitLeastPages) {
int writePos = this.wrotePosition.get();
int lastCommittedPosition = this.committedPosition.get();
if (writePos - lastCommittedPosition > commitLeastPages) {
try {
ByteBuffer byteBuffer = writeBuffer.slice();
byteBuffer.position(lastCommittedPosition);
byteBuffer.limit(writePos);
this.fileChannel.position(lastCommittedPosition);
this.fileChannel.write(byteBuffer); // here
this.committedPosition.set(writePos);
} catch (Throwable e) {
log.error("Error occurred when commit data to FileChannel.", e);
}
}
}
两种方式的对比
(1)默认方式,Mmap+PageCache的方式,读写消息都走的是pageCache,这样子读写都在pagecache里面不可避免会有锁的问题,在并发的读写操作情况下**,会出现缺页中断降低,内存加锁,污染页的回写**(脏页面)。
(2)堆外缓冲区,DirectByteBuffer(堆外内存)+PageCache的两层架构方式,这样子可以实现读写消息分离,写入消息时候写到的是DirectByteBuffer——堆外内存中,读消息走的是PageCache(对于,DirectByteBuffer是两步刷盘,一步是刷到PageCache,还有一步是刷到磁盘文件中),带来的好处就是,避免了内存操作的很多容易堵的地方,降低了时延,比如说缺页中断降低,内存加锁,污染页的回写。
所以使用堆外缓冲区的方式相对来说会比较好,但是肯定的是,需要消耗一定的内存,如果服务器内存吃紧就不推荐这种模式,同时的话,堆外缓冲区的话也需要配合异步刷盘才能使用(要配置正确)。