mmap 性能分析与优化

最近项目中需要实现一个进程间共享的动态增长队列(单写多读),采用的是文件 mmap 的方案,有这么几点考虑:

  • 进程间可以共享 mmap 文件映射的内存页,省去额外的内核态到用户态的内存拷贝及可能的 IO 开销,且修改能“实时”被看到(页表已经建好时)
  • mmap 使用方便,就跟访问本地内存一样;而普通文件 read/write 处理动态增长的数据很麻烦
  • mmap 的数据能被 kernel 在后台自动同步回文件,解决了持久化的问题
  • 底层文件可以放在 tmpfs 上,这样性能就与正常的共享内存无异了(事实上 Linux 的 POSIX shared memory 就是这么实现的)

看起来不错,便捷、高效。鱼掌可以兼得,就是这么任性——直到上线后发现系统指标异常,一路怀疑到这里可能有性能问题,才知道事实并不尽然。

初步调优

我们测量了每个数据从入队列到出队列的时间差,数据分布见下表的第一行数据(Disk),其中大于 1ms 的毛刺点竟达到 3.8‰,均值和 90% 分位点也比预期的要高。初步猜测有两个因素可能影响比较大:

  • 底层存储在磁盘上,发生 major pagefault 时 IO 开销比较大
  • 运行环境系统比较繁忙,CPU 资源紧张
+-----------+-------+-----------+---------+----------+---------+
| configure | store | bind_core | >1ms(‰) | mean(us) | 90%(us) |
+-----------+-------+-----------+---------+----------+---------+
| Disk      | disk  | N         |     3.8 |       18 |       9 |
| Bind-d    | disk  | Y         |     1.6 |       19 |      12 |
| Tmpfs     | mem   | N         |     2.2 |       16 |       9 |
| Bind-m    | mem   | Y         |     0.5 |        9 |       7 |
+-----------+-------+-----------+---------+----------+---------+

随后我们分别尝试了用 tmpfs 替代磁盘,绑定进程运行的 CPU 核等不同配置组合。从上表可以看到绑核对毛刺影响较大,再加上用 tmpfs 存储,可以大幅度优化延迟。

其实我们创建 mmap 时,已经用了 MADV_SEQUENTIAL 来提示 kernel 我们是顺序访问,事实上也是如此。但如果这样有效的话,换成 tmpfs 并不能带来多大的加速,这和我们预期不符。所以有必要进一步搞清楚 mmap 的使用方式。

细化分析

我们先来梳理下 mmap 的机制。mmap 分两种,匿名的和有文件映射的,我们只讨论第二种。mmap 的语义是将指定文件区间映射到当前进程的虚拟地址空间,调用返回空间起始指针,后续对这段空间的内存读写就相当于对底层文件内容的读写。默认情况下,mmap 调用时并不会帮你把整个文件都映射进内存,而是按需分配页表:因为你的文件可能很大以至于超过可用内存大小;也可能你只需要随机访问其中一小部分,没必要都映射进来。
那么当你访问到一个尚未分配页表的虚拟地址,CPU 就会触发一次 page fault,当前进程进入相应的内核 page fault handler。在这里,内核需要

  1. 在文件系统中分配该文件区域对应的 block(如果还未分配的话)
  2. 分配一个空闲物理内存页
  3. 读取该段文件内容到对应物理内存页
  4. 更新内存页表,以建立物理内存页到虚拟内存页的映射

当然如果只是 minor page fault(比如访问的共享数据已经被其他进程加载进内存),就只需要第 4 步操作。除此外,大范围的 mmap 还有个问题就是会给 TLB 带来很大负担,影响到整个系统的性能。

从使用者的角度,mmap 的这几项开销的优化思路主要就是预处理了,比如预先分配文件内容(Prealloc),预先触发 page fault(Prefault),让操作系统协助预取(Prefetch)。实现预处理的手段也有好多种:

Prealloc

  • 调用 fallocate 预先分配文件空间

Prefetch

  • 调用 madvise 设置 MADV_SEQUENTIAL 策略(Seq)
  • 调用 madvise 设置 MADV_WILLNEED 策略(Need)

Prefault

  • mmap 调用中设置 MAP_POPULATE 标志位(Prefault)
  • 自行预先访问 mmap 出来的内存区间(ManualPrefault)

Prealloc 节省了步骤 1 的开销,Prefault 节省了步骤 1-4 的开销,Prefetch 最理想的情况下和 Prefault 效果一致。为此我们设计了一组对照实验,测量每一组配置下用 128 Bytes 的数据块去写入 256MB mmap 区间所需的时间,以模拟我们真实的使用模式。底层的存储都使用 tmpfs,我们还分别测量了 tmpfs 使用的内存与测试进程在不同/相同 NUMA node 的情况。

