mmap 和 disk I/O 在不同情形下的性能分析

mmap 和 disk I/O 在不同情形下的性能分析

mmap(memory mapped file)从字面意思来看,似乎是在节省每次同 disk 做 I/O 的巨额开销(expensive overhead),通过把 main memory 作为 cache 来提升 disk I/O 的效率。

但这样的认知完全是错的,因为 OS(operating system)已经为 disk I/O 提供了 page cache 这样的机制,来实现上述基于 main memory 的 cache 优化

如此,我们不免要进一步地追问,那 mmap 到底为何而存在?它能提供哪些优化?在什么样的场景下,mmap 会为 program 带来更好的性能?而什么情形下引入 mmap 反而会给 program 带来更大的 overhead ?

如上所述,由于 page cache 的存在,mmap 所提供的 cache 功能并不能作为其 performance 由于 disk I/O 的理由。于是,我们需要分别考察这两种机制的 data pipeline。

对于 mmap 来讲,它通过 system call mmap() 将 disk partition 映射为某段 virtual memory address。需要注意的是,mmap 采用了 copy-on-write 的 lazy execution 优化机制,并不会在第一次的 mapping 阶段就分配出 physical memory address。它要等到第一次对这段 virtual memory address 的访问时,通过引发 page fault 这样的 system exception,来间接触发 physical memory address 的分配。

也即是,每一次对新的 disk part 的映射,都会在它第一次 access 时引发一次 page fault。这里之所以要单独提出 page fault 这件事,是因为 page fault 通常具有 high expensive overhead,甚至会高过下面会提到的 user/kernel space 之间的 data copy。

对于 disk I/O 来讲,它首先需要为 disk 分配出相应的 main memory cache,也即是 page cache,供 disk I/O 做 batch read/write 的优化使用。显然,对于 OS 来讲,disk 作为外部的 hardware resource 只能在 kernel space 做交互。而 page cache 需要同 disk 做直接交互,这就意味着 page cache 是 kernel space 下的 data container。

对于 application 来讲,它们只能通过 system call read() 和 write() 来触发对 disk 的交互,而它们又在 user space 下,这就意味着 page cache 这样的在 kernel space 下的数据需要被 copy 到 user space 下的 application data buffer。 read() 需要从 kernel space –> user space 的 copy,而 write() 需要 user space –> kernel space 的 copy。

如此,如果我们单纯考察对于 data 的使用,即:data read 的部分,那么 mmap 和 disk I/O 这两种机制,分别会在什么样的情形下,会让各自具备更好的 performance 呢?

面对 engineering problem,“哪种方案更好” 通常不是一个好的问题的提法。更好的提问方式,应该是询问各自会在什么样的情形下展现出更好的 performance。

后面这种提法,更容易让你保持 open mind,更深入细致地弄清楚每种解决方案的使用情景、边界与适用机制,而不是无脑地神吹某种特定情形下的妥协方案。

须知,在工程领域,永远只有基于特定情形下的 balance,没有 silver bullet。

情形 0x01

假设我们每次读入的都是新数据,即:new disk data,那么这就意味,对于这些 new data 的使用,mmap 每一次都需要承受 page fault 带来的 overhead,而 disk I/O 需要承受 read() 带来的 user/kernel space copy 的 overhead。由于每次都是新数据,显然这些读入的数据都无法被复用,每一次都会周而复始地承担这两种开销。

