从一到无穷大 #11 Is mmap shit or not?

news2024/12/22 22:03:11

在这里插入图片描述本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

本作品 (李兆龙 博文, 由 李兆龙 创作),由 李兆龙 确认,转载请注明版权。

文章目录

  • 前言
  • mmap原理
  • madvise / mlock / msync
  • 放弃使用mmap的理由
    • Influxdb
    • singlestore
    • RocksDB
    • VictoriaMetrics
    • scylladb
  • 现在还在使用mmap的理由
    • mongoDB
    • 冷数据沉降
    • RUMA(Rewired User-space Memory Access)
  • 总结

前言

最近正在研究InfluxDB1.8的series file实现,竟发现其中大量使用mmap,这和我的固有认知有非常大的差异,因为一年以前曾看过一篇出自Andrew Pavlo之手的吐槽文《Are You Sure You Want to Use MMAP in Your Database Management System?》,此后对mmap深恶痛绝,没想到现在竟然真的在线上运营的系统上看到了它。

So, Is mmap shit or not?

mmap原理

在这里插入图片描述

  1. 调用void *mmap(void addr[.length], size_t length, int prot, int flags, int fd, off_t offset);,获取一个指向内存映射文件的指针
  2. OS保留内存映射区域的虚拟内存(在VMA中创建一项),但是不载入文件
  3. 程序使用第一步返回的指针操作文件内容
  4. OS在VMA中检索对应的虚拟地址空间,发现不存在有效映射
  5. 触发缺页中断,从二级存储中读取这部分数据
  6. 在页表项中添加一个新的条目,并将虚拟地址映射到物理地址
  7. 在TLB中缓存该页表项,以加速后续访问

madvise / mlock / msync

还有部分系统调用可以配合mmap使用,达到加速访问(madvise,mlock)和安全(msync)的功能。

这里最全的解释还是参考[1],这里man文档的解释没有注释中清晰。

int madvise(void addr[.length], size_t length, int advice);
int mlock(const void addr[.len], size_t len);
int msync(void addr[.length], size_t length, int flags);

下面是一些madvise常用的flag,可以看到都是一些很通用化的策略:

  • MADV_NORMAL:When a page fault occurs in Linux with the default MADV_NORMAL hint, the OS will fetch the accessed page, as well as the next 16 and previous 15 pages. With 4 KB pages, MADV_NORMAL causes the OS to read 128 KB from secondary storage, even though the caller only requested a single page.
  • MADV_RANDOM:the system should read the minimum amount of data on any access, since it is unlikely that the application will need more than what it asks for.
  • MADV_SEQUENTIAL :pages in the given range will probably be accessed once, so they can be aggressively read ahead, and can be freed soon after they are accessed.
  • MADV_DONTNEED:the application is finished with the given range, so the kernel can free resources associated with it.
  • MADV_FREE:the application marks pages in the given range as lazy free, where actual purges are postponed until memory pressure happens.

放弃使用mmap的理由

Influxdb

在我们公司的实际线上运营经验来看,influxdb使用mmap是一个灾难性的事情。
influxdb使用mmap管理series file,包括hash->id id->offset的index,以及存储seriesKey的segment,series file是shard间共享的,意味着这一个series file其实可能承载上亿条时间线(mmap映射的文件大小没有上限,这对于IO数来讲是一个毁灭性打击),这在时间线爆炸是对性能有毁灭性打击:

  1. 每次查询时都需要在tsi中通过tagvalue反查series id,随后查询series file获取实际的series key;其次写入时需要查询series file检查是否存在某个series key,这个过程不命中内存的话会直接查询mmap映射的哈希表(线性探测再散列的两个哈希表),其次确定不存在时还会直接插入segment,触发mmap的IO操作;这里也可以看出这个文件其实是无限增长的,暂且不谈每次查询的IO操作,当大小超过TLB的限制时也可预见的会造成TLB shootdown
  2. influxdb iox支持S3接口,意味着需要允许容器中没有挂载磁盘时使用influxdb,mmap使得这个需求无法完成,所以必须使用自定义管理IO操作

