Rocket MQ : 拒绝神化零拷贝

news2025/1/31 2:49:12

注:

  • 本文绝非对零拷贝机制的否定
  • 笔者能力有限,理解偏差请大家多多指正

      不可否认零拷贝对于Rocket MQ的高性能表现有着积极正面的作用,但是笔者认为只是锦上添花,并非决定性因素。Rocket MQ性能卓越的原因绝非零拷贝就可以一言以蔽之。

      笔者企图从源码以及Linux内核背后探讨一下其他可能的原因。

预热机制

      Rocket MQ采用内存映射来提高文件I/O访问性能,MappedFile、MappedFileQueue管理存储文件。MappedFileQueue对存储文件进行封装可以理解为MappedFile的管理容器。譬如CommitLog文件存储位置:${ROCKE_HOME}/store/commitlog/该目录下存在多个MappedFile文件。

      MappedFile是内存映射的具体实现:构造方法包含文件名称、文件大小、以及一个transientStorePool标识位,如果开启transientStorePoolEnable机制则表示内容先存储在堆外内存,而后通过Commit线程将数据提交到FileChannel,而后Flush线程负责持久化。

public MappedFile(String fileName, int fileSize) throws IOException {
    init(fileName, fileSize);
}

public MappedFile(String fileName, int fileSize,
    TransientStorePool transientStorePool) throws IOException {
    init(fileName, fileSize, transientStorePool);
}
复制代码

      两个构造方法不约而同的都会走init(),我们看两参数的那个即可。

private void init(String fileName, 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 {
        this.fileChannel = new RandomAccessFile(this.file, "rw").getChannel();
        /**
         * fileChannel.map(MapMode mode, long position, long size)
         * 将此 fileChannel 对应的一个区域直接映射到内存中
         */
        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("Failed to create file " + this.fileName, e);
        throw e;
    } catch (IOException e) {
        log.error("Failed to map file " + this.fileName, e);
        throw e;
    } finally {
        if (!ok && this.fileChannel != null) {
            this.fileChannel.close();
        }
    }
}
复制代码

      Java中在尝试文件映射的时候提供三种模式:

  • MapMode.READ_ONLY: 任何修改缓冲区的尝试将导致抛出ReadOnlyBufferException
  • MapMode.READ_WRITE:对结果缓冲区所做的更改将最终被传播到文件中
  • MapMode.PRIVATE: 对结果缓冲区所做的更改不会传播到文件中,其他程序不可见

      值得注意的是映射一旦建立成功,就不再依赖fileChannel,即使此时关闭通道也不会影响映射的有效性,因此可以根据实际情况决定要不要close。

      如果了解Linux内核的话,请您一定要注意直到此时为该文件分配的映射空间都是虚拟内存,并没有真的关联物理内存,当程序需要而物理内存又没有分配的时候则会触发一个Page Fault交由内核处理:

      上图展示的只是一个大概过程,实际情况要复杂一些,因为缺页处理程序必须应对多种细分的特殊情况,超级复杂(参见《深入理解LINUX内核》378页),CommitLog文件大小固定为1G,如此大内存空间读写操作势必造成大量的缺页中断,显然这里绝对存在大量优化空间的。我们看看Rocket MQ作者如何优化。

       不妨跟随笔者视角一探CommitLog如何获取MappedFile文件。

public MappedFile getLastMappedFile(long startOffset) {
    return getLastMappedFile(startOffset, true);
}
复制代码

      getLastMappedFile方法会往AllocateMappedFileService#requestQueue阻塞队列提交AllocateRequest任务。AllocateMappedFileService服务线程此时会被唤醒执行mmapOperation方法。大致流程:

  • 阻塞队列requestQueue.take()出来一个任务对象,服务线程被唤醒,拿到AllocateRequest对象
  • 判断是否开启内存读写分离机制,决定选择如何构造MappedFile。
