• 消息队列(五)--- RocketMQ-消息存储2


    概述

    RocketMQ存储中主要用到以下知识点:

    • mmap 文件映射
    • 内存池
    • 异步刷盘
    • consumeQueue 同时本节将介绍各个重要的类,本篇文章将介绍 mmap 文件映射的相关方法和内存池相关知识点,刷盘和 consumeQueue 相关知识点在下篇介绍。

    MappedFile

    mappedFile 对应着底层映射文件,主要的功能是

    • bytebuffer写入映射文件
    • 回刷回文件

    重要字段

        public static final int OS_PAGE_SIZE = 1024 * 4;
        protected static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
    
        private static final AtomicLong TOTAL_MAPPED_VIRTUAL_MEMORY = new AtomicLong(0);
    
        private static final AtomicInteger TOTAL_MAPPED_FILES = new AtomicInteger(0);
        //加了 final,值不能修改
        protected final AtomicInteger wrotePosition = new AtomicInteger(0);
        //ADD BY ChenYang
        //  先commit 后 flush 
        protected final AtomicInteger committedPosition = new AtomicInteger(0);
        private final AtomicInteger flushedPosition = new AtomicInteger(0);
        protected int fileSize;
    
        protected FileChannel fileChannel;
        /**
         * Message will put to here first, and then reput to FileChannel if writeBuffer is not null.
         * 消息先放到这里先,如果此时 writeBuffer 不为 null (此时有东西在写入)那么再次放入 fileChannel
         */
        protected ByteBuffer writeBuffer = null;
        //
        protected TransientStorePool transientStorePool = null;
        private String fileName;
        private long fileFromOffset;
        private File file;
        //虚拟内存映射 buffer
        private MappedByteBuffer mappedByteBuffer;
        private volatile long storeTimestamp = 0;
        private boolean firstCreateInQueue = false;
    
    

    fileChannel 映射持久化的文件进来,使用原子类纪录 commit 和 flush 的节点。

    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;
        }
    
        /**
         * 初始化最主要就是文件映射
         */
        private void init(final String fileName, final int fileSize) throws IOException {
            this.fileName = fileName;
            this.fileSize = fileSize;
            this.file = new File(fileName);
            this.fileFromOffset = Long.parseLong(this.file.getName());
            boolean ok = false;
    
            ensureDirOK(this.file.getParent());
    
            try {
                // RandomAccessFile
                // 参见 : https://blog.csdn.net/qq496013218/article/details/69397380
                this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
                // fileChannel.map 返回的是堆外内存(java.nio.directByteBuffer)
                this.mappedByteBuffer = this.fileChannel.map(MapMode.READ_WRITE, 0, fileSize);
                TOTAL_MAPPED_VIRTUAL_MEMORY.addAndGet(fileSize);
                TOTAL_MAPPED_FILES.incrementAndGet();
                ok = true;
            } catch (FileNotFoundException e) {
                log.error("create file channel " + this.fileName + " Failed. ", e);
                throw e;
            } catch (IOException e) {
                log.error("map file " + this.fileName + " Failed. ", e);
                throw e;
            } finally {
                if (!ok && this.fileChannel != null) {
                    this.fileChannel.close();
                }
            }
        }
    
    

    commit 操作。

        /**
         * 1. 判断是否达到commit 的要求
         * 2. 获得锁
         * 3. commit
         * 4. 释放锁
         */
        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);
                    this.release();
                } else {
                    log.warn("in commit, hold failed, commit offset = " + this.committedPosition.get());
                }
            }
            //TODO  下面是什么操作
            // 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();
        }
    
        /**
         * 使用 JAVA NIO 的 bytebuffer 创建子 buffer
         * 然后写入到 filechannel 中去
         * @param commitLeastPages
         */
        protected void commit0(final int commitLeastPages) {
            int writePos = this.wrotePosition.get();
            int lastCommittedPosition = this.committedPosition.get();
    
            if (writePos - this.committedPosition.get() > 0) {
                try {
                    ByteBuffer byteBuffer = writeBuffer.slice();
                    byteBuffer.position(lastCommittedPosition);
                    byteBuffer.limit(writePos);
                    this.fileChannel.position(lastCommittedPosition);
                    this.fileChannel.write(byteBuffer);
                    this.committedPosition.set(writePos);
                } catch (Throwable e) {
                    log.error("Error occurred when commit data to FileChannel.", e);
                }
            }
        }
    
    
    
        /**
         *  判断是否满了或是达到了最小的 commit 页数
         * @param commitLeastPages 最小commit 页数
         * @return 是否可以 commit
         */
        protected boolean isAbleToCommit(final int commitLeastPages) {
            int flush = this.committedPosition.get();
            int write = this.wrotePosition.get();
    
            if (this.isFull()) {
                return true;
            }
    
            if (commitLeastPages > 0) {
                return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= commitLeastPages;
            }
    
            return write > flush;
        }
    
    
    FileChannel,MappedByteBuffer:
    这两个类代表的是Mmap 这样的内存映射技术,Mmap 能够将文件直接映射到用户态的内存地址,使得对文件的操作不再是 write/read,而转化为直接对内存地址的操作。
    
    Mmap技术本身也有局限性,也就是操作的文件大小不能太大,因此RocketMQ 中限制了单文件大小来避免这个问题。也就是那个filesize定为1G的原因。
    

    flush 回刷到文件中去

        /**
         * @return The current flushed position
         */
        public int flush(final int flushLeastPages) {
            if (this.isAbleToFlush(flushLeastPages)) {
                if (this.hold()) {
                    int value = getReadPosition();
    
                    try {
                        //我们只增加数据到 fileChannel 或是 mappedByteBuffer ,从不同时两者一起增加
                        //We only append data to fileChannel or mappedByteBuffer, never both.
                        if (writeBuffer != null || this.fileChannel.position() != 0) {
                            this.fileChannel.force(false);
                        } else {
                            this.mappedByteBuffer.force();
                        }
                    } catch (Throwable e) {
                        log.error("Error occurred when force data to disk.", e);
                    }
    
                    this.flushedPosition.set(value);
                    this.release();
                } else {
                    log.warn("in flush, hold failed, flush offset = " + this.flushedPosition.get());
                    this.flushedPosition.set(getReadPosition());
                }
            }
            return this.getFlushedPosition();
        }
    
    
        private boolean isAbleToFlush(final int flushLeastPages) {
            int flush = this.flushedPosition.get();
            int write = getReadPosition();
    
            if (this.isFull()) {
                return true;
            }
    
            if (flushLeastPages > 0) {
                return ((write / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE)) >= flushLeastPages;
            }
    
            return write > flush;
        }
    
    

    其中 writebuffer 和 filechannel 什么情况会刷回磁盘呢?以下这种图回答了这个问题。 1297993-20191026154615246-169928348.png

    同时mappedFile还有预热处理,具体见 warmMappedFile 方法 。

    
    

    TransientStorePool

    该类的主要作用是创建内存池,而且是堆外内存,主要作用是消除了申请内存空间,回收的时间,提高了使用的性能。 字段

        private final int poolSize;
        private final int fileSize;
        private final Deque<ByteBuffer> availableBuffers;
        private final MessageStoreConfig storeConfig;
    
    
    public class TransientStorePool {
        private static final Logger log = LoggerFactory.getLogger(LoggerName.STORE_LOGGER_NAME);
    
        private final int poolSize;//池的大小有多少,默认5
        private final int fileSize;//每个commitLog文件大小,默认1G
        private final Deque<ByteBuffer> availableBuffers;//双端队列记录可用的buffers
        private final MessageStoreConfig storeConfig;//存储配置
    
        public TransientStorePool(final MessageStoreConfig storeConfig) {
            this.storeConfig = storeConfig;
            this.poolSize = storeConfig.getTransientStorePoolSize();
            this.fileSize = storeConfig.getMapedFileSizeCommitLog();
            this.availableBuffers = new ConcurrentLinkedDeque<>();
        }
    
        /**
         * It's a heavy init method.
         * 初始化函数,分配poolSize个fileSize的堆外空间
         */
        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);
            }
        }
    
        //销毁availableBuffers中所有buffer数据
        public void destroy() {
            for (ByteBuffer byteBuffer : availableBuffers) {
                final long address = ((DirectBuffer) byteBuffer).address();
                Pointer pointer = new Pointer(address);
                LibC.INSTANCE.munlock(pointer, new NativeLong(fileSize));
            }
        }
    
        //用完了之后,返还一个buffer,对buffer数据进行清理
        public void returnBuffer(ByteBuffer byteBuffer) {
            byteBuffer.position(0);
            byteBuffer.limit(fileSize);
            this.availableBuffers.offerFirst(byteBuffer);
        }
    
        //借一个buffer出去
        public ByteBuffer borrowBuffer() {
            ByteBuffer buffer = availableBuffers.pollFirst();
            if (availableBuffers.size() < poolSize * 0.4) {
                log.warn("TransientStorePool only remain {} sheets.", availableBuffers.size());
            }
            return buffer;
        }
    
        //剩余可用的buffers数量
        public int remainBufferNumbs() {
            if (storeConfig.isTransientStorePoolEnable()) {
                return availableBuffers.size();
            }
            return Integer.MAX_VALUE;
        }
    }
    
    

    可以看到内存池在初始化的过程中,将内存用“lock”锁,防止CPU将进程在主存中的这一部分内存给交换回硬盘。

    补充

    内存池

    在netty的过程在使用过程中,也会使用内存池,内存池的优势是集中管理内存的分配和释放,同时提高分配和释放内存的性能,很多框架会先预先申请一大块内存,然后通过提供响应的分配 和释放接口来使用内存,这样系统的性能也会打打提高。

    随机读写,顺序读写

    随机和顺序读写,是存储器的两种输入输出方式。存储的数据在磁盘中占据空间,对于一个新磁盘,操作系统会将数据文件依次写入磁盘,当有些数据被删除时,就会空出该数据原来占有的存储空间,时间长了,不断的写入、删除数据,就会产生很多零零散散的存储空间,就会造成一个较大的数据文件放在许多不连续的存贮空间上,读写些这部分数据时,就是随机读写,磁头要不断的调整磁道的位置,以在不同位置上的读写数据,相对于连续空间上的顺序读写,要耗时很多。 在开机时、启动大型程序时,电脑要读取大量小文件,而这些文件也不是连续存放的,也属于随机读取的范围。 随机读写:每一段数据有地址码,可以任意跳到某个地址读取该段数据 顺序读写:数据以一定长度连续存储,中间没有地址码,只能顺序读取

    改善方法:做磁盘碎片整理,合并碎片文件,但随后还会再产生碎片造成磁盘读写性能下降,而且也解决不了小文件的随机存取的问题,这只是治标。更好的解决办法:更换电子硬盘(SSD),电子盘由于免除了机械硬盘的磁头运动,对于随机数据的读写极大的提高。 举个例子1: SSD的随机读取延迟只有零点几毫秒,而7200RPM的随机读取延迟有7毫秒左右,5400RPM硬盘更是高达9毫秒之多,体现在性能上就是开关机速度。

    举个例子2:假设有1到1000笔的数据。 情况1:现在要读出第1000笔,顺序读写的方式是从第1笔开始读,一直找到第1000笔;随机读写是通过运算,很快的找到第1000笔。 情况2:要找出含“abc”的数据,顺序读写还是从第1笔开始读,一直找到第1000笔;随机读写是通过运算,很快的找到“abc”的数据。

    总结

    本节介绍了rocketmq中的存储细节,包括 mmap 相关,内存池相关知识点。

    参考资料

    • http://silence.work/2019/05/03/RocketMQ-Broker
    • https://www.jianshu.com/p/771cce379994
    • https://blog.csdn.net/qq_33611327/article/details/81738195 (推荐一看)
  • 相关阅读:
    list转datatable c#
    按钮靠右css小结
    IE浏览器打印合格证相关问题
    vue项目插入视频-mp4
    vue项目bug-Couldn’t find preset "es2015"
    Mac打开swf文件
    mac+windows下从git上拉取项目及运行
    echarts.js制作中国地图
    前端数据可视化echarts.js
    vue-router 基本使用
  • 原文地址:https://www.cnblogs.com/Benjious/p/11785942.html
Copyright © 2020-2023  润新知