7. 源码分析之堆外内存

源码分析之堆外内存

​ 我们根据之前了解可以,一般情况下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,还有一步是刷到磁盘文件中),带来的好处就是,避免了内存操作的很多容易堵的地方,降低了时延,比如说缺页中断降低,内存加锁,污染页的回写。

所以使用堆外缓冲区的方式相对来说会比较好,但是肯定的是,需要消耗一定的内存如果服务器内存吃紧就不推荐这种模式,同时的话,堆外缓冲区的话也需要配合异步刷盘才能使用(要配置正确)。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

岁月玲珑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值