/* 是否开启内存读写分离 */
if (messageStore.getMessageStoreConfig().isTransientStorePoolEnable()) {
    try {
        /* Rocket允许自己定制实现细节 */
        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,warmMappedFile方法负责具体的预热行为。这里这么做的原因是直接将缺页中断提前至初始化阶段,后续就不会因为频繁中断导致性能下降
public void warmMappedFile(FlushDiskType type, int pages) {
    long beginTime = System.currentTimeMillis();
    ByteBuffer byteBuffer = this.mappedByteBuffer.slice();
    int flush = 0;
    long time = System.currentTimeMillis();
    for (int i = 0, j = 0; i < this.fileSize; i += MappedFile.OS_PAGE_SIZE, j++) {
        byteBuffer.put(i, (byte) 0);
        /* force flush when flush disk type is sync */
        if (type == FlushDiskType.SYNC_FLUSH) {
            /* 每写入 pages 个内存页时刷盘一次 */
            if ((i / OS_PAGE_SIZE) - (flush / OS_PAGE_SIZE) >= pages) {
                flush = i;
                mappedByteBuffer.force();
            }
        }

        /* prevent gc */
        if (j % 1000 == 0) {
            log.info("j={}, costTime={}", j, System.currentTimeMillis() - time);
            time = System.currentTimeMillis();
            try {
                Thread.sleep(0);
            } catch (InterruptedException e) {
                log.error("Interrupted", e);
            }
        }
    }

    /* force flush when prepare load finished */
    if (type == FlushDiskType.SYNC_FLUSH) {
        log.info("mapped file warm-up done, force to disk, mappedFile={}, costTime={}",
            this.getFileName(), System.currentTimeMillis() - beginTime
        );
        mappedByteBuffer.force();
    }
    log.info("mapped file warm-up done. mappedFile={}, costTime={}",
        this.getFileName(), System.currentTimeMillis() - beginTime
    );
    
    /* !!! 这一行超级重要 !!! */
    this.mlock();
}
复制代码

      这里需要一点点Linux内核管理内存的前置知识:不了解的朋友可以稍微了解一下swap的概念。内存可以说是计算机系统中最为宝贵的资源了,再怎么多也不够用,当系统运行时间长了之后,难免会遇到内存紧张的时候,这时候就需要内核将那些不经常使用的内存页面回收起来,或者将那些可以迁移的页面进行内存规整,从而可以腾出连续的物理内存页面供内核分配。

      简而言之就是当物理内存紧张的时候Linux内核会将别的进程的占用的物理内存swap到交换区(目前个人了解大部分都是磁盘)。

      如此一来同一物理机如果有更加需要内存资源的进程,Linux内核完全有可能将我们通过预热机制好不容易全部都分配好的内存全部交换出去,这样Rocket MQ的性能一定呈现断崖式的下跌。

      有没有一种机制使得进程可以独占一部分物理内存,不允许内核交换呢?神说要有光,于是Linux就暴露了mlock system call,而且Rocket MQ就是这么做的,上文提到的warmMappedFile方法的最后一行this.mlock就是用来lock memory的。

      查阅一下手册就知道Linux提供了mlock, mlock2, munlock, mlockall, munlockall用来locK和unlock内存。

#include <sys/mman.h>

int mlock(const void *addr, size_t len);
int mlock2(const void *addr, size_t len, int flags);
int munlock(const void *addr, size_t len);

int mlockall(int flags);
int munlockall(void);
复制代码

      总结一下就是Rocket MQ为了自身的高性能拒绝内存被操作系统交换

madvise

      为了防止剧透,刚刚一直没有带大家看看MappedFile#mlock其实该方法还有别的妙处。

public void mlock() {
    long beginTime = System.currentTimeMillis();
    long address = ((DirectBuffer) (this.mappedByteBuffer)).address();
    Pointer pointer = new Pointer(address);
    {
        int ret = LibC.INSTANCE.mlock(pointer, new NativeLong(this.fileSize));
        log.info("mlock {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime);
    }

    {
        /**
         * MADV_WILLNEED 表示应用程序希望很快访问此地址范围
         */
        int ret = LibC.INSTANCE.madvise(pointer, new NativeLong(this.fileSize), LibC.MADV_WILLNEED);
        log.info("madvise {} {} {} ret = {} time consuming = {}", address, this.fileName, this.fileSize, ret, System.currentTimeMillis() - beginTime);
    }
}
复制代码

      为了更加极致的性能体验,Linux操作系统暴露了madvise sysytem call ,madvise()系统调用,用于向内核提供对于起始地址为addr,长度为length内存空间的操作建议或者指示。在大多数情况下,此类建议的目标是提高系统或者应用程序的性能。

#include <sys/mman.h>

int madvise(void *addr, size_t length, int advice);
复制代码

      最初,此系统调用,仅仅支持一组常规的(conventional)建议值,这些建议值在各种系统中也有实现,(但是请注意,POSIX中并没有指定madvise()),后来又添加了许多特定于Linux的建议值。第三个参数advice其实就是一个标识,根据标识不同Linux内核采取的策略也有所区别。

  • Conventional advice values
    • MADV_NORMAL:不做任何特殊处理,这是默认操作
    • MADV_RANDOM:期望以随机的顺序访问page,这等价于告诉内核,随机性强,局部性弱,预读机制意义不大
    • MADV_SEQUENTIAL:与MADV_RANDOM相反,期望顺序的访问page,因此内核应该积极的预读给定范围内的page,并在访问过后快速释放
    • MADV_WILLNEED:预计不久将会被访问,因此提前预读几页是个不错的主意
    • MADV_DONTNEED:与MADV_WILLNEED相反,预计未来长时间不会被访问,可以认为应用程序完成了对这部分内容的访问,因此内核可以释放与之相关的资源
  • Linux-specific advice values:Rocket MQ用的就是常规值,然后Linux特定值又特别多,所以这里挑选几个讲一下
    • MADV_DONTFORK:在执行fork(2)后,子进程不允许使用此范围的页面。这样是为了避免COW机制导致父进程在写入页面时更改页面的物理位置
    • MADV_DOFORK:撤销MADV_DONTFORK的效果,恢复默认行为
    • MADV_NOHUGEPAGE:确保指定范围内的页面不会使用透明大页。

      Rocket MQ使用的是MADV_WILLNEED建议值,每次会预取提高性能。

文件系统设计

      针对Producer和Consumer分别采用了数据和索引部分相分离的存储结构,Producer发送消息至Broker端,然后Broker端使用同步或者异步的方式对消息刷盘持久化,保存至CommitLog中。只要消息被刷盘持久化至磁盘文件CommitLog中,那么Producer发送的消息就不会丢失。正因为如此,Consumer也就肯定有机会去消费这条消息。

      当无法拉取到消息后,可以等下一次消息拉取,同时服务端也支持长轮询模式,如果一个消息拉取请求未拉取到消息,Broker允许等待30s的时间,只要这段时间内有新消息到达,将直接返回给消费端。这里,RocketMQ的具体做法是,使用Broker端的后台服务线程—ReputMessageService不停地分发请求并异步构建ConsumeQueue(逻辑消费队列)和IndexFile(索引文件)数据。

      RocketMQ采用混合型存储结构(多个Topic的消息实体内容都存储于一个CommitLog中),虽然说因为两种索引文件ConsumeQueue和IndexFile导致Rocket MQ其实不是全局的顺序写,但是这两种文件其实足够小,况且索引文件自身也是顺序写,可以说Rocket MQ已经尽最大努力保证全局顺序写了。

硬件加持

      页缓存(PageCache)是OS对文件的缓存,用于加速对文件的读写。一般来说,程序对文件进行顺序读写的速度几乎接近于内存的读写速度,主要原因就是由于OS使用PageCache机制对读写访问操作进行了性能优化,将一部分的内存用作PageCache。对于数据的写入,OS会先写入至Cache内,随后通过异步的方式由pdflush内核线程将Cache内的数据刷盘至物理磁盘上。对于数据的读取,如果一次读取文件时出现未命中PageCache的情况,OS从物理磁盘上访问读取文件的同时,会顺序对其他相邻块的数据文件进行预读取。

      在RocketMQ中,ConsumeQueue逻辑消费队列存储的数据较少,并且是顺序读取,在page cache机制的预读取作用下,Consume Queue文件的读性能几乎接近读内存,即使在有消息堆积情况下也不会影响性能。而对于CommitLog消息存储的日志数据文件来说,读取消息内容时候会产生较多的随机访问读取,严重影响性能。如果选择合适的系统IO调度算法,比如设置调度算法为“Deadline”(此时块存储采用SSD的话),随机读的性能也会有所提升。

      另外,RocketMQ主要通过MappedByteBuffer对文件进行读写操作。其中,利用了NIO中的FileChannel模型将磁盘上的物理文件直接映射到用户态的内存地址中,将对文件的操作转化为直接对内存地址进行操作,从而极大地提高了文件的读写效率(正因为需要使用内存映射机制,故RocketMQ的文件存储都使用定长结构来存储,方便一次将整个文件映射至内存)。

      上面提到的相邻文件预读、Mmap内存映射其实本质原因都是因为可以向内存借力,没有更快更好的内存硬件一切都是空谈。其实软件工程师所能做的相对有限,我们只是在最大限度的发挥硬件的能力。

总结

      笔者认为Rocket MQ高性能的关键是:

  • 内存加持,充分发挥硬件能力
  • 文件预热,将中断响应提前到初始化阶段
  • mlock禁止Linux交换内存
  • madvise向操作系统提出内存空间的操作建议或者指示
  • 优秀的文件系统设计,尽最大可能保证顺序写
  • 将磁盘上的物理文件直接映射到用户态的内存地址中(这种Mmap的方式减少了传统IO将磁盘文件数据在操作系统内核地址空间的缓冲区和用户应用程序地址空间的缓冲区之间来回进行拷贝的性能开销)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/39838.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第146篇 笔记-智能合约介绍

定义&#xff1a;当满足某些预定义条件时&#xff0c;智能合约是一种在区块链网络上运行的防篡改程序。 1.什么是智能合约 智能合约是在区块链网络上托管和执行的计算机程序。每个智能合约都包含指定预定条件的代码&#xff0c;这些条件在满足时会触发并产生结果。通过在去中…

IDEA热部署插件JRebel and XRebel

IDEA热部署插件JRebel and XRebel嘚吧嘚下载安装激活配置使用嘚吧嘚 刚开始用过一段时间的eclipse&#xff0c;其他方面没感觉&#xff0c;但是eclipse的热部署真的是深得我心啊&#x1f60a;。 后来换了IDEA&#xff0c;瞬间就心动了&#xff0c;各个方面真的很好用&#xf…

U3D VideoPlayer播放视频和坑点

最近做的游戏里,需要先播放一段几秒钟的工作室LOGO片头,拿到的视频是AVI格式,以前没在U3D里用到过视频,本以为很简单,没想到都2022年了,U3D播放视频还这么烂。。。 插件最好用的是AVPro,除非你有大量的视频要播放,否则没必要用插件,一个是贵,另一个插件很大。 首先…

Python爬虫从入门到进阶

前言 董伟明&#xff0c;国内某知名Python应用网站高级产品开发工程师&#xff0c;《 Python Web 开发实战》作者&#xff0c;本书目前已经售出 17k 余本&#xff0c;另外也已经在台湾地区上市。在 2012 和 2014 年分别通过 2 个爬虫免试获得 2 个业界知名公司 offer&#xff…

MyBatis缓存机制之一级缓存

MyBatis缓存机制之一级缓存 前言 MyBatis内部封装了JDBC&#xff0c;简化了加载驱动、创建连接、创建statement等繁杂的过程&#xff0c;是我们常见的持久性框架。缓存是在计算机内存中保存的临时数据&#xff0c;读取时无需再从磁盘中读取&#xff0c;从而减少数据库的查询次…

Node.js 入门教程 1 Node.js 简介

Node.js 入门教程 Node.js官方入门教程 Node.js中文网 本文仅用于学习记录&#xff0c;不存在任何商业用途&#xff0c;如侵删 文章目录Node.js 入门教程1 Node.js 简介1.1 大量的库1.2 Node.js 应用程序的示例1.3 Node.js框架和工具1 Node.js 简介 Node.js 是一个开源和跨平台…

子矩形计数(冬季每日一题 17)

给定一个长度为 nnn 的数组 aaa 和一个长度为 mmm 的数组 bbb。 两个数组均只包含 000 和 111。 利用两个给定数组生成一个 nmnmnm 的矩阵 ccc&#xff0c;其中 cijaibjc_{ij}a_ib_jcij​ai​bj​。 显然&#xff0c;矩阵 ccc 中也只包含 000 和 111。 请问&#xff0c;矩阵…

期末复习 c

作者&#xff1a;小萌新 专栏&#xff1a;C语言复习 作者简介&#xff1a; 大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;回顾之前的分支循环以及一些题目博客 [TOC](这里写目录标题分支循环选择switch casegetchar putchar 以及EOF三个C语言练习题总结…

C++智能指针之unique_ptr

C智能指针之unique_ptr前言一、unique_ptr1.1 unique_ptr类的初始化1.2 unique_ptr禁止拷贝和赋值1.3 release、reset函数1.4 向unique_ptr传递删除器1.5 unique_ptr与动态数组的使用总结前言 在C中&#xff0c;动态内存的申请和释放是通过运算符&#xff1a;new 和 delete 进行…

【无线传感器】基于Matlab实现WSN 查找两个节点之间的最短路径并发送数据

✅作者简介&#xff1a;热爱科研的Matlab仿真开发者&#xff0c;修心和技术同步精进&#xff0c;matlab项目合作可私信。 &#x1f34e;个人主页&#xff1a;Matlab科研工作室 &#x1f34a;个人信条&#xff1a;格物致知。 更多Matlab仿真内容点击&#x1f447; 智能优化算法 …

Linux基础内容(13)—— 进程控制

目录 1.fork函数的进程创建 1.fork返回值 2.fork返回值 3.fork调用失败 2.写时拷贝 3.退出码的知识 4.进程退出 1.退出的情况 2.正常退出 5.进程等待 1.调用系统等待函数杀死僵尸进程 2.僵尸状态与PCB的关系 3.进程阻塞等待与非阻塞等待方式 6.进程程序替换 1.替…

【网络篇】第十八篇——IP协议相关技术

目录 DNS DNS背景 域名的层级关系 域名解析过程 使用dig工具分析DNS过程 ARP DHCP NAT NAT IP转换过程 NAPT NAT技术的缺陷 如何解决NAT潜在问题 ICMP ICMP功能 ICMP协议格式 ping命令 traceroute命令 IGMP 跟IP 协议相关的技术也不少&#xff0c;接下来说说与IP 协议相关的重…

Docker——Prometheus监控服务治理

摘要 Prometheus是继Kubernetes之后&#xff0c;第二个从云原生计算基金会&#xff08;CNCF&#xff09;毕业的项目。Prometheus是Google监控系统BorgMon类似实现的开源版&#xff0c;整套系统由监控服务、告警服务、时序数据库等几个部分&#xff0c;及周边生态的各种指标收集…

uniapp vuex正确的打开方式

uniapp vuex正确的打开方式一、vuex与全局变量globalData的区别二、uniapp vuex使用目录结构如下1. 根目录创建vuex目录&#xff0c;创建index.js文件2. 模块化代码3. 在 main.js 中导入store文件4. 调用一、vuex与全局变量globalData的区别 uni-app像小程序一样有globalData&…

项目开发——【流程图】软件工程程序流程图详解《如何正确绘制项目开发流程图》

程序流程图详解 介绍&#xff1a;通过图形符号形象的表示解决问题的步骤和程序。好的流程图&#xff0c;不仅能对我们的程序设计起到作用&#xff1b;在帮助理解时&#xff0c;往往能起到"一张图胜过千言万语"的效果。 一、程序流程图基本控制结构 顺序型&#xf…

如何实现RTS游戏中鼠标在屏幕边缘时移动视角

文章目录&#x1f9e8; Preface&#x1f38f; 判断鼠标是否处于屏幕边缘⚽ 获取鼠标处于屏幕边缘时的移动方向&#x1f3a8; 控制相机在x、z轴形成的平面上移动&#x1f3d3; 完整示例代码&#x1f9e8; Preface 本文简单介绍如何在Unity中实现即时战略游戏中鼠标在屏幕边缘的…

创新赋能合作伙伴,亚马逊云科技re:Invent科技盛宴

北京时间11月29号&#xff0c;亚马逊云科技年度峰会re:Invent 2022将在拉斯维加斯开幕。这场年度最重磅的云计算技术大会不仅是科技盛宴&#xff0c;也是亚马逊云科技与诸多客户交流互鉴的绝佳平台&#xff0c;今天带大家认识一下几位资深云计算用户&#xff0c;以及他们和re:I…

MyBatis ---- 搭建MyBatis

MyBatis ---- 搭建MyBatis1. 开发环境2. 创建maven工程a>打包方式&#xff1a;jarb>引入依赖3. 创建MyBatis的核心配置文件4. 创建mapper接口5. 创建MyBatis的映射文件6. 通过junit测试功能7. 加入log4j日志功能a>加入依赖b>加入log4j的配置文件1. 开发环境 IDE&a…

Linus 文件处理(四)

目录 一、前言 二、高级主题: fcntl和mmap 1、fcntl 2、mmap 3、Using mmap 一、前言 本文将简单介绍Linux文件和目录&#xff0c;以及如何操作它们&#xff08;如何创建文件、打开、读、写和关闭&#xff0c;程序如何操作目录&#xff0c;如创建、扫描和删除目录等&…

池风水利用工具

引用 这篇文章的目的是介绍一种基于内核态内存的越界写入通用利用技术和相关工具复现. 文章目录引用简介分析调试分析漏洞利用工具使用方法工具使用效果相关引用参与贡献简介 笔者的在原作者池风水利用工具(以下简称工具)基础上进行二次开发,新增了全自动获取内核调试模块符号…