andy在cmu15-445 中提出了不要在数据库系统中使用mmap
因为这个,在网络上找了许多关于 mmap 的文章,无一例外都是对 mmap 的褒奖, 对于mmap 可能带来的负面影响少之又少,因此整理了一下andy 的论文。
andy的论文
大佬的整理
Ravendb‘s ceo的回应
问题引出
通常情况下,数据库系统对于文件IO管理 有两种选择:
- 开发者自己实现
buffer pool 来管理文件io读入内存的数据 - 使用linux实现的
mmap 系统调用将文件直接映射到用户地址空间, 并且利用对开发者透明的page cache 来实现页面的换入换出
由于第二种方案,开发者不需要手动管理内存,实现起来简单,因此很多数据库系统曾经使用MMAP 来代替buffer pool , 但是由于一些问题导致它们最终弃用了MMAP(这一点也是论文的一个重要的论据),改为自己管理文件I/O。
理论介绍
buffer pool的原理
基于传统的IO方式,底层实际上通过调用read() 和write() 来实现。
- 将文件数据从硬盘读取到
bufferpool - 将数据复制到
缓冲区中 - 对于脏页面需要重新写回磁盘中,通过合理的置换算法清除缓冲区缓存
在不考虑socket的情况下,上述的过程发生了下面变化:用户态 和内核态 的变化。
- 用户进程通过
read() 方法向操作系统发起调用。上下文:用户态 -->内核态 - 将数据从硬盘拷贝到
缓冲区(buffer pool) 中 - cpu将缓冲区数据拷贝到
用户缓冲区中 ,上下文:内核态–>用户态,read()结束
以上过程发生了 2次上下文切换 和 2次拷贝
mmap的原理
对于mmap的原理,基于 0拷贝技术 。通过优化将数据从系统缓存 复制到用户缓存 这个过程,减少了1次拷贝。
使用 mmap 系统调用,可以将用户空间的 虚拟内存地址 与文件进行映射 , 对映射后的虚拟内存地址 进行读写 就等价于 对文件进行读写操作。
基本原理:mmap直接映射了缓存区的页缓存 ,而非磁盘中的文件本身。
论文中介绍了程序使用MMAP 访问文件,流程如下
- 程序开始调用 mmap (上下文:用户态–>内核态)
- 操作系统保留一部分虚拟地址空间(进程地址空间中),此时没有开始加载文件。
- 数据从硬盘中拷贝到了 读缓冲区中(系统缓存)
- 操作系统尝试在
系统缓存 中找到所需的 页面的物理地址 - 由于 内存页 不存在(缺页),因此触发了
页错误 ,开始从外部存储 将第3步获取的那部分内容加载到物理内存页中 - 操作系统在页表中添加一个条目,将
虚拟地址 映射到新的物理地址 - 上述操作使用的cpu核心将页表项加载到页表缓存(TLB)中(上下文: 内核态 --> 用户态 )
上面可以看出发生了,2次上下文切换 ,1次拷贝
当程序访问不在内存中的页面,os会将他们加载到内存中。如果页面缓存 已满,会根据置换算法逐出页面。
访问tlb很简单,但是删除tlb需要os法成 昂贵的处理器间中断 刷新,这被称为TLB击落 。 TLB击落 对后面实验有很大的影响。
mmap的api
作者给出了内存映射文件io的POSIX系统调用 。
- mmap:介绍了
MAP_SHARED 和MAP_PRIVATE 在可见性上的区别 - madvise:介绍了
MADV_NORMAL ,MADV_RANDOM 和MADV_SEQUENTIAL 在预读上的区别 - mlock:可以尝试性的锁住内存中的页面,一定程度上防止被写回存储。(然而并不是确定性的锁住)
- msync:将页面从内存写回存储的接口
mmap
主要讲述了MAP_SHARED 和MAP_PRIVATE 的区别。
此调用导致文件映射到DBMS 的虚拟地址空间,然后DBMS 可以使用普通的内存操作读取 或写入 文件内容。
- MAP_SHARED:将修改后的文件 重写 回底层文件
- MAP_PRIVATE:创建一个只有调用者可以访问的写时复制映射,不会重写回 底层文件。
madvise
介绍了MADV_NORMAL 和MADV_RANDOM ,MADV_SEQUENTIAL 三种标志的区别
- MADV_NORMAL:告诉linux系统执行默认操作,系统将会返回接下来
前15个页面 和后16个页面 。 即使只需要一个页面,系统也会返回32个页面,带来了极大的系统开销 - MADV_RANDOM:告诉系统,访问页面是随机的。
- MADV_SEQUENTIAL:告诉系统,我们将要顺序执行。
mlock
该调用允许dbms 将页面固定在内存中,并保证操作系统永远不会驱逐他
但是根据POSIX标准和linux实现,即使被固定了,操作系统随时会将脏页重写回磁盘中。因此,mlock 只能保证页面不会被驱逐,不能够保证脏页 不会被重写回磁盘中,这对于事务安全 有很大的影响。
msync
将内存范围更改显示刷新到 外部存储中 ,如果没有这个方法,DBMS 就没有其他方法将 更新 刷新到 外部存储中
两者的对比
表面上来看,两种io操作带来的开销
buffer pool :2次上下文切换,2次拷贝mmap :2次上下文切换,1次拷贝
看起来,似乎 mmap 更有吸引力,但下面将会给出 mmap 将会引发的问题。
同时,越来越多的数据库都在使用 mmap 之后,改用buffer pool
问题陈述
论文列举了四个使用MMAP可能引发的问题
问题一:事务安全
在基于MMAP 的DBMS 中修改页面会带来很大的挑战。
由于透明分页,操作系统可以随时将脏页重写回外部设备, 而不管写入事务是否已经提交。 DBMS无法阻止刷新,并且不会在刷新时收到任何警告 。因此,必须要用复杂的协议来确保透明分页 不会 违反事务安全保证 。
我们将更新的方法分为:
- 操作系统写时复制
- 用户空间写时复制
- 影子分页
操作系统写时复制(copy-on-write)
使用MAP_PRIVATE 标识位创建一个独立的写空间(物理内存复制),写操作在这个写空间执行,读操作仍读取原来的空间。同时使用WAL 保证写入操作被持久化。 事务提交后,将写空间新增内容复制到读空间,将相应的WAL复制到辅助存储中。
注意: 操作系统必须确保所有更新都写入到读空间中,才会删除私有空间。操作系统可以使用mremap 来定期收缩私有空间
用户空间写时复制(copy-on-write)
类似操作系统写时复制,不过写空间在用户空间开辟,同样写入WAL保证持久性。事务提交后,从用户空间写回读空间。
影子分页
影子分页类似第一种方法,不过没有WAL的参与,而是直接出来两份分页,一份只读的,另一份可写,事务提交就是在可写页表做msync之后再让只读页表可以读到最新的页。
具体流程:
- 分出两个页面,一个为主页面,一个为影子页面
- 对影子页面进行修改,将对应的修改重写回外部存储中
- 以影子页面作为新的主页面,原来的主页面作为影子页面
问题二:io停顿
缓冲池:可以使用异步io来避免查询执行期间阻塞线程。
mmap:可以会带来下面问题
- dbms不能异步读取。
- 操作系统可以对页面透明操作,会无警告的将页面逐出内存。导致缺页错误。
即,由于缓存的页面置换是由os控制的,所以无法控制page cache的换入换出,导致io停顿。
操作系统使用页缓存没有对数据库场景进行特别优化,所以使用的时候会影响性能
问题三:错误处理
缓冲池对于文件内容的校准可以以多个页面来做,mmap只能以单个页面来做,因为当前页面不受控制,可能随机被换出。
另外,指针错误可能会导致页面错误,缓冲池可以在刷新前检查文件内容来避免出错,而mmap只能直接刷新,不能检查
还有,mmap应对的系统调用会出现sigbus信号报错,而其他io方式的buffer pool可以更好的解决io错误
问题四:性能问题
业界普遍认为MMAP比传统的read/write更快,主要基于以下两点原因:
- 操作系统会在后台负责文件映射和页面错误,负责文件映射操作,并处理page falut的是内核,而不是应用程序
- MMAP帮助避免了用户空间的额外的复制操作,相应的减少了内存占用
接下来作者提出了他们的发现:MMAP相对于传统read/write I/O在目前高带宽的存储设备上是更差的。并指出了三点原因:
- 页表竞争
- 单线程的页面换出:页面换出是单线程的(使用kswapd),这点与cpu相关
- TLB击落 :当一个页面失效,每次都需要一个中断来做刷新操作,但是对于一个中断会损耗几千个时钟周期,是非常耗时的
实验说明
在这里,作者使用了两个访问方式来做实验:随机访问 ,循序访问
随机访问
如图给出了随机访问中,io读取和mmap三种读取方式中,100个线程每秒读取的页面数
可以看到,传统io读取显然更优,作者给出了三种原因解释
- tlb击落
- MMAP中操作系统仅使用单个进程(kswapd)进行页面驱逐,这会受到cpu的限制
- MMAP中操作系统必须同步页表,同步操作会与多个并发线程竞争
对于上面的三个问题,传统io都可以通过DBMS控制来给予 进程优先级 和 多进程并发
循序访问
主要带来的性能问题在于
- 随机访问的三个缺点
- 在循序访问中,MMAP不能访问多个SSD,而缓冲池在人为操作下可以
总结
mmap主要优点在于更好实现,但是性能相比于传统io设备,随着多角度分析会逐渐显露出来。
作者给予db开发人员以下建议 当你不应该在dbms中使用mmap的情况
- 需要事务安全的方式执行更新
- 希望在不阻塞慢速io的情况下处理页面错误,或者需要控制内存中的数据
- 关心错误处理并需要返回正确的结果
- 需要快速持久存储设备的高吞吐两
在使用mmap时需要注意
- 数据库或者工作集 适合内存并且工作负载是只读的
- 不关心数据一致性或长期的工程难题
|