+-------------------------------+-----------+-----------+
|             name              | same_node | diff_node |
+-------------------------------+-----------+-----------+
| BM_MMap11ManualPrefault       |    48.127 |    63.045 |
| BM_MMap10ManualPrefaultNeed   |    48.294 |    62.709 |
| BM_MMap06PreallocPrefault     |    48.316 |    62.539 |
| BM_MMap03Prefault             |    48.421 |    62.744 |
| BM_MMap07PreallocPrefaultNeed |    48.527 |    62.970 |
| BM_MMap04PrefaultNeed         |    48.543 |    63.133 |
| BM_MMap05PrefaultSeq          |    48.570 |    62.954 |
| BM_MMap02Prealloc             |   106.558 |   152.471 |
| BM_MMap08PrefetchNeed         |   127.054 |   174.662 |
| BM_MMap09PrefetchSeq          |   128.844 |   173.948 |
| BM_MMap01                     |   129.806 |   174.084 |
+-------------------------------+-----------+-----------+

我们可以看到,Prefech 的行为没有明显的效果,主要是因为操作系统需要一定时间去做预取,并且这个行为我们是不可控的,所以也没有特别安排不同等待时间的实验。Prealloc 能带来 20% 不到的提升,这也是步骤 1 的开销。其他各种 Prefault 的组合效果差不多,大约 60% 出头,这基本就是 page fault 的所有开销了。ManualPrefault 效果更好应该是因为它同时也做了 cache prefetch。

感兴趣的同学可以跑下我的实验代码,比较不同环境的测试结果。

用户态动态预处理

从实验结果我们可以看到,Prefault 能极大地减少 mmap 的开销,但它的代价也是很大的——需要把整个文件都事先加载进来。对于我们这个动态增长队列的用法就更糟糕了,相当于我们必须加载进可能的最大队列长度,而实际上大多数队列只使用了一小部分空间,这就造成极大的浪费。

比较折衷的办法是把不可控的 kernel pretch 搬到用户态来:预先分配一小部分空间,在运行的过程中根据使用情况预先处理。每次预处理长度的算法可以根据实际应用具体调整,丰俭由人。由于 page fault 是在 kernel 态完成的,天然就是线程安全,所以多线程的预取实现起来很方便,也不会影响主线程的正常访问。

常见错误

在设计实验前,参考了些其他相关的测试代码,发现不少 madvise 的错误用法,一般有这么两种错误类型:

  1. 用 | 连接两个不同的策略。事实上,madvise 的策略枚举值是互斥的,不是比特标志位所以不能用 | 连用。这是一部分枚举值的定义:
# define MADV_NORMAL 0 /* No further special treatment. */
# define MADV_RANDOM 1 /* Expect random page references. */
# define MADV_SEQUENTIAL 2 /* Expect sequential page references. */
# define MADV_WILLNEED 3 /* Will need these pages. */
# define MADV_DONTNEED 4 /* Don't need these pages. */
  1. 连续调用多次 madvise 以实现多种策略混搭的效果。其实只有最后一条 madvise 语句生效。Linux glibc madvise 实现中,madvise 直接调用 syscall,并透传参数。 而在 Linux madvise syscall 代码中也可以看出每次调用都会清空并覆盖之前策略的设置。

另外一个是 man mmap 里对 MAP_POPULATE 的解释有歧义:

MAP_POPULATE is supported for private mappings only since Linux 2.6.23.

它其实想说的是,从 Linux 2.6.23 版本以后才开始支持私有映射,共享映射一直都是支持的。但乍一看很容易理解成从 Linux 2.6.23 版本以后只支持私有映射。吐槽的人不只我一个哦。

大页支持

传统页表大小只有 4KB,其实现代处理器架构可以处理更大的页表,从而减少访问同样内存大小所需的 page fault 数量和 TLB 压力。Linux 有两种大页支持,hugetlbpagetransparent hugepage。hugetlbpage 需要创建一个 hugetlb 文件系统,但是只能读不能写,不符合我们的需求;transparent hugepage 灵活一些,可以在 mount tmpfs 时指定 huge 参数,但我们现在生产环境版本还没有这项功能。

参考

  1. mmap-vs-reading-blocks
  2. kernel mail list
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,911评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,014评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 142,129评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,283评论 1 264
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,159评论 4 357
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,161评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,565评论 3 382
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,251评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,531评论 1 292
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,619评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,383评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,255评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,624评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,916评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,199评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,553评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,756评论 2 335

推荐阅读更多精彩内容