01 背景
大页技术是操作系统中优化内存访问延迟的一种技术,其优化原理与 CPU TLB 硬件有直接关系,而其优化效果不仅受 CPU TLB 硬件影响,还需要看应用访存特点。只考虑 Arm 和 x86 两种平台,已知的大页技术包括透明大页、hugetlbfs、16k 和 64k 全局大页。在合适的场景,大页技术可以提升应用性能达 10% 以上,尤其是针对当前云上应用逐年增长的内存使用趋势,使用大页技术是其中重要的提升“性能-成本”比例的优化手段。透明大页(Transparent Huge Pages,THP)从 2011 年开始在 Linux 内核中已经支持起来,其通过一次性分配 2M 页填充进程页表,避免多次缺页开销,更深层次从硬件角度优化了 TLB 缺失开销,在最好情况下,对应用的优化效果达到 10% 左右。除以上优点外,透明大页(主要供堆栈使用)使用过度也会导致严重的内存碎片化、内存膨胀和内存利用率低等问题,这就是当前透明大页没有在数据库中使用的核心原因,只能感叹“卿本巧技,奈何有坑”。
代码大页在透明大页的基础上,将支持扩展到可执行二进制文件,包括进程二进制文件本身、共享库等可执行数据。与透明大页相比,由于代码大页仅将占比较低且有限的可执行文件页部分转换为大页,从根本上避开了内存碎片以及内存不足的问题。与此同时,由于代码类数据和普通堆栈数据访问热度对整体性能影响不同(主要指代码数据或堆栈数据访问缺页一次的性能影响),导致代码类数据使用大页所提升的性能远大于同样分量的透明大页。所以推广和完善代码大页相比透明大页更加简单和容易。
本文主要介绍我们的代码大页方案以及一些实验阶段性能测试。为了方便阅读,在这里简单归纳了一下 Linux 系统中大页的支持现状和和必要的数据库相关背景。
02 大页现状
当前 Linux 内核支持的大页包括 THP 和 hugetlb,其页大小分别是:
不考虑其他架构,在 x86 和 Arm 架构中,提到 THP,我们可以一股脑地认为其大小就是 2MB,当前内核暂时还不支持 1GB THP,技术上实现没有什么问题,社区隐隐约约也有人曾经发过相关补丁...。回到这里,Arm64 相比 x86,hugtlb 多两种大页支持,主要是 contiguous bit,该特性主要是针对 TLB entry 的优化(连续的 16 个 PTE/PMB,若其上的 PFN 也是连续的,cont bit 会将其使用的 16 个 TLB entry 优化仅占一个 TLB entry)。这里的 64KB 和 32MB 的由来就是 16*4KB 和 16*2MB。
另外,在全局页粒度的支持上,Arm64 也比 x86 更会玩,提供的 16KB (CONFIG_16KB 表示)和 64KB(CONFIG_64KB表示)两种选择,这两种页相比 4KB 页,在 TLB 和 cache 上都有明显的优化相关,从而优化宏观指标,当然也并非所有 benchmark 表现出性能提升,例如在 SPECjvm2008 和 stream 中,我们就发现有多项指标在使用 CONFIG_64KB 的时候有较严重的性能下降。除以上问题,还想再啰嗦一下,CONFIG_64KB 中,THP 的大小着实有点“吓人”,有 512MB。
因此,大伙对 16KB、64KB 还是又爱又恨。
03 Mysql、PostgreSQL 和 OceanBase
站在内存管理的角度,我们仅仅关心 Mysql、PostgreSQL 这些数据库用了多少内存、页缓存占多少、匿名页占多少以及代码段还有 iTLB/dTLB miss 到底高不高。当然还随便想知道 THP 有没有优化,下面几个是简单归纳的几点我们关系的数据库特点:
Mysql
- Mysql 是一个多线程模式的数据库,其代码段大小一般 18M 左右。
- THP 不敏感,打开 THP,大约仅有不到 3% 的性能提升。
- 跨 NUMA 敏感,本地虚拟机 32 核验证跨 NUMA 抖动在 5~7% 左右。
PostgreSQL
- 多进程模型,代码段大小大约 10M 左右。
- 应用 iTLB-load-misses 较高,大约 1.41% 左右。
OceanBase
- 多线程模型,代码段大小大约 200M~280M。
- 一般独占单机使用,性能验证过程中并发数要求高:128、1000、1500。
- THP 本地验证不敏感。
这些数据库大约至少有两个共同点:代码段大、iTLB Miss 高。本文也是基于这两个特征进行的优化,当然代码大页优化目标也不局限于这三种数据库。
04 代码大页
接着前面数据库背景介绍,这里直接开始代码大页方案。代码大页大致实现分为:整理结构、大致实现、填充功能简介还有最后的代码大页性能评估(包括 Mysql 和 PostgreSQL),最后是我们专门为解决 x86 平台设计的自适应功能。
4.1 整体结构与实现
基于透明大页异步整合大页(主要指 khugepaged 内核线程)的框架:
上图所展示的代码大页方案主要包括三个部分:
(1)映射首地址对齐(蓝色高亮):这个部分主要是在 elf binary 和 DSO 建立映射的过程中,优先考虑分配 2M 对齐的虚拟地址空间,便于映射到 2M 大页。
(2)异步 khugepaged 扫描整合以及加速(橙色高亮):与 THP 相似,单独设计用户态接口 hugetext_enabled 控制。复用 khugepaged 整合 2M 大页。此外,由于 hugetext 与 THP 共用 khugepaged,在 THP=always 时,也能整合部分符合条件的代码大页;关于加速部分,我们解决了在 THP 使能的场景中,代码段整合慢的问题,这也是我们改进 READ_ONLY_THP_FOR_FS 带来的挑战之一。
(3)DSO写回退(紫色高亮):对于 DSO 所建立的映射,内核屏蔽了 MAP_DENYWRITE,导致用户态可以写打开共享库文件(尽管一旦对该共享库写,进程多数情况会 core dump)。针对这种情况,在检测到该共享库存在写者时,对其 pagecache 进行清空;DSO 为什么有这么多顾虑可以跳转:https://developer.aliyun.com/article/863760
注意:首地址 2M 对齐的本意是 mmap addr = mmap pgoff (mod 2M),由于 elf binary 和 DSO 的可执行 LOAD 段的 pgoff 一般为 0,这里为了叙述方便,我们简称地址 2M 对齐。