singlestore

单纯的遇到了性能瓶颈,本质的原因是mmap_lock争用。

同进程的不同线程虽然对应不同的task_struct ,但因为共享虚拟地址空间都共享同一个mm_struct,每个mm_struct包含一个mmap_lock(信号量),进程内大部分的内存操作都需要先对它上锁,比如如下操作:

  1. 所有对 VMA 的操作 (mmap、munmap)
  2. 所有对页表的修改 (page fault 等)
  3. madvise (如 jemalloc 经常使用的 MADV_DONTNEED)

在[3]中作者使用16 个线程(每个 CPU 核心一个)执行 select count(*) from t where i > 5,按照现在的磁盘效率,这应该是一个经典的CPU密集型场景,磁盘只需要读取原始数据,剩下的全部是内存中的计算任务,但是结果是近50%的时间都处于空闲状态,导致性能较差,作者使用off-cpu分析程序阻塞的瓶颈,可以看到如下火焰图:
在这里插入图片描述
可以很明显的看到瓶颈是由于大量的缺页/munmap造成的对mmap_lock的争用,这就导致了没次调用都会产生10-20ms的誓言,当修改为read系统调用后这个问题就解决了,也很好理解,因为缺页对vma的修改是细粒度的,而read可以一次修改大量vma项,这就使得锁粒度变低了不少。

RocksDB

在[5]中可以看到rocksdb认为leveldb性能较差的几个问题:

  1. We found that LevelDB’s single-threaded compaction process was insufficient to drive server workloads.
  2. We saw frequent write-stalls with LevelDB that caused 99-percentile latency to be tremendously large.
  3. We found that mmap-ing a file into the OS cache introduced performance bottlenecks for reads. We could not make LevelDB consume all the IOs offered by the underlying Flash storage.

levelDB的随机读抽象接口RandomAccessFile允许使用mmappread来实现,在[6]中可以看到这个修改使得性能提升了不少,但是仅仅是在磁盘小于内存时,当磁盘远大于内存时mmap无法充分利用磁盘的IO能力。

VictoriaMetrics

mmap非常糟糕,在go中使用mmap更加糟糕[7]。

我曾经实现过用户态抢占式协程框架[9]和协作式协程框架[8],对这个问题可谓极其感同身受。考虑一个问题,在一个N:M的协程框架中何时执行协程的执行权切换?

  1. 协作式框架会在显示调用阻塞式系统调用时切换到其他协程执行
  2. 抢占式框架会在某个协程执行达到某个条件时主动切换其执行权(比如执行时间过长)

这两点都可以看出协程可以理解为一个逻辑单位,对于底层线程的执行时不关心的,换句话说协程眼中自己是始终在执行的,但显然这只是一种假设,协程最怕的就是线程突然阻塞,哪些情况可能导致这一点呢?

  1. 正常的线程切换
  2. 内核中慢操作(page fault,跨CPU数据同步(总线锁)等等)

这些操作都会导致此线程上的协程全部阻塞,进而导致影响sla。

而mmap在二级运储远大于内存时会造成大量的缺页中断,前面还讲到同一个进程的缺页中断还会抢夺mm->mmap_lock

scylladb

[11]中阐述了scylladb使用AIO而不是其他IO方法的原因,这里对于mmap的不满主要是以下几个方面:

  1. 内核中的预读策略和驱逐策略是通用的,虽然提供了madvisefadvise,但是还是无法满足部分应用的定制化需求,比如何时预读多少,那些内存合适淘汰,是否需要batch等
  2. 较大的内存开销,mmap需要大量的RSS,当文件较大时页表的内存开销非常大,一个页表项8字节,映射4kb空间,这需要映射文件大小的约0.2%,如果二级存储是1T,内存中有2G都会用于页表。
  3. 程序失去了对于IO的控制,当内核大量回写脏页时可以印象此时的读延迟;其次内核无法区分重要与不重要的IO操作,可能导致不重要的IO比重要的IO更先执行