之前我们提到过,page fault 的 overhead 是相当巨大的,通常会比 user/kernel space copy 的 overhead 来得高(可参考 Linus 在 email-list 中的回答:http://lkml.iu.edu/hypermail/linux/kernel/0802.0/1496.html)。于是,在这种情形下,每一次的 data access 都意味着 disk I/O 会被 mmap 来得高效。因此,这种情形下 disk I/O 一定是好得多的解决方案。

情形 0x02

基于此,我们再考虑「情形 0x01」的变形:不是每次读取新的数据,而是不断地重复读取相同 disk 的数据(假设读取的这部分数据可以被一个 memory page 容纳;不能容纳的情形,我们后面会继续分析讨论)。我们再来分析两种机制下的 data pipeline。

对于 mmap 来讲,由于每次 access 的都是同样的 disk data part,那么它只需要承担第一次的 page fault overhead,后面的 access 都可直接复用已经在 memory 中的数据,即变成了较为高效的 memory access。所以,mmap 的 N 次 access 的 overhead 就本等价于:1 次 page fault + (N-1) 次 memory access。

而对于 disk I/O 来讲,每次都调用 read() system call 来访问同样的数据(当然,你可以 argue,既然是同样的数据访问,为啥不使用一次 disk I/O,再将后续它保存到某个 variable 中,以便在 memory 中做复用?对于某些应用场景来讲,你并不能一开始就做出如此的判断;再来是,这样的优化属于 application layer 的优化,而我们当前仅集中于底层数据流的讨论。),就意味着每次都需要承担 user/kernel space copy 的 overhead。于是,N 次 access 的 overhead 就是:N 次 user-kernel-data-copy。

显然,当 N 大于等于 2 时,mmap 的 performance 会比 disk 好得多。

情形 0x03

再来,让我们考虑「情形 0x02」的变形:同样是不断地读取同一份数据,但 access 它的频率却非常低,例如每天一次。

表面上看,这种情形所经历的 data pipeline 应该同「情形 0x02」完全一样。可是,memory 的资源是非常宝贵的,OS 对 memory 的使用有相应的优化。当某块 memory 长久不被使用后,它将被 OS swap off 到 disk 中,将空出来的 memory 供其他 task 使用。

此时,mmap 出的 disk 部分因为长久未使用的关系,每次都会被 swap off 到 disk。而再使用它时,又必须重新加载到 memory,再经历 page default interrupt 等流程。此时,「情形 0x03」就等价为了「情形 0x01」,即:每次 access,mmap 要经历 page default interrupt,而 disk I/O 要经历 user/kernel space data copy。那么自然,disk I/O 成了更好的选择。

情形 0x04

最后,让我们考虑更为微妙的一种情形,「情形 0x02」的变形:同样是不断地读取同一份数据,但这份数据比较大,或者是 memory pressure 比较大,以至于会触发 OS 对部分 memory 的 swap off 的操作。

此时,memory 不得不将这一份 data 分批导入到 memory 来做 access。这种情形的 overhead 就较为微妙,因为它会根据 memory pressure 的状态,时不时地触发由于 swap off 而引发的 mmap 的再次 page default interrupt。

此时,相比于每一次 disk I/O 都需要经历 user/kernel space data copy,到底谁的 overhead 更大?我们不得而知,这取决于具体的 computer hardware 和 runtime memory pressure:

  • 当间歇性的 page default interrupt 的 overhead 大于每一次 user/kernel space data copy overhead 时,disk I/O 是更好的选择;
  • 而当间歇性的 page default interrupt overhead 小于 user/kernel space data copy overhead 时,mmap 是更好的选择。

综上所述,mmap 和 disk I/O 到底在什么样的情形下的 overhead 更低,取决于两部分:

  • Part I:mmap 「首次」访问所引发的 page default interrupt overhead。
  • Part II:disk I/O 「每次」访问的 user/kernel space data copy overhead。

降低「Part I」的 overhead 的方式,可以是在 memory 中复用数据,同时避免因为低频使用、memory pressure 而引起的 swap off,进而引发再次 page default interrupt overhead。

但如果无法明显分辨出「Part I」和「Part II」谁的 overhead 更大,则只能通过多次测试的「经验数据」来做指导,并没有什么通用的、确定的理论指导。

Remarks

1、有些材料上讲,Kafka 的高吞吐量来自于使用了 mmap,这是不正确的。Kafka 仅将 log index file 做成了 mmap,而并未将存储原始数据的 log file 做成 mmap。

由上述讨论可知,如果将 log file 做成 mmap,则在写入阶段会不断地引发 page default interrupt,其效率反而会远远低于 disk I/O。如果我们非要讨论数据持久化部分的高吞吐,那一定是来自于 OS 的 page cache 机制,让 disk I/O 变得高效。mmap 在这种情形下只会起到相反的作用。

由于有了 page cache 的存在,正常的 disk I/O 通常能够在大部分情况下成为较为优秀的解决方案。

2、使用 mmap 做优化的一个优秀例子是 ElasticSearch[1],它 使用 mmap 来存放 index[2](对应的 disk 文件称之为 segment)。

ElasticSearch 作为搜索引擎,最重要的一个 data structure 便是 inverted index,它需要被持久化到 disk 中做保存 [3]。但显然,使用 inverted index 的频率也是极高的,每次搜索都需要 inverted index 来完成。如此,如果一直使用 disk I/O 来做 read 操作,显然是极其低效的。

于是,ElasticSearch 选择将 segment disk 映射为 mmap 来做优化。此时,官方文档中特别指出,需要 disable OS 的 swapping 功能 [4]。如果从本文的视角来看,停止 OS 的 swap off 操作是极其必要的。因为一旦 segment 被 swap 到 disk,当它再被 reload 到 memory 时,就需要经历 disk page default interrupt 的 overhead。而它会 远远高于普通的 disk I/O。

3、mmap 除了能够在特定情形下提供数据复用的 performance improvement,还能作为 process 之间通信的桥梁。因为它将 disk 所映射到的 memory,并不私有于任何特定的 process,成了各个 process 之间的 share data。

也因为此,如果以 cache 的视角去考察它,它也不会因为某个特定 application 的重启(进而导致 process 的 kill 和重分配)而让 cache 失效,省去了 cache 从 cool 到 warm 的 overhead。

(更适合在 PC 上阅读的格式,可点击「阅读原文」)


引用链接

[1] ElasticSearch: https://www.elastic.co/guide/en/elasticsearch/reference/6.3/index.html
[2] 使用 mmap 来存放 index: https://www.elastic.co/guide/en/elasticsearch/reference/6.3/vm-max-map-count.html
[3] 被持久化到 disk 中做保存: https://www.yuque.com/terencexie/geekartt/es-index-store
[4] 需要 disable OS 的 swapping 功能: https://www.elastic.co/guide/en/elasticsearch/reference/6.3/setup-configuration-memory.html


近期回顾

Notes:对话李录,第十三届哥伦比亚大学中国商业论坛
「理性投资」被当做玄学的一个缘由
reverse problem 的不同应用


如果你喜欢我的文章或分享,请长按下面的二维码关注我的微信公众号,谢谢!

发表评论