现在还在使用mmap的理由

mongoDB

正如[2]中提到,mongoDB给出了在今天依旧使用mmap的合理理由。这是一个很好的思路,但是我个人认为仍旧存在mmap的固有缺陷。

随着硬件性能的提升,通用存储栈的成本比例越来越高,[2]中提到[13]的研究表明通过使用mmap进行 I/O,并在文件需要增长时预先分配一些额外的空间,他们可以实现几乎与完全不存在文件系统时的相同性能。在 MongoDB 的存储引擎 WiredTiger 中验证了这个想法,并获得了较为明显的性能提升。

这个思路其实本质是为了减少IO次数,mmap的基本访问原理如下:

  1. 如果包含文件数据的物理页仍在缓冲区高速缓存中并且页表条目位于 TLB 中,不需要OS参与
  2. 如果包含文件数据的页面仍在缓冲区高速缓存中,但 TLB 条目已被逐出,则需要陷入内核模式,遍历页表以找到该页表项,将其安装到 TLB 中
  3. 如果包含文件数据的页面不在缓冲区高速缓存中,则需要陷入内核模式,OS要求文件系统获取该页面,设置页表项,然后执行第二步

如果始终处于第一步则性能可以节省两个主要开销:

  1. read/write 系统调用开销
  2. 内核态向用户态拷贝数据的开销

此外文章还提到如果将数据从内存映射文件区域复制到另一个应用程序缓冲区,则通常会使用高度优化的基于 AVX 的 memcpy 实现。当通过系统调用将数据从内核空间复制到用户空间时,内核必须使用效率较低的实现,因为内核不使用 AVX 寄存器 [14]。

冷数据沉降

[16]利用mmap的几个优势:

  1. 拷贝快可能更快
  2. 减少拷贝次数
  3. 减少系统调用数

这样可以低开销的方式把内存冷数据沉降到二级存储中。

RUMA(Rewired User-space Memory Access)

[17]中提到的技术利用mmap在运行时重新分配虚拟内存地址到物理内存地址的映射,这篇论文没有看,但是看起来diaodiao的。

总结

以下几个原因是导致mmap不该被使用在DBMS中的主要原因:

  1. 性能问题,主要集中在三个方面 :
    a. TLB shootdown[11]
    b. kswapd 单线程驱逐瓶颈[12]
    c. 锁竞争,在二级存储远大于内存时,单线程和多线程mmap都无法利用全部的IO带宽
  2. 内存问题:mmap会创建与文件大小相同的虚拟内存,并创建VMA和页表,在文件较大时对内存影响较大
  3. 错误处理:很多DBMS使用内存不安全的语言编写,且存在CPU/内存错误导致比特跳变,页级别的校验和就显得非常重,但是mmap的回写是OS控制的,可能写入一个受损坏的页。
  4. I/O Stalls:mmap无法异步化,且任何操作都可能导致缺页,虽然mlock/madvise可以在部分情况下缓解,但是错误的提示可能导致性能大幅度下降,其次可以选择使用一个独立的线程负责预取,以防止主线程阻塞
  5. 事务不安全:无法控制何时下刷脏页,需要其他复杂的机制来保障
  6. 无法容器化部署:随着对象存储的发展,越来越到的云原生数据库用对象存储作为存储层,而mmap假设底层一定是磁盘。

引用:

  1. linux kernel 6.2 madvise.c
  2. Getting Storage Engines Ready for Fast Storage Devices
  3. Investigating Linux Performance with Off-CPU Flame Graphs
  4. Off-CPU分析
  5. https://rocksdb.org/docs/support/faq.html
  6. https://groups.google.com/g/leveldb/c/mkKRKA4XGb0
  7. mmap may slow down your Go app
  8. https://github.com/Super-long/RocketCo
  9. https://github.com/xiyou-linuxer/LUTF
  10. Different I/O Access Methods for Linux, What We Chose for ScyllaDB, and Why
  11. DiDi: Mitigating the Performance Impact of TLB Shootdowns Using a Shared TLB Directory,PACT 2011
  12. linux kswapd浅析
  13. Finding and Fixing Performance Pathologies in Persistent Memory Software Stacks,ASPLOS 2019
  14. Why mmap is faster than system calls
  15. Are You Sure You Want to Use MMAP in Your Database Management System? CIDR 2022
  16. Ailamaki. Enabling Efficient OS Paging for Main-Memory OLTP Databases, DaMoN 2013.
  17. RUMA has it: Rewired User-space Memory Access is Possible! vldb2016

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

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

相关文章

链表之第三回

欢迎来到我的:世界 该文章收入栏目:链表 希望作者的文章对你有所帮助,有不足的地方还请指正,大家一起学习交流 ! 目录 前言第一题:判断是否为环形链表第二题:找到两条链表的相交点第三题:返回…

调整逻辑卷的大小

调整逻辑卷的大小 将名字为 vo 的逻辑卷的大小调整到 500MiB ,确保文件系统的内容保持不变。 调整后的逻辑卷的大小范围 在 480MiB 到 520MiB 的范围内都是可以接受的。 lsblk lvresize -L 500M /dev/vg0/vo 4. 创建交换分区 向 serverb 添加一个额外的交换…

解决nginx的负载均衡下上传webshell的问题

目录 环境 问题 访问的ip会变动 执行命令的服务器未知 上传大文件损坏 深入内网 解决方案 环境 ps :现在已经拿下服务器了,要解决的是负载均衡问题, 以下是docker环境: 链接: https://pan.baidu.com/s/1cjMfyFbb50NuUtk6JNfXNQ?pwd1aqw 提…

HotSpot虚拟机之内存模型与线程安全

目录 一、线程内存模型 1. 内存模型 2. 内存模型操作 二、Happens-Before原则 三、Java线程 1. 线程实现方式 2. Java线程状态 四、Java线程安全 1. 线程安全程度 2. 锁优化 五、参考资料 一、线程内存模型 1. 内存模型 内存模型主要目的是定义共享变量的访问规则&…

iconfont的使用方法 | 踩过的坑

据 iconfont官网,其使用方法是 拷贝项目下面生成的fontclass代码,挑选相应图标并获取类名。 注意,这个fontclass代码是涵盖了你所有要使用的图标 起因:部分icont图标生效了,部分图标无效。 问题:fontclas…

邮件开发信技巧大公开!

特别是对外贸企业或者跨境电商企业来讲,写邮件开发信仍是一个常用的手。通过邮件开发信,企业可以很快地获得精准客户,同时扩展业务,进行营销活动。但是做过邮件群发的人可能都会遇到类似问题,比如邮件到达率低、邮件回…

Python框架【url_for 函数、重定向、响应内容、自定义响应、模板介绍、模板的使用、过滤器介绍、Jinja模板自带过滤器】(二)

👏作者简介:大家好,我是爱敲代码的小王,CSDN博客博主,Python小白 📕系列专栏:python入门到实战、Python爬虫开发、Python办公自动化、Python数据分析、Python前后端开发 📧如果文章知识点有错误…

大数据课程K3——Spark的常用案例

文章作者邮箱:yugongshiyesina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 掌握Spark的常用案例——WordCount; ⚪ 掌握Spark的常用案例——求平均值; ⚪ 掌握Spark的常用案例——求最大值和最小值; ⚪ 掌握…

​8th参考文献:[8]许少辉.乡村振兴战略下传统村落文化旅游设计[M]北京:中国建筑出版传媒,2022.

​8th参考文献:[8]许少辉.乡村振兴战略下传统村落文化旅游设计[M]北京:中国建筑出版传媒,2022&…

根据源码,模拟实现 RabbitMQ - 内存数据管理(4)

目录 一、内存数据管理 1.1、需求分析 1.2、实现 MemoryDataCenter 类 1.2.1、ConcurrentHashMap 数据管理 1.2.2、封装交换机操作 1.2.3、封装队列操作 1.2.4、封装绑定操作 1.2.5、封装消息操作 1.2.6、封装未确认消息操作 1.2.7、封装恢复数据操作 一、内存数据管理…

机器学习深度学习——NLP实战(情感分析模型——数据集)

👨‍🎓作者简介:一位即将上大四,正专攻机器学习的保研er 🌌上期文章:机器学习&&深度学习——BERT(来自transformer的双向编码器表示) 📚订阅专栏:机器…

【数据结构】_7.二叉树

目录 1.树形结构 1.1 树的概念 1.2 树的相关概念 1.3 树的表示 1.4 树在实际中的应用—表示文件系统的目录树结构 ​编辑​2.二叉树 2.1 概念 2.2 特殊二叉树 2.3 二叉树的性质 2.4 二叉树的存储结构 2.4.1 顺序存储结构(数组存储结构) 2.4.2…

LeetCode--HOT100题(36)

目录 题目描述:146. LRU 缓存(中等)题目接口解题思路代码 PS: 题目描述:146. LRU 缓存(中等) 请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。 实现 LRUCache 类: LRUCache…

引人关注的领域 ---- 信号稀疏表示

本篇文章是博主在人工智能等领域学习时,用于个人学习、研究或者欣赏使用,并基于博主对人工智能等领域的一些理解而记录的学习摘录和笔记,若有不当和侵权之处,指出后将会立即改正,还望谅解。文章分类在学习摘录和笔记专…

redis实战-缓存数据解决缓存与数据库数据一致性

缓存的定义 缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码。防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪,这在实际开发中对企业讲,对产品口碑,用户评价都是致命的;所以企业非常重视缓存…

基于蜉蝣算法优化的BP神经网络(预测应用) - 附代码

基于蜉蝣算法优化的BP神经网络(预测应用) - 附代码 文章目录 基于蜉蝣算法优化的BP神经网络(预测应用) - 附代码1.数据介绍2.蜉蝣优化BP神经网络2.1 BP神经网络参数设置2.2 蜉蝣算法应用 4.测试结果:5.Matlab代码 摘要…

MySQL8.0.26-Linux版安装

MySQL8.0.26-Linux版安装 1. 准备一台Linux服务器 云服务器或者虚拟机都可以; Linux的版本为 CentOS7; 2. 下载Linux版MySQL安装包 MySQL :: Download MySQL Community Server (Archived Versions) 3. 上传MySQL安装包 4. 创建目录,并解压 mkdir mysql ​ tar -xvf mysql-8…

网站老域名跳转到新域名有哪些方法?内网穿透内网主机让外网访问

在网站服务器变更及本地主机搭建时,我们经常会遇到老域名地址跳转到新URL的配置,一些朋友还会面对无公网IP让外网访问的问题。今天我们来了解下网站老域名跳转到新域名有哪些方法,以及如何通过内网穿透实现内网主机让外网访问。 网站老域名跳…

【Unity小技巧】Unity2D TileMap的探究(最简单,最全面的TileMap使用介绍)

文章目录 前言介绍一、TileMap简单的使用1、创建Unity工程2、Tilemap的使用2.1、导入素材图片2.2、切割图片2.3、创建画板2.4、创建瓦片2.5、创建网格2.6、在网格上刷瓦片2.7、解决瓦片没有占满格子的问题2.8、解决瓦片之间有缝隙的问题2.9、擦除瓦片2.10、区域瓦片绘制2.11、